Spaces:
Sleeping
Sleeping
Deploy EventFlow to HF Spaces with Docker
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .editorconfig +18 -0
- .env.example +65 -0
- .gitattributes +11 -35
- .github/workflows/deploy.yml +51 -0
- .gitignore +24 -0
- Dockerfile +56 -0
- README.md +59 -10
- app/Console/Commands/ExpirePendingOrders.php +29 -0
- app/Http/Controllers/Admin/CategoryController.php +39 -0
- app/Http/Controllers/Admin/DashboardController.php +29 -0
- app/Http/Controllers/Admin/EventController.php +31 -0
- app/Http/Controllers/Admin/ReportController.php +41 -0
- app/Http/Controllers/Admin/UserController.php +42 -0
- app/Http/Controllers/Auth/AuthenticatedSessionController.php +47 -0
- app/Http/Controllers/Auth/ConfirmablePasswordController.php +40 -0
- app/Http/Controllers/Auth/EmailVerificationNotificationController.php +24 -0
- app/Http/Controllers/Auth/EmailVerificationPromptController.php +21 -0
- app/Http/Controllers/Auth/NewPasswordController.php +62 -0
- app/Http/Controllers/Auth/PasswordController.php +29 -0
- app/Http/Controllers/Auth/PasswordResetLinkController.php +44 -0
- app/Http/Controllers/Auth/RegisteredUserController.php +50 -0
- app/Http/Controllers/Auth/VerifyEmailController.php +27 -0
- app/Http/Controllers/CheckoutController.php +83 -0
- app/Http/Controllers/Controller.php +8 -0
- app/Http/Controllers/DashboardController.php +30 -0
- app/Http/Controllers/EventController.php +74 -0
- app/Http/Controllers/HomeController.php +28 -0
- app/Http/Controllers/MyTicketsController.php +42 -0
- app/Http/Controllers/OrdersController.php +73 -0
- app/Http/Controllers/Organizer/DashboardController.php +34 -0
- app/Http/Controllers/Organizer/EventController.php +123 -0
- app/Http/Controllers/Organizer/OrderController.php +71 -0
- app/Http/Controllers/Organizer/ScannerController.php +31 -0
- app/Http/Controllers/Organizer/TicketTierController.php +74 -0
- app/Http/Controllers/ProfileController.php +60 -0
- app/Http/Middleware/RoleMiddleware.php +23 -0
- app/Http/Requests/Auth/LoginRequest.php +85 -0
- app/Http/Requests/ProfileUpdateRequest.php +30 -0
- app/Models/Attendee.php +57 -0
- app/Models/Category.php +27 -0
- app/Models/Event.php +100 -0
- app/Models/Order.php +70 -0
- app/Models/OrderItem.php +35 -0
- app/Models/PromoCode.php +43 -0
- app/Models/TicketTier.php +53 -0
- app/Models/User.php +47 -0
- app/Providers/AppServiceProvider.php +24 -0
- app/Services/OrderService.php +154 -0
- app/Services/TicketService.php +77 -0
- artisan +18 -0
.editorconfig
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
root = true
|
| 2 |
+
|
| 3 |
+
[*]
|
| 4 |
+
charset = utf-8
|
| 5 |
+
end_of_line = lf
|
| 6 |
+
indent_size = 4
|
| 7 |
+
indent_style = space
|
| 8 |
+
insert_final_newline = true
|
| 9 |
+
trim_trailing_whitespace = true
|
| 10 |
+
|
| 11 |
+
[*.md]
|
| 12 |
+
trim_trailing_whitespace = false
|
| 13 |
+
|
| 14 |
+
[*.{yml,yaml}]
|
| 15 |
+
indent_size = 2
|
| 16 |
+
|
| 17 |
+
[compose.yaml]
|
| 18 |
+
indent_size = 4
|
.env.example
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
APP_NAME=Laravel
|
| 2 |
+
APP_ENV=local
|
| 3 |
+
APP_KEY=
|
| 4 |
+
APP_DEBUG=true
|
| 5 |
+
APP_URL=http://localhost
|
| 6 |
+
|
| 7 |
+
APP_LOCALE=en
|
| 8 |
+
APP_FALLBACK_LOCALE=en
|
| 9 |
+
APP_FAKER_LOCALE=en_US
|
| 10 |
+
|
| 11 |
+
APP_MAINTENANCE_DRIVER=file
|
| 12 |
+
# APP_MAINTENANCE_STORE=database
|
| 13 |
+
|
| 14 |
+
# PHP_CLI_SERVER_WORKERS=4
|
| 15 |
+
|
| 16 |
+
BCRYPT_ROUNDS=12
|
| 17 |
+
|
| 18 |
+
LOG_CHANNEL=stack
|
| 19 |
+
LOG_STACK=single
|
| 20 |
+
LOG_DEPRECATIONS_CHANNEL=null
|
| 21 |
+
LOG_LEVEL=debug
|
| 22 |
+
|
| 23 |
+
DB_CONNECTION=sqlite
|
| 24 |
+
# DB_HOST=127.0.0.1
|
| 25 |
+
# DB_PORT=3306
|
| 26 |
+
# DB_DATABASE=laravel
|
| 27 |
+
# DB_USERNAME=root
|
| 28 |
+
# DB_PASSWORD=
|
| 29 |
+
|
| 30 |
+
SESSION_DRIVER=database
|
| 31 |
+
SESSION_LIFETIME=120
|
| 32 |
+
SESSION_ENCRYPT=false
|
| 33 |
+
SESSION_PATH=/
|
| 34 |
+
SESSION_DOMAIN=null
|
| 35 |
+
|
| 36 |
+
BROADCAST_CONNECTION=log
|
| 37 |
+
FILESYSTEM_DISK=local
|
| 38 |
+
QUEUE_CONNECTION=database
|
| 39 |
+
|
| 40 |
+
CACHE_STORE=database
|
| 41 |
+
# CACHE_PREFIX=
|
| 42 |
+
|
| 43 |
+
MEMCACHED_HOST=127.0.0.1
|
| 44 |
+
|
| 45 |
+
REDIS_CLIENT=phpredis
|
| 46 |
+
REDIS_HOST=127.0.0.1
|
| 47 |
+
REDIS_PASSWORD=null
|
| 48 |
+
REDIS_PORT=6379
|
| 49 |
+
|
| 50 |
+
MAIL_MAILER=log
|
| 51 |
+
MAIL_SCHEME=null
|
| 52 |
+
MAIL_HOST=127.0.0.1
|
| 53 |
+
MAIL_PORT=2525
|
| 54 |
+
MAIL_USERNAME=null
|
| 55 |
+
MAIL_PASSWORD=null
|
| 56 |
+
MAIL_FROM_ADDRESS="hello@example.com"
|
| 57 |
+
MAIL_FROM_NAME="${APP_NAME}"
|
| 58 |
+
|
| 59 |
+
AWS_ACCESS_KEY_ID=
|
| 60 |
+
AWS_SECRET_ACCESS_KEY=
|
| 61 |
+
AWS_DEFAULT_REGION=us-east-1
|
| 62 |
+
AWS_BUCKET=
|
| 63 |
+
AWS_USE_PATH_STYLE_ENDPOINT=false
|
| 64 |
+
|
| 65 |
+
VITE_APP_NAME="${APP_NAME}"
|
.gitattributes
CHANGED
|
@@ -1,35 +1,11 @@
|
|
| 1 |
-
*
|
| 2 |
-
|
| 3 |
-
*.
|
| 4 |
-
*.
|
| 5 |
-
*.
|
| 6 |
-
*.
|
| 7 |
-
*.
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
-
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
-
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
-
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
-
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
-
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
-
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
-
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
-
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
-
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
-
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
-
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
-
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
-
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
-
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
-
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
-
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
-
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
-
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
-
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
-
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
-
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
-
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
-
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
| 1 |
+
* text=auto eol=lf
|
| 2 |
+
|
| 3 |
+
*.blade.php diff=html
|
| 4 |
+
*.css diff=css
|
| 5 |
+
*.html diff=html
|
| 6 |
+
*.md diff=markdown
|
| 7 |
+
*.php diff=php
|
| 8 |
+
|
| 9 |
+
/.github export-ignore
|
| 10 |
+
CHANGELOG.md export-ignore
|
| 11 |
+
.styleci.yml export-ignore
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.github/workflows/deploy.yml
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Build & Push Docker Image to GHCR
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
push:
|
| 5 |
+
branches: [main]
|
| 6 |
+
workflow_dispatch:
|
| 7 |
+
|
| 8 |
+
env:
|
| 9 |
+
REGISTRY: ghcr.io
|
| 10 |
+
IMAGE_NAME: ${{ github.repository }}
|
| 11 |
+
|
| 12 |
+
jobs:
|
| 13 |
+
build-and-push:
|
| 14 |
+
runs-on: ubuntu-latest
|
| 15 |
+
permissions:
|
| 16 |
+
contents: read
|
| 17 |
+
packages: write
|
| 18 |
+
|
| 19 |
+
steps:
|
| 20 |
+
- name: Checkout repository
|
| 21 |
+
uses: actions/checkout@v4
|
| 22 |
+
|
| 23 |
+
- name: Log in to GitHub Container Registry
|
| 24 |
+
uses: docker/login-action@v3
|
| 25 |
+
with:
|
| 26 |
+
registry: ${{ env.REGISTRY }}
|
| 27 |
+
username: ${{ github.actor }}
|
| 28 |
+
password: ${{ secrets.GITHUB_TOKEN }}
|
| 29 |
+
|
| 30 |
+
- name: Extract metadata (tags, labels)
|
| 31 |
+
id: meta
|
| 32 |
+
uses: docker/metadata-action@v5
|
| 33 |
+
with:
|
| 34 |
+
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
| 35 |
+
tags: |
|
| 36 |
+
type=raw,value=latest,enable={{is_default_branch}}
|
| 37 |
+
type=sha,prefix=,format=short
|
| 38 |
+
|
| 39 |
+
- name: Set up Docker Buildx
|
| 40 |
+
uses: docker/setup-buildx-action@v3
|
| 41 |
+
|
| 42 |
+
- name: Build and push Docker image
|
| 43 |
+
uses: docker/build-push-action@v5
|
| 44 |
+
with:
|
| 45 |
+
context: .
|
| 46 |
+
file: ./docker/Dockerfile.sumopod
|
| 47 |
+
push: true
|
| 48 |
+
tags: ${{ steps.meta.outputs.tags }}
|
| 49 |
+
labels: ${{ steps.meta.outputs.labels }}
|
| 50 |
+
cache-from: type=gha
|
| 51 |
+
cache-to: type=gha,mode=max
|
.gitignore
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
*.log
|
| 2 |
+
.DS_Store
|
| 3 |
+
.env
|
| 4 |
+
.env.backup
|
| 5 |
+
.env.production
|
| 6 |
+
.phpactor.json
|
| 7 |
+
.phpunit.result.cache
|
| 8 |
+
/.fleet
|
| 9 |
+
/.idea
|
| 10 |
+
/.nova
|
| 11 |
+
/.phpunit.cache
|
| 12 |
+
/.vscode
|
| 13 |
+
/.zed
|
| 14 |
+
/auth.json
|
| 15 |
+
/node_modules
|
| 16 |
+
/public/build
|
| 17 |
+
/public/hot
|
| 18 |
+
/public/storage
|
| 19 |
+
/storage/*.key
|
| 20 |
+
/storage/pail
|
| 21 |
+
/vendor
|
| 22 |
+
Homestead.json
|
| 23 |
+
Homestead.yaml
|
| 24 |
+
Thumbs.db
|
Dockerfile
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM php:8.4-apache
|
| 2 |
+
|
| 3 |
+
# Enable Apache modules and ensure correct MPM for mod_php
|
| 4 |
+
RUN a2dismod mpm_event mpm_worker || true \
|
| 5 |
+
&& a2enmod mpm_prefork rewrite headers
|
| 6 |
+
|
| 7 |
+
# Install system dependencies + PHP extensions (PostgreSQL)
|
| 8 |
+
RUN apt-get update && apt-get install -y \
|
| 9 |
+
git curl libpng-dev libonig-dev libxml2-dev libpq-dev libzip-dev \
|
| 10 |
+
libfreetype6-dev libjpeg-dev libicu-dev zip unzip \
|
| 11 |
+
&& docker-php-ext-configure gd --with-freetype --with-jpeg \
|
| 12 |
+
&& docker-php-ext-install pdo pdo_pgsql pgsql mbstring exif pcntl bcmath gd zip intl \
|
| 13 |
+
&& apt-get clean && rm -rf /var/lib/apt/lists/*
|
| 14 |
+
|
| 15 |
+
# PHP production settings
|
| 16 |
+
RUN cp /usr/local/etc/php/php.ini-production /usr/local/etc/php/php.ini \
|
| 17 |
+
&& sed -i 's/memory_limit = .*/memory_limit = 256M/' /usr/local/etc/php/php.ini \
|
| 18 |
+
&& sed -i 's/upload_max_filesize = .*/upload_max_filesize = 10M/' /usr/local/etc/php/php.ini \
|
| 19 |
+
&& sed -i 's/post_max_size = .*/post_max_size = 12M/' /usr/local/etc/php/php.ini \
|
| 20 |
+
&& sed -i 's/max_execution_time = .*/max_execution_time = 60/' /usr/local/etc/php/php.ini
|
| 21 |
+
|
| 22 |
+
# Install Composer
|
| 23 |
+
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
|
| 24 |
+
|
| 25 |
+
# Set Apache document root to Laravel's public directory
|
| 26 |
+
RUN sed -ri -e 's!/var/www/html!/var/www/public!g' /etc/apache2/sites-available/*.conf \
|
| 27 |
+
&& sed -ri -e 's!/var/www/!/var/www/public/!g' /etc/apache2/apache2.conf /etc/apache2/conf-available/*.conf
|
| 28 |
+
|
| 29 |
+
# Allow .htaccess overrides (required for Laravel routing)
|
| 30 |
+
RUN sed -i '/<Directory \/var\/www\/public\/>/,/<\/Directory>/ s/AllowOverride None/AllowOverride All/' /etc/apache2/apache2.conf \
|
| 31 |
+
|| echo "<Directory /var/www/public/>\n AllowOverride All\n Require all granted\n</Directory>" >> /etc/apache2/apache2.conf
|
| 32 |
+
|
| 33 |
+
WORKDIR /var/www
|
| 34 |
+
|
| 35 |
+
# Install PHP dependencies (production only)
|
| 36 |
+
COPY composer.json composer.lock* ./
|
| 37 |
+
RUN composer install --no-dev --no-scripts --no-autoloader --prefer-dist
|
| 38 |
+
|
| 39 |
+
# Copy application
|
| 40 |
+
COPY . /var/www
|
| 41 |
+
|
| 42 |
+
# Dump optimized autoloader
|
| 43 |
+
RUN composer dump-autoload --optimize
|
| 44 |
+
|
| 45 |
+
# HF Spaces runs as non-root (uid 1000), so open permissions
|
| 46 |
+
RUN mkdir -p storage/logs storage/framework/cache storage/framework/sessions storage/framework/views bootstrap/cache storage/app/public \
|
| 47 |
+
&& chmod -R 777 storage bootstrap/cache \
|
| 48 |
+
&& chmod -R 755 public
|
| 49 |
+
|
| 50 |
+
# Make start script executable
|
| 51 |
+
RUN chmod +x /var/www/docker/start-hfspace.sh
|
| 52 |
+
|
| 53 |
+
# HF Spaces expects port 7860
|
| 54 |
+
EXPOSE 7860
|
| 55 |
+
|
| 56 |
+
CMD ["bash", "/var/www/docker/start-hfspace.sh"]
|
README.md
CHANGED
|
@@ -1,10 +1,59 @@
|
|
| 1 |
-
---
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<p align="center"><a href="https://laravel.com" target="_blank"><img src="https://raw.githubusercontent.com/laravel/art/master/logo-lockup/5%20SVG/2%20CMYK/1%20Full%20Color/laravel-logolockup-cmyk-red.svg" width="400" alt="Laravel Logo"></a></p>
|
| 2 |
+
|
| 3 |
+
<p align="center">
|
| 4 |
+
<a href="https://github.com/laravel/framework/actions"><img src="https://github.com/laravel/framework/workflows/tests/badge.svg" alt="Build Status"></a>
|
| 5 |
+
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/dt/laravel/framework" alt="Total Downloads"></a>
|
| 6 |
+
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/v/laravel/framework" alt="Latest Stable Version"></a>
|
| 7 |
+
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/l/laravel/framework" alt="License"></a>
|
| 8 |
+
</p>
|
| 9 |
+
|
| 10 |
+
## About Laravel
|
| 11 |
+
|
| 12 |
+
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:
|
| 13 |
+
|
| 14 |
+
- [Simple, fast routing engine](https://laravel.com/docs/routing).
|
| 15 |
+
- [Powerful dependency injection container](https://laravel.com/docs/container).
|
| 16 |
+
- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage.
|
| 17 |
+
- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent).
|
| 18 |
+
- Database agnostic [schema migrations](https://laravel.com/docs/migrations).
|
| 19 |
+
- [Robust background job processing](https://laravel.com/docs/queues).
|
| 20 |
+
- [Real-time event broadcasting](https://laravel.com/docs/broadcasting).
|
| 21 |
+
|
| 22 |
+
Laravel is accessible, powerful, and provides tools required for large, robust applications.
|
| 23 |
+
|
| 24 |
+
## Learning Laravel
|
| 25 |
+
|
| 26 |
+
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. You can also check out [Laravel Learn](https://laravel.com/learn), where you will be guided through building a modern Laravel application.
|
| 27 |
+
|
| 28 |
+
If you don't feel like reading, [Laracasts](https://laracasts.com) can help. Laracasts contains thousands of video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library.
|
| 29 |
+
|
| 30 |
+
## Laravel Sponsors
|
| 31 |
+
|
| 32 |
+
We would like to extend our thanks to the following sponsors for funding Laravel development. If you are interested in becoming a sponsor, please visit the [Laravel Partners program](https://partners.laravel.com).
|
| 33 |
+
|
| 34 |
+
### Premium Partners
|
| 35 |
+
|
| 36 |
+
- **[Vehikl](https://vehikl.com)**
|
| 37 |
+
- **[Tighten Co.](https://tighten.co)**
|
| 38 |
+
- **[Kirschbaum Development Group](https://kirschbaumdevelopment.com)**
|
| 39 |
+
- **[64 Robots](https://64robots.com)**
|
| 40 |
+
- **[Curotec](https://www.curotec.com/services/technologies/laravel)**
|
| 41 |
+
- **[DevSquad](https://devsquad.com/hire-laravel-developers)**
|
| 42 |
+
- **[Redberry](https://redberry.international/laravel-development)**
|
| 43 |
+
- **[Active Logic](https://activelogic.com)**
|
| 44 |
+
|
| 45 |
+
## Contributing
|
| 46 |
+
|
| 47 |
+
Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions).
|
| 48 |
+
|
| 49 |
+
## Code of Conduct
|
| 50 |
+
|
| 51 |
+
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).
|
| 52 |
+
|
| 53 |
+
## Security Vulnerabilities
|
| 54 |
+
|
| 55 |
+
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.
|
| 56 |
+
|
| 57 |
+
## License
|
| 58 |
+
|
| 59 |
+
The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).
|
app/Console/Commands/ExpirePendingOrders.php
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
|
| 3 |
+
namespace App\Console\Commands;
|
| 4 |
+
|
| 5 |
+
use App\Models\Order;
|
| 6 |
+
use App\Services\OrderService;
|
| 7 |
+
use Illuminate\Console\Command;
|
| 8 |
+
|
| 9 |
+
class ExpirePendingOrders extends Command
|
| 10 |
+
{
|
| 11 |
+
protected $signature = 'orders:expire-pending';
|
| 12 |
+
protected $description = 'Expire pending orders that have passed their expiry time and restore stock';
|
| 13 |
+
|
| 14 |
+
public function handle(OrderService $orderService): int
|
| 15 |
+
{
|
| 16 |
+
$orders = Order::where('status', 'pending')
|
| 17 |
+
->where('expires_at', '<', now())
|
| 18 |
+
->get();
|
| 19 |
+
|
| 20 |
+
$count = 0;
|
| 21 |
+
foreach ($orders as $order) {
|
| 22 |
+
$orderService->expireOrder($order);
|
| 23 |
+
$count++;
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
$this->info("Expired {$count} pending orders.");
|
| 27 |
+
return Command::SUCCESS;
|
| 28 |
+
}
|
| 29 |
+
}
|
app/Http/Controllers/Admin/CategoryController.php
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
|
| 3 |
+
namespace App\Http\Controllers\Admin;
|
| 4 |
+
|
| 5 |
+
use App\Http\Controllers\Controller;
|
| 6 |
+
use App\Models\Category;
|
| 7 |
+
use Illuminate\Http\Request;
|
| 8 |
+
|
| 9 |
+
class CategoryController extends Controller
|
| 10 |
+
{
|
| 11 |
+
public function index()
|
| 12 |
+
{
|
| 13 |
+
$categories = Category::withCount('events')->orderBy('name')->paginate(20);
|
| 14 |
+
return view('admin.categories.index', compact('categories'));
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
public function store(Request $request)
|
| 18 |
+
{
|
| 19 |
+
$request->validate(['name' => 'required|string|max:100|unique:categories,name']);
|
| 20 |
+
Category::create(['name' => $request->name]);
|
| 21 |
+
return back()->with('success', 'Kategori berhasil ditambahkan.');
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
public function update(Request $request, Category $category)
|
| 25 |
+
{
|
| 26 |
+
$request->validate(['name' => 'required|string|max:100|unique:categories,name,' . $category->id]);
|
| 27 |
+
$category->update(['name' => $request->name]);
|
| 28 |
+
return back()->with('success', 'Kategori berhasil diperbarui.');
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
public function destroy(Category $category)
|
| 32 |
+
{
|
| 33 |
+
if ($category->events()->exists()) {
|
| 34 |
+
return back()->with('error', 'Kategori tidak bisa dihapus karena masih digunakan oleh event.');
|
| 35 |
+
}
|
| 36 |
+
$category->delete();
|
| 37 |
+
return back()->with('success', 'Kategori berhasil dihapus.');
|
| 38 |
+
}
|
| 39 |
+
}
|
app/Http/Controllers/Admin/DashboardController.php
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
|
| 3 |
+
namespace App\Http\Controllers\Admin;
|
| 4 |
+
|
| 5 |
+
use App\Http\Controllers\Controller;
|
| 6 |
+
use App\Models\Event;
|
| 7 |
+
use App\Models\Order;
|
| 8 |
+
use App\Models\User;
|
| 9 |
+
|
| 10 |
+
class DashboardController extends Controller
|
| 11 |
+
{
|
| 12 |
+
public function index()
|
| 13 |
+
{
|
| 14 |
+
$totalRevenue = Order::where('status', 'paid')->sum('total');
|
| 15 |
+
$totalOrders = Order::where('status', 'paid')->count();
|
| 16 |
+
$totalEvents = Event::count();
|
| 17 |
+
$totalUsers = User::count();
|
| 18 |
+
$pendingOrders = Order::where('status', 'pending')->count();
|
| 19 |
+
|
| 20 |
+
$recentOrders = Order::with('user', 'event')
|
| 21 |
+
->latest()
|
| 22 |
+
->take(10)
|
| 23 |
+
->get();
|
| 24 |
+
|
| 25 |
+
return view('admin.dashboard', compact(
|
| 26 |
+
'totalRevenue', 'totalOrders', 'totalEvents', 'totalUsers', 'pendingOrders', 'recentOrders'
|
| 27 |
+
));
|
| 28 |
+
}
|
| 29 |
+
}
|
app/Http/Controllers/Admin/EventController.php
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
|
| 3 |
+
namespace App\Http\Controllers\Admin;
|
| 4 |
+
|
| 5 |
+
use App\Http\Controllers\Controller;
|
| 6 |
+
use App\Models\Event;
|
| 7 |
+
use Illuminate\Http\Request;
|
| 8 |
+
|
| 9 |
+
class EventController extends Controller
|
| 10 |
+
{
|
| 11 |
+
public function index(Request $request)
|
| 12 |
+
{
|
| 13 |
+
$query = Event::with('organizer', 'category');
|
| 14 |
+
|
| 15 |
+
if ($request->filled('status')) {
|
| 16 |
+
$query->where('status', $request->status);
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
$events = $query->latest()->paginate(15);
|
| 20 |
+
|
| 21 |
+
return view('admin.events.index', compact('events'));
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
public function updateStatus(Request $request, Event $event)
|
| 25 |
+
{
|
| 26 |
+
$request->validate(['status' => 'required|in:draft,published,ended,cancelled']);
|
| 27 |
+
$event->update(['status' => $request->status]);
|
| 28 |
+
|
| 29 |
+
return back()->with('success', "Status event diupdate ke \"{$request->status}\".");
|
| 30 |
+
}
|
| 31 |
+
}
|
app/Http/Controllers/Admin/ReportController.php
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
|
| 3 |
+
namespace App\Http\Controllers\Admin;
|
| 4 |
+
|
| 5 |
+
use App\Http\Controllers\Controller;
|
| 6 |
+
use App\Models\Event;
|
| 7 |
+
use App\Models\Order;
|
| 8 |
+
use Illuminate\Http\Request;
|
| 9 |
+
use Illuminate\Support\Facades\DB;
|
| 10 |
+
|
| 11 |
+
class ReportController extends Controller
|
| 12 |
+
{
|
| 13 |
+
public function index()
|
| 14 |
+
{
|
| 15 |
+
// Monthly revenue (last 6 months)
|
| 16 |
+
$monthlyRevenue = Order::where('status', 'paid')
|
| 17 |
+
->where('paid_at', '>=', now()->subMonths(6))
|
| 18 |
+
->select(
|
| 19 |
+
DB::raw("DATE_FORMAT(paid_at, '%Y-%m') as month"),
|
| 20 |
+
DB::raw('SUM(total) as revenue'),
|
| 21 |
+
DB::raw('COUNT(*) as order_count')
|
| 22 |
+
)
|
| 23 |
+
->groupBy('month')
|
| 24 |
+
->orderBy('month')
|
| 25 |
+
->get();
|
| 26 |
+
|
| 27 |
+
// Top events by revenue
|
| 28 |
+
$topEvents = Event::withSum(['orders as revenue' => fn($q) => $q->where('status', 'paid')], 'total')
|
| 29 |
+
->withCount(['orders as paid_orders' => fn($q) => $q->where('status', 'paid')])
|
| 30 |
+
->orderByDesc('revenue')
|
| 31 |
+
->take(10)
|
| 32 |
+
->get();
|
| 33 |
+
|
| 34 |
+
// Order status breakdown
|
| 35 |
+
$statusBreakdown = Order::select('status', DB::raw('COUNT(*) as count'))
|
| 36 |
+
->groupBy('status')
|
| 37 |
+
->pluck('count', 'status');
|
| 38 |
+
|
| 39 |
+
return view('admin.reports', compact('monthlyRevenue', 'topEvents', 'statusBreakdown'));
|
| 40 |
+
}
|
| 41 |
+
}
|
app/Http/Controllers/Admin/UserController.php
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
|
| 3 |
+
namespace App\Http\Controllers\Admin;
|
| 4 |
+
|
| 5 |
+
use App\Http\Controllers\Controller;
|
| 6 |
+
use App\Models\User;
|
| 7 |
+
use Illuminate\Http\Request;
|
| 8 |
+
|
| 9 |
+
class UserController extends Controller
|
| 10 |
+
{
|
| 11 |
+
public function index(Request $request)
|
| 12 |
+
{
|
| 13 |
+
$query = User::query();
|
| 14 |
+
|
| 15 |
+
if ($request->filled('role')) {
|
| 16 |
+
$query->where('role', $request->role);
|
| 17 |
+
}
|
| 18 |
+
if ($request->filled('q')) {
|
| 19 |
+
$s = $request->q;
|
| 20 |
+
$query->where(fn($q) => $q->where('name', 'like', "%{$s}%")->orWhere('email', 'like', "%{$s}%"));
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
$users = $query->latest()->paginate(20);
|
| 24 |
+
return view('admin.users.index', compact('users'));
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
public function update(Request $request, User $user)
|
| 28 |
+
{
|
| 29 |
+
$request->validate(['role' => 'required|in:admin,organizer,customer']);
|
| 30 |
+
$user->update(['role' => $request->role]);
|
| 31 |
+
return back()->with('success', "Role user \"{$user->name}\" berhasil diupdate ke {$request->role}.");
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
public function destroy(User $user)
|
| 35 |
+
{
|
| 36 |
+
if ($user->id === auth()->id()) {
|
| 37 |
+
return back()->with('error', 'Tidak bisa menghapus akun sendiri.');
|
| 38 |
+
}
|
| 39 |
+
$user->delete();
|
| 40 |
+
return back()->with('success', 'User berhasil dihapus.');
|
| 41 |
+
}
|
| 42 |
+
}
|
app/Http/Controllers/Auth/AuthenticatedSessionController.php
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
|
| 3 |
+
namespace App\Http\Controllers\Auth;
|
| 4 |
+
|
| 5 |
+
use App\Http\Controllers\Controller;
|
| 6 |
+
use App\Http\Requests\Auth\LoginRequest;
|
| 7 |
+
use Illuminate\Http\RedirectResponse;
|
| 8 |
+
use Illuminate\Http\Request;
|
| 9 |
+
use Illuminate\Support\Facades\Auth;
|
| 10 |
+
use Illuminate\View\View;
|
| 11 |
+
|
| 12 |
+
class AuthenticatedSessionController extends Controller
|
| 13 |
+
{
|
| 14 |
+
/**
|
| 15 |
+
* Display the login view.
|
| 16 |
+
*/
|
| 17 |
+
public function create(): View
|
| 18 |
+
{
|
| 19 |
+
return view('auth.login');
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
/**
|
| 23 |
+
* Handle an incoming authentication request.
|
| 24 |
+
*/
|
| 25 |
+
public function store(LoginRequest $request): RedirectResponse
|
| 26 |
+
{
|
| 27 |
+
$request->authenticate();
|
| 28 |
+
|
| 29 |
+
$request->session()->regenerate();
|
| 30 |
+
|
| 31 |
+
return redirect()->intended(route('dashboard', absolute: false));
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
/**
|
| 35 |
+
* Destroy an authenticated session.
|
| 36 |
+
*/
|
| 37 |
+
public function destroy(Request $request): RedirectResponse
|
| 38 |
+
{
|
| 39 |
+
Auth::guard('web')->logout();
|
| 40 |
+
|
| 41 |
+
$request->session()->invalidate();
|
| 42 |
+
|
| 43 |
+
$request->session()->regenerateToken();
|
| 44 |
+
|
| 45 |
+
return redirect('/');
|
| 46 |
+
}
|
| 47 |
+
}
|
app/Http/Controllers/Auth/ConfirmablePasswordController.php
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
|
| 3 |
+
namespace App\Http\Controllers\Auth;
|
| 4 |
+
|
| 5 |
+
use App\Http\Controllers\Controller;
|
| 6 |
+
use Illuminate\Http\RedirectResponse;
|
| 7 |
+
use Illuminate\Http\Request;
|
| 8 |
+
use Illuminate\Support\Facades\Auth;
|
| 9 |
+
use Illuminate\Validation\ValidationException;
|
| 10 |
+
use Illuminate\View\View;
|
| 11 |
+
|
| 12 |
+
class ConfirmablePasswordController extends Controller
|
| 13 |
+
{
|
| 14 |
+
/**
|
| 15 |
+
* Show the confirm password view.
|
| 16 |
+
*/
|
| 17 |
+
public function show(): View
|
| 18 |
+
{
|
| 19 |
+
return view('auth.confirm-password');
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
/**
|
| 23 |
+
* Confirm the user's password.
|
| 24 |
+
*/
|
| 25 |
+
public function store(Request $request): RedirectResponse
|
| 26 |
+
{
|
| 27 |
+
if (! Auth::guard('web')->validate([
|
| 28 |
+
'email' => $request->user()->email,
|
| 29 |
+
'password' => $request->password,
|
| 30 |
+
])) {
|
| 31 |
+
throw ValidationException::withMessages([
|
| 32 |
+
'password' => __('auth.password'),
|
| 33 |
+
]);
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
$request->session()->put('auth.password_confirmed_at', time());
|
| 37 |
+
|
| 38 |
+
return redirect()->intended(route('dashboard', absolute: false));
|
| 39 |
+
}
|
| 40 |
+
}
|
app/Http/Controllers/Auth/EmailVerificationNotificationController.php
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
|
| 3 |
+
namespace App\Http\Controllers\Auth;
|
| 4 |
+
|
| 5 |
+
use App\Http\Controllers\Controller;
|
| 6 |
+
use Illuminate\Http\RedirectResponse;
|
| 7 |
+
use Illuminate\Http\Request;
|
| 8 |
+
|
| 9 |
+
class EmailVerificationNotificationController extends Controller
|
| 10 |
+
{
|
| 11 |
+
/**
|
| 12 |
+
* Send a new email verification notification.
|
| 13 |
+
*/
|
| 14 |
+
public function store(Request $request): RedirectResponse
|
| 15 |
+
{
|
| 16 |
+
if ($request->user()->hasVerifiedEmail()) {
|
| 17 |
+
return redirect()->intended(route('dashboard', absolute: false));
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
$request->user()->sendEmailVerificationNotification();
|
| 21 |
+
|
| 22 |
+
return back()->with('status', 'verification-link-sent');
|
| 23 |
+
}
|
| 24 |
+
}
|
app/Http/Controllers/Auth/EmailVerificationPromptController.php
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
|
| 3 |
+
namespace App\Http\Controllers\Auth;
|
| 4 |
+
|
| 5 |
+
use App\Http\Controllers\Controller;
|
| 6 |
+
use Illuminate\Http\RedirectResponse;
|
| 7 |
+
use Illuminate\Http\Request;
|
| 8 |
+
use Illuminate\View\View;
|
| 9 |
+
|
| 10 |
+
class EmailVerificationPromptController extends Controller
|
| 11 |
+
{
|
| 12 |
+
/**
|
| 13 |
+
* Display the email verification prompt.
|
| 14 |
+
*/
|
| 15 |
+
public function __invoke(Request $request): RedirectResponse|View
|
| 16 |
+
{
|
| 17 |
+
return $request->user()->hasVerifiedEmail()
|
| 18 |
+
? redirect()->intended(route('dashboard', absolute: false))
|
| 19 |
+
: view('auth.verify-email');
|
| 20 |
+
}
|
| 21 |
+
}
|
app/Http/Controllers/Auth/NewPasswordController.php
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
|
| 3 |
+
namespace App\Http\Controllers\Auth;
|
| 4 |
+
|
| 5 |
+
use App\Http\Controllers\Controller;
|
| 6 |
+
use App\Models\User;
|
| 7 |
+
use Illuminate\Auth\Events\PasswordReset;
|
| 8 |
+
use Illuminate\Http\RedirectResponse;
|
| 9 |
+
use Illuminate\Http\Request;
|
| 10 |
+
use Illuminate\Support\Facades\Hash;
|
| 11 |
+
use Illuminate\Support\Facades\Password;
|
| 12 |
+
use Illuminate\Support\Str;
|
| 13 |
+
use Illuminate\Validation\Rules;
|
| 14 |
+
use Illuminate\View\View;
|
| 15 |
+
|
| 16 |
+
class NewPasswordController extends Controller
|
| 17 |
+
{
|
| 18 |
+
/**
|
| 19 |
+
* Display the password reset view.
|
| 20 |
+
*/
|
| 21 |
+
public function create(Request $request): View
|
| 22 |
+
{
|
| 23 |
+
return view('auth.reset-password', ['request' => $request]);
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
/**
|
| 27 |
+
* Handle an incoming new password request.
|
| 28 |
+
*
|
| 29 |
+
* @throws \Illuminate\Validation\ValidationException
|
| 30 |
+
*/
|
| 31 |
+
public function store(Request $request): RedirectResponse
|
| 32 |
+
{
|
| 33 |
+
$request->validate([
|
| 34 |
+
'token' => ['required'],
|
| 35 |
+
'email' => ['required', 'email'],
|
| 36 |
+
'password' => ['required', 'confirmed', Rules\Password::defaults()],
|
| 37 |
+
]);
|
| 38 |
+
|
| 39 |
+
// Here we will attempt to reset the user's password. If it is successful we
|
| 40 |
+
// will update the password on an actual user model and persist it to the
|
| 41 |
+
// database. Otherwise we will parse the error and return the response.
|
| 42 |
+
$status = Password::reset(
|
| 43 |
+
$request->only('email', 'password', 'password_confirmation', 'token'),
|
| 44 |
+
function (User $user) use ($request) {
|
| 45 |
+
$user->forceFill([
|
| 46 |
+
'password' => Hash::make($request->password),
|
| 47 |
+
'remember_token' => Str::random(60),
|
| 48 |
+
])->save();
|
| 49 |
+
|
| 50 |
+
event(new PasswordReset($user));
|
| 51 |
+
}
|
| 52 |
+
);
|
| 53 |
+
|
| 54 |
+
// If the password was successfully reset, we will redirect the user back to
|
| 55 |
+
// the application's home authenticated view. If there is an error we can
|
| 56 |
+
// redirect them back to where they came from with their error message.
|
| 57 |
+
return $status == Password::PASSWORD_RESET
|
| 58 |
+
? redirect()->route('login')->with('status', __($status))
|
| 59 |
+
: back()->withInput($request->only('email'))
|
| 60 |
+
->withErrors(['email' => __($status)]);
|
| 61 |
+
}
|
| 62 |
+
}
|
app/Http/Controllers/Auth/PasswordController.php
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
|
| 3 |
+
namespace App\Http\Controllers\Auth;
|
| 4 |
+
|
| 5 |
+
use App\Http\Controllers\Controller;
|
| 6 |
+
use Illuminate\Http\RedirectResponse;
|
| 7 |
+
use Illuminate\Http\Request;
|
| 8 |
+
use Illuminate\Support\Facades\Hash;
|
| 9 |
+
use Illuminate\Validation\Rules\Password;
|
| 10 |
+
|
| 11 |
+
class PasswordController extends Controller
|
| 12 |
+
{
|
| 13 |
+
/**
|
| 14 |
+
* Update the user's password.
|
| 15 |
+
*/
|
| 16 |
+
public function update(Request $request): RedirectResponse
|
| 17 |
+
{
|
| 18 |
+
$validated = $request->validateWithBag('updatePassword', [
|
| 19 |
+
'current_password' => ['required', 'current_password'],
|
| 20 |
+
'password' => ['required', Password::defaults(), 'confirmed'],
|
| 21 |
+
]);
|
| 22 |
+
|
| 23 |
+
$request->user()->update([
|
| 24 |
+
'password' => Hash::make($validated['password']),
|
| 25 |
+
]);
|
| 26 |
+
|
| 27 |
+
return back()->with('status', 'password-updated');
|
| 28 |
+
}
|
| 29 |
+
}
|
app/Http/Controllers/Auth/PasswordResetLinkController.php
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
|
| 3 |
+
namespace App\Http\Controllers\Auth;
|
| 4 |
+
|
| 5 |
+
use App\Http\Controllers\Controller;
|
| 6 |
+
use Illuminate\Http\RedirectResponse;
|
| 7 |
+
use Illuminate\Http\Request;
|
| 8 |
+
use Illuminate\Support\Facades\Password;
|
| 9 |
+
use Illuminate\View\View;
|
| 10 |
+
|
| 11 |
+
class PasswordResetLinkController extends Controller
|
| 12 |
+
{
|
| 13 |
+
/**
|
| 14 |
+
* Display the password reset link request view.
|
| 15 |
+
*/
|
| 16 |
+
public function create(): View
|
| 17 |
+
{
|
| 18 |
+
return view('auth.forgot-password');
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
/**
|
| 22 |
+
* Handle an incoming password reset link request.
|
| 23 |
+
*
|
| 24 |
+
* @throws \Illuminate\Validation\ValidationException
|
| 25 |
+
*/
|
| 26 |
+
public function store(Request $request): RedirectResponse
|
| 27 |
+
{
|
| 28 |
+
$request->validate([
|
| 29 |
+
'email' => ['required', 'email'],
|
| 30 |
+
]);
|
| 31 |
+
|
| 32 |
+
// We will send the password reset link to this user. Once we have attempted
|
| 33 |
+
// to send the link, we will examine the response then see the message we
|
| 34 |
+
// need to show to the user. Finally, we'll send out a proper response.
|
| 35 |
+
$status = Password::sendResetLink(
|
| 36 |
+
$request->only('email')
|
| 37 |
+
);
|
| 38 |
+
|
| 39 |
+
return $status == Password::RESET_LINK_SENT
|
| 40 |
+
? back()->with('status', __($status))
|
| 41 |
+
: back()->withInput($request->only('email'))
|
| 42 |
+
->withErrors(['email' => __($status)]);
|
| 43 |
+
}
|
| 44 |
+
}
|
app/Http/Controllers/Auth/RegisteredUserController.php
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
|
| 3 |
+
namespace App\Http\Controllers\Auth;
|
| 4 |
+
|
| 5 |
+
use App\Http\Controllers\Controller;
|
| 6 |
+
use App\Models\User;
|
| 7 |
+
use Illuminate\Auth\Events\Registered;
|
| 8 |
+
use Illuminate\Http\RedirectResponse;
|
| 9 |
+
use Illuminate\Http\Request;
|
| 10 |
+
use Illuminate\Support\Facades\Auth;
|
| 11 |
+
use Illuminate\Support\Facades\Hash;
|
| 12 |
+
use Illuminate\Validation\Rules;
|
| 13 |
+
use Illuminate\View\View;
|
| 14 |
+
|
| 15 |
+
class RegisteredUserController extends Controller
|
| 16 |
+
{
|
| 17 |
+
/**
|
| 18 |
+
* Display the registration view.
|
| 19 |
+
*/
|
| 20 |
+
public function create(): View
|
| 21 |
+
{
|
| 22 |
+
return view('auth.register');
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
/**
|
| 26 |
+
* Handle an incoming registration request.
|
| 27 |
+
*
|
| 28 |
+
* @throws \Illuminate\Validation\ValidationException
|
| 29 |
+
*/
|
| 30 |
+
public function store(Request $request): RedirectResponse
|
| 31 |
+
{
|
| 32 |
+
$request->validate([
|
| 33 |
+
'name' => ['required', 'string', 'max:255'],
|
| 34 |
+
'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:'.User::class],
|
| 35 |
+
'password' => ['required', 'confirmed', Rules\Password::defaults()],
|
| 36 |
+
]);
|
| 37 |
+
|
| 38 |
+
$user = User::create([
|
| 39 |
+
'name' => $request->name,
|
| 40 |
+
'email' => $request->email,
|
| 41 |
+
'password' => Hash::make($request->password),
|
| 42 |
+
]);
|
| 43 |
+
|
| 44 |
+
event(new Registered($user));
|
| 45 |
+
|
| 46 |
+
Auth::login($user);
|
| 47 |
+
|
| 48 |
+
return redirect(route('dashboard', absolute: false));
|
| 49 |
+
}
|
| 50 |
+
}
|
app/Http/Controllers/Auth/VerifyEmailController.php
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
|
| 3 |
+
namespace App\Http\Controllers\Auth;
|
| 4 |
+
|
| 5 |
+
use App\Http\Controllers\Controller;
|
| 6 |
+
use Illuminate\Auth\Events\Verified;
|
| 7 |
+
use Illuminate\Foundation\Auth\EmailVerificationRequest;
|
| 8 |
+
use Illuminate\Http\RedirectResponse;
|
| 9 |
+
|
| 10 |
+
class VerifyEmailController extends Controller
|
| 11 |
+
{
|
| 12 |
+
/**
|
| 13 |
+
* Mark the authenticated user's email address as verified.
|
| 14 |
+
*/
|
| 15 |
+
public function __invoke(EmailVerificationRequest $request): RedirectResponse
|
| 16 |
+
{
|
| 17 |
+
if ($request->user()->hasVerifiedEmail()) {
|
| 18 |
+
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
if ($request->user()->markEmailAsVerified()) {
|
| 22 |
+
event(new Verified($request->user()));
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
|
| 26 |
+
}
|
| 27 |
+
}
|
app/Http/Controllers/CheckoutController.php
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
|
| 3 |
+
namespace App\Http\Controllers;
|
| 4 |
+
|
| 5 |
+
use App\Models\Event;
|
| 6 |
+
use App\Models\Order;
|
| 7 |
+
use App\Services\OrderService;
|
| 8 |
+
use App\Services\TicketService;
|
| 9 |
+
use Illuminate\Http\Request;
|
| 10 |
+
|
| 11 |
+
class CheckoutController extends Controller
|
| 12 |
+
{
|
| 13 |
+
public function store(Request $request, Event $event, OrderService $orderService)
|
| 14 |
+
{
|
| 15 |
+
$request->validate([
|
| 16 |
+
'tiers' => 'required|array|min:1',
|
| 17 |
+
'tiers.*.tier_id' => 'required|exists:ticket_tiers,id',
|
| 18 |
+
'tiers.*.qty' => 'required|integer|min:1',
|
| 19 |
+
'promo_code' => 'nullable|string|max:32',
|
| 20 |
+
]);
|
| 21 |
+
|
| 22 |
+
// Filter out zero-qty tiers
|
| 23 |
+
$items = collect($request->tiers)->filter(fn($t) => ($t['qty'] ?? 0) > 0)->values()->toArray();
|
| 24 |
+
|
| 25 |
+
if (empty($items)) {
|
| 26 |
+
return back()->with('error', 'Pilih minimal 1 tiket.');
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
try {
|
| 30 |
+
$order = $orderService->createOrder(auth()->user(), $event, $items, $request->promo_code);
|
| 31 |
+
return redirect()->route('checkout.summary', $order);
|
| 32 |
+
} catch (\Exception $e) {
|
| 33 |
+
return back()->with('error', $e->getMessage());
|
| 34 |
+
}
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
public function summary(Order $order)
|
| 38 |
+
{
|
| 39 |
+
abort_unless($order->user_id === auth()->id(), 403);
|
| 40 |
+
$order->load('event', 'items.ticketTier', 'user');
|
| 41 |
+
|
| 42 |
+
// Check if already expired
|
| 43 |
+
if ($order->isExpirable()) {
|
| 44 |
+
app(OrderService::class)->expireOrder($order);
|
| 45 |
+
return redirect()->route('events.show', $order->event->slug)
|
| 46 |
+
->with('error', 'Order sudah expired. Silakan order ulang.');
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
return view('checkout.summary', compact('order'));
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
public function pay(Order $order, TicketService $ticketService)
|
| 53 |
+
{
|
| 54 |
+
abort_unless($order->user_id === auth()->id(), 403);
|
| 55 |
+
|
| 56 |
+
if (!$order->isPending()) {
|
| 57 |
+
return redirect()->route('orders.show', $order)
|
| 58 |
+
->with('error', 'Order tidak dalam status pending.');
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
// Check expiry
|
| 62 |
+
if ($order->isExpirable()) {
|
| 63 |
+
app(OrderService::class)->expireOrder($order);
|
| 64 |
+
return redirect()->route('events.show', $order->event->slug)
|
| 65 |
+
->with('error', 'Order sudah expired. Silakan order ulang.');
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
try {
|
| 69 |
+
$ticketService->processPayment($order, 'mock_gateway');
|
| 70 |
+
return redirect()->route('checkout.success', $order);
|
| 71 |
+
} catch (\Exception $e) {
|
| 72 |
+
return back()->with('error', $e->getMessage());
|
| 73 |
+
}
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
public function success(Order $order)
|
| 77 |
+
{
|
| 78 |
+
abort_unless($order->user_id === auth()->id(), 403);
|
| 79 |
+
$order->load('event', 'items.ticketTier', 'attendees');
|
| 80 |
+
|
| 81 |
+
return view('checkout.success', compact('order'));
|
| 82 |
+
}
|
| 83 |
+
}
|
app/Http/Controllers/Controller.php
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
|
| 3 |
+
namespace App\Http\Controllers;
|
| 4 |
+
|
| 5 |
+
abstract class Controller
|
| 6 |
+
{
|
| 7 |
+
//
|
| 8 |
+
}
|
app/Http/Controllers/DashboardController.php
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
|
| 3 |
+
namespace App\Http\Controllers;
|
| 4 |
+
|
| 5 |
+
use App\Models\Order;
|
| 6 |
+
use Illuminate\Http\Request;
|
| 7 |
+
|
| 8 |
+
class DashboardController extends Controller
|
| 9 |
+
{
|
| 10 |
+
public function index()
|
| 11 |
+
{
|
| 12 |
+
$user = auth()->user();
|
| 13 |
+
|
| 14 |
+
$activeTickets = $user->orders()
|
| 15 |
+
->where('status', 'paid')
|
| 16 |
+
->withCount('attendees')
|
| 17 |
+
->get()
|
| 18 |
+
->sum('attendees_count');
|
| 19 |
+
|
| 20 |
+
$totalOrders = $user->orders()->count();
|
| 21 |
+
|
| 22 |
+
$recentOrders = $user->orders()
|
| 23 |
+
->with('event')
|
| 24 |
+
->latest()
|
| 25 |
+
->take(5)
|
| 26 |
+
->get();
|
| 27 |
+
|
| 28 |
+
return view('dashboard', compact('activeTickets', 'totalOrders', 'recentOrders'));
|
| 29 |
+
}
|
| 30 |
+
}
|
app/Http/Controllers/EventController.php
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
|
| 3 |
+
namespace App\Http\Controllers;
|
| 4 |
+
|
| 5 |
+
use App\Models\Category;
|
| 6 |
+
use App\Models\Event;
|
| 7 |
+
use Illuminate\Http\Request;
|
| 8 |
+
|
| 9 |
+
class EventController extends Controller
|
| 10 |
+
{
|
| 11 |
+
public function index(Request $request)
|
| 12 |
+
{
|
| 13 |
+
$query = Event::with('category', 'ticketTiers', 'organizer')->published();
|
| 14 |
+
|
| 15 |
+
// Search
|
| 16 |
+
if ($request->filled('q')) {
|
| 17 |
+
$s = $request->q;
|
| 18 |
+
$query->where(function ($q) use ($s) {
|
| 19 |
+
$q->where('title', 'like', "%{$s}%")
|
| 20 |
+
->orWhere('city', 'like', "%{$s}%")
|
| 21 |
+
->orWhere('venue_name', 'like', "%{$s}%")
|
| 22 |
+
->orWhere('description', 'like', "%{$s}%");
|
| 23 |
+
});
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
// Filter: category
|
| 27 |
+
if ($request->filled('category')) {
|
| 28 |
+
$query->where('category_id', $request->category);
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
// Filter: city
|
| 32 |
+
if ($request->filled('city')) {
|
| 33 |
+
$query->where('city', $request->city);
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
// Filter: date
|
| 37 |
+
if ($request->filled('date_from')) {
|
| 38 |
+
$query->where('start_at', '>=', $request->date_from);
|
| 39 |
+
}
|
| 40 |
+
if ($request->filled('date_to')) {
|
| 41 |
+
$query->where('start_at', '<=', $request->date_to . ' 23:59:59');
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
// Filter: free only
|
| 45 |
+
if ($request->boolean('free')) {
|
| 46 |
+
$query->whereHas('ticketTiers', fn($q) => $q->where('price', 0));
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
// Sort
|
| 50 |
+
$sort = $request->input('sort', 'date');
|
| 51 |
+
$query = match ($sort) {
|
| 52 |
+
'popular' => $query->withCount(['orders' => fn($q) => $q->where('status', 'paid')])->orderByDesc('orders_count'),
|
| 53 |
+
'price' => $query->orderByRaw('(SELECT MIN(price) FROM ticket_tiers WHERE event_id = events.id)'),
|
| 54 |
+
default => $query->orderBy('start_at'),
|
| 55 |
+
};
|
| 56 |
+
|
| 57 |
+
$events = $query->paginate(12)->withQueryString();
|
| 58 |
+
|
| 59 |
+
$categories = Category::orderBy('name')->get();
|
| 60 |
+
$cities = Event::published()->whereNotNull('city')->distinct()->pluck('city');
|
| 61 |
+
|
| 62 |
+
return view('events.index', compact('events', 'categories', 'cities'));
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
public function show(string $slug)
|
| 66 |
+
{
|
| 67 |
+
$event = Event::where('slug', $slug)
|
| 68 |
+
->published()
|
| 69 |
+
->with(['category', 'organizer', 'ticketTiers'])
|
| 70 |
+
->firstOrFail();
|
| 71 |
+
|
| 72 |
+
return view('events.show', compact('event'));
|
| 73 |
+
}
|
| 74 |
+
}
|
app/Http/Controllers/HomeController.php
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
|
| 3 |
+
namespace App\Http\Controllers;
|
| 4 |
+
|
| 5 |
+
use App\Models\Category;
|
| 6 |
+
use App\Models\Event;
|
| 7 |
+
|
| 8 |
+
class HomeController extends Controller
|
| 9 |
+
{
|
| 10 |
+
public function index()
|
| 11 |
+
{
|
| 12 |
+
$featuredEvents = Event::with('category', 'ticketTiers', 'organizer')
|
| 13 |
+
->published()
|
| 14 |
+
->where('start_at', '>=', now())
|
| 15 |
+
->orderBy('start_at')
|
| 16 |
+
->take(6)
|
| 17 |
+
->get();
|
| 18 |
+
|
| 19 |
+
$categories = Category::withCount(['events' => fn($q) => $q->published()])
|
| 20 |
+
->having('events_count', '>', 0)
|
| 21 |
+
->orderBy('name')
|
| 22 |
+
->get();
|
| 23 |
+
|
| 24 |
+
$totalEvents = Event::published()->count();
|
| 25 |
+
|
| 26 |
+
return view('welcome', compact('featuredEvents', 'categories', 'totalEvents'));
|
| 27 |
+
}
|
| 28 |
+
}
|
app/Http/Controllers/MyTicketsController.php
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
|
| 3 |
+
namespace App\Http\Controllers;
|
| 4 |
+
|
| 5 |
+
use App\Models\Attendee;
|
| 6 |
+
use App\Models\Order;
|
| 7 |
+
use Illuminate\Http\Request;
|
| 8 |
+
|
| 9 |
+
class MyTicketsController extends Controller
|
| 10 |
+
{
|
| 11 |
+
public function index()
|
| 12 |
+
{
|
| 13 |
+
$attendees = Attendee::whereHas('orderItem.order', function ($q) {
|
| 14 |
+
$q->where('user_id', auth()->id())->where('status', 'paid');
|
| 15 |
+
})
|
| 16 |
+
->with('orderItem.ticketTier', 'orderItem.order.event')
|
| 17 |
+
->latest()
|
| 18 |
+
->paginate(12);
|
| 19 |
+
|
| 20 |
+
return view('tickets.index', compact('attendees'));
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
public function show(Attendee $attendee)
|
| 24 |
+
{
|
| 25 |
+
abort_unless($attendee->orderItem->order->user_id === auth()->id(), 403);
|
| 26 |
+
$attendee->load('orderItem.ticketTier', 'orderItem.order.event');
|
| 27 |
+
|
| 28 |
+
return view('tickets.show', compact('attendee'));
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
public function downloadPdf(Attendee $attendee)
|
| 32 |
+
{
|
| 33 |
+
abort_unless($attendee->orderItem->order->user_id === auth()->id(), 403);
|
| 34 |
+
$attendee->load('orderItem.ticketTier', 'orderItem.order.event');
|
| 35 |
+
|
| 36 |
+
$pdf = app('dompdf.wrapper')
|
| 37 |
+
->loadView('tickets.pdf', compact('attendee'))
|
| 38 |
+
->setPaper('a5', 'landscape');
|
| 39 |
+
|
| 40 |
+
return $pdf->download('ticket-' . $attendee->ticket_code . '.pdf');
|
| 41 |
+
}
|
| 42 |
+
}
|
app/Http/Controllers/OrdersController.php
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
|
| 3 |
+
namespace App\Http\Controllers;
|
| 4 |
+
|
| 5 |
+
use App\Models\Order;
|
| 6 |
+
use App\Services\OrderService;
|
| 7 |
+
use Illuminate\Http\Request;
|
| 8 |
+
use Symfony\Component\HttpFoundation\StreamedResponse;
|
| 9 |
+
|
| 10 |
+
class OrdersController extends Controller
|
| 11 |
+
{
|
| 12 |
+
public function index(Request $request)
|
| 13 |
+
{
|
| 14 |
+
$query = Order::where('user_id', auth()->id())->with('event');
|
| 15 |
+
|
| 16 |
+
if ($request->filled('status')) {
|
| 17 |
+
$query->where('status', $request->status);
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
$orders = $query->latest()->paginate(10);
|
| 21 |
+
|
| 22 |
+
return view('orders.index', compact('orders'));
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
public function show(Order $order)
|
| 26 |
+
{
|
| 27 |
+
abort_unless($order->user_id === auth()->id(), 403);
|
| 28 |
+
$order->load('event', 'items.ticketTier', 'attendees');
|
| 29 |
+
|
| 30 |
+
return view('orders.show', compact('order'));
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
public function cancel(Order $order, OrderService $orderService)
|
| 34 |
+
{
|
| 35 |
+
abort_unless($order->user_id === auth()->id(), 403);
|
| 36 |
+
|
| 37 |
+
if (!$order->isPaid()) {
|
| 38 |
+
return back()->with('error', 'Hanya order yang sudah dibayar yang bisa dibatalkan.');
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
$success = $orderService->cancelOrder($order);
|
| 42 |
+
|
| 43 |
+
if (!$success) {
|
| 44 |
+
return back()->with('error', 'Tidak bisa membatalkan order. Batas waktu refund sudah lewat (minimal 24 jam sebelum event).');
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
return back()->with('success', 'Order berhasil dibatalkan dan akan direfund.');
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
public function exportCsv(): StreamedResponse
|
| 51 |
+
{
|
| 52 |
+
$orders = Order::where('user_id', auth()->id())
|
| 53 |
+
->with('event')
|
| 54 |
+
->latest()
|
| 55 |
+
->get();
|
| 56 |
+
|
| 57 |
+
return response()->streamDownload(function () use ($orders) {
|
| 58 |
+
$handle = fopen('php://output', 'w');
|
| 59 |
+
fputcsv($handle, ['Order Code', 'Event', 'Total', 'Status', 'Tanggal']);
|
| 60 |
+
|
| 61 |
+
foreach ($orders as $order) {
|
| 62 |
+
fputcsv($handle, [
|
| 63 |
+
$order->order_code,
|
| 64 |
+
$order->event->title,
|
| 65 |
+
'Rp ' . number_format($order->total, 0, ',', '.'),
|
| 66 |
+
$order->status,
|
| 67 |
+
$order->created_at->format('d/m/Y H:i'),
|
| 68 |
+
]);
|
| 69 |
+
}
|
| 70 |
+
fclose($handle);
|
| 71 |
+
}, 'orders-' . now()->format('Ymd') . '.csv', ['Content-Type' => 'text/csv']);
|
| 72 |
+
}
|
| 73 |
+
}
|
app/Http/Controllers/Organizer/DashboardController.php
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
|
| 3 |
+
namespace App\Http\Controllers\Organizer;
|
| 4 |
+
|
| 5 |
+
use App\Http\Controllers\Controller;
|
| 6 |
+
use App\Models\Event;
|
| 7 |
+
use App\Models\Order;
|
| 8 |
+
use Illuminate\Http\Request;
|
| 9 |
+
|
| 10 |
+
class DashboardController extends Controller
|
| 11 |
+
{
|
| 12 |
+
public function index()
|
| 13 |
+
{
|
| 14 |
+
$user = auth()->user();
|
| 15 |
+
$events = $user->events()->withCount('orders')->get();
|
| 16 |
+
|
| 17 |
+
$totalRevenue = Order::whereIn('event_id', $events->pluck('id'))
|
| 18 |
+
->where('status', 'paid')->sum('total');
|
| 19 |
+
$totalOrders = Order::whereIn('event_id', $events->pluck('id'))
|
| 20 |
+
->where('status', 'paid')->count();
|
| 21 |
+
$totalSold = $events->sum(fn ($e) => $e->ticketTiers->sum('sold_count'));
|
| 22 |
+
$activeEvents = $events->where('status', 'published')->count();
|
| 23 |
+
|
| 24 |
+
$recentOrders = Order::whereIn('event_id', $events->pluck('id'))
|
| 25 |
+
->with('user', 'event')
|
| 26 |
+
->latest()
|
| 27 |
+
->take(10)
|
| 28 |
+
->get();
|
| 29 |
+
|
| 30 |
+
return view('organizer.dashboard', compact(
|
| 31 |
+
'events', 'totalRevenue', 'totalOrders', 'totalSold', 'activeEvents', 'recentOrders'
|
| 32 |
+
));
|
| 33 |
+
}
|
| 34 |
+
}
|
app/Http/Controllers/Organizer/EventController.php
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
|
| 3 |
+
namespace App\Http\Controllers\Organizer;
|
| 4 |
+
|
| 5 |
+
use App\Http\Controllers\Controller;
|
| 6 |
+
use App\Models\Category;
|
| 7 |
+
use App\Models\Event;
|
| 8 |
+
use Illuminate\Http\Request;
|
| 9 |
+
use Illuminate\Support\Facades\Storage;
|
| 10 |
+
|
| 11 |
+
class EventController extends Controller
|
| 12 |
+
{
|
| 13 |
+
public function index()
|
| 14 |
+
{
|
| 15 |
+
$events = auth()->user()->events()
|
| 16 |
+
->with('category', 'ticketTiers')
|
| 17 |
+
->latest()
|
| 18 |
+
->paginate(10);
|
| 19 |
+
|
| 20 |
+
return view('organizer.events.index', compact('events'));
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
public function create()
|
| 24 |
+
{
|
| 25 |
+
$categories = Category::orderBy('name')->get();
|
| 26 |
+
return view('organizer.events.create', compact('categories'));
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
public function store(Request $request)
|
| 30 |
+
{
|
| 31 |
+
$validated = $request->validate([
|
| 32 |
+
'title' => 'required|string|max:255',
|
| 33 |
+
'category_id' => 'required|exists:categories,id',
|
| 34 |
+
'description' => 'required|string',
|
| 35 |
+
'venue_name' => 'nullable|string|max:255',
|
| 36 |
+
'venue_address' => 'nullable|string|max:500',
|
| 37 |
+
'city' => 'nullable|string|max:100',
|
| 38 |
+
'is_online' => 'boolean',
|
| 39 |
+
'start_at' => 'required|date|after:now',
|
| 40 |
+
'end_at' => 'required|date|after:start_at',
|
| 41 |
+
'status' => 'required|in:draft,published',
|
| 42 |
+
'banner' => 'nullable|image|max:2048',
|
| 43 |
+
'terms' => 'nullable|string',
|
| 44 |
+
'refund_policy' => 'nullable|string',
|
| 45 |
+
]);
|
| 46 |
+
|
| 47 |
+
if ($request->hasFile('banner')) {
|
| 48 |
+
$validated['banner'] = $request->file('banner')->store('banners', 'public');
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
$validated['organizer_id'] = auth()->id();
|
| 52 |
+
$validated['is_online'] = $request->boolean('is_online');
|
| 53 |
+
|
| 54 |
+
$event = Event::create($validated);
|
| 55 |
+
|
| 56 |
+
return redirect()->route('organizer.events.edit', $event)
|
| 57 |
+
->with('success', 'Event berhasil dibuat! Sekarang tambahkan tiket tier.');
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
public function edit(Event $event)
|
| 61 |
+
{
|
| 62 |
+
$this->authorizeOrganizer($event);
|
| 63 |
+
$categories = Category::orderBy('name')->get();
|
| 64 |
+
$event->load('ticketTiers');
|
| 65 |
+
|
| 66 |
+
return view('organizer.events.edit', compact('event', 'categories'));
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
public function update(Request $request, Event $event)
|
| 70 |
+
{
|
| 71 |
+
$this->authorizeOrganizer($event);
|
| 72 |
+
|
| 73 |
+
$validated = $request->validate([
|
| 74 |
+
'title' => 'required|string|max:255',
|
| 75 |
+
'category_id' => 'required|exists:categories,id',
|
| 76 |
+
'description' => 'required|string',
|
| 77 |
+
'venue_name' => 'nullable|string|max:255',
|
| 78 |
+
'venue_address' => 'nullable|string|max:500',
|
| 79 |
+
'city' => 'nullable|string|max:100',
|
| 80 |
+
'is_online' => 'boolean',
|
| 81 |
+
'start_at' => 'required|date',
|
| 82 |
+
'end_at' => 'required|date|after:start_at',
|
| 83 |
+
'status' => 'required|in:draft,published,ended,cancelled',
|
| 84 |
+
'banner' => 'nullable|image|max:2048',
|
| 85 |
+
'terms' => 'nullable|string',
|
| 86 |
+
'refund_policy' => 'nullable|string',
|
| 87 |
+
]);
|
| 88 |
+
|
| 89 |
+
if ($request->hasFile('banner')) {
|
| 90 |
+
if ($event->banner) Storage::disk('public')->delete($event->banner);
|
| 91 |
+
$validated['banner'] = $request->file('banner')->store('banners', 'public');
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
$validated['is_online'] = $request->boolean('is_online');
|
| 95 |
+
|
| 96 |
+
$event->update($validated);
|
| 97 |
+
|
| 98 |
+
return redirect()->route('organizer.events.index')
|
| 99 |
+
->with('success', 'Event berhasil diperbarui!');
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
public function destroy(Event $event)
|
| 103 |
+
{
|
| 104 |
+
$this->authorizeOrganizer($event);
|
| 105 |
+
|
| 106 |
+
if ($event->orders()->where('status', 'paid')->exists()) {
|
| 107 |
+
return back()->with('error', 'Event tidak bisa dihapus karena sudah memiliki order yang dibayar.');
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
if ($event->banner) Storage::disk('public')->delete($event->banner);
|
| 111 |
+
$event->delete();
|
| 112 |
+
|
| 113 |
+
return redirect()->route('organizer.events.index')
|
| 114 |
+
->with('success', 'Event berhasil dihapus.');
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
private function authorizeOrganizer(Event $event): void
|
| 118 |
+
{
|
| 119 |
+
if ($event->organizer_id !== auth()->id() && !auth()->user()->isAdmin()) {
|
| 120 |
+
abort(403, 'Anda tidak memiliki akses ke event ini.');
|
| 121 |
+
}
|
| 122 |
+
}
|
| 123 |
+
}
|
app/Http/Controllers/Organizer/OrderController.php
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
|
| 3 |
+
namespace App\Http\Controllers\Organizer;
|
| 4 |
+
|
| 5 |
+
use App\Http\Controllers\Controller;
|
| 6 |
+
use App\Models\Attendee;
|
| 7 |
+
use App\Models\Event;
|
| 8 |
+
use App\Models\Order;
|
| 9 |
+
use Illuminate\Http\Request;
|
| 10 |
+
use Symfony\Component\HttpFoundation\StreamedResponse;
|
| 11 |
+
|
| 12 |
+
class OrderController extends Controller
|
| 13 |
+
{
|
| 14 |
+
public function index(Event $event)
|
| 15 |
+
{
|
| 16 |
+
$this->authorizeOrganizer($event);
|
| 17 |
+
|
| 18 |
+
$orders = Order::where('event_id', $event->id)
|
| 19 |
+
->with('user', 'items.ticketTier')
|
| 20 |
+
->latest()
|
| 21 |
+
->paginate(20);
|
| 22 |
+
|
| 23 |
+
return view('organizer.orders.index', compact('event', 'orders'));
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
public function show(Event $event, Order $order)
|
| 27 |
+
{
|
| 28 |
+
$this->authorizeOrganizer($event);
|
| 29 |
+
abort_unless($order->event_id === $event->id, 404);
|
| 30 |
+
|
| 31 |
+
$order->load('user', 'items.ticketTier', 'attendees');
|
| 32 |
+
|
| 33 |
+
return view('organizer.orders.show', compact('event', 'order'));
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
public function exportCsv(Event $event): StreamedResponse
|
| 37 |
+
{
|
| 38 |
+
$this->authorizeOrganizer($event);
|
| 39 |
+
|
| 40 |
+
$attendees = Attendee::whereHas('orderItem.order', function ($q) use ($event) {
|
| 41 |
+
$q->where('event_id', $event->id)->where('status', 'paid');
|
| 42 |
+
})->with('orderItem.ticketTier', 'orderItem.order')->get();
|
| 43 |
+
|
| 44 |
+
$filename = 'attendees-' . $event->slug . '-' . now()->format('Ymd') . '.csv';
|
| 45 |
+
|
| 46 |
+
return response()->streamDownload(function () use ($attendees) {
|
| 47 |
+
$handle = fopen('php://output', 'w');
|
| 48 |
+
fputcsv($handle, ['No', 'Nama', 'Email', 'Telepon', 'Ticket Tier', 'Ticket Code', 'Checked In']);
|
| 49 |
+
|
| 50 |
+
foreach ($attendees as $i => $att) {
|
| 51 |
+
fputcsv($handle, [
|
| 52 |
+
$i + 1,
|
| 53 |
+
$att->full_name,
|
| 54 |
+
$att->email,
|
| 55 |
+
$att->phone ?? '-',
|
| 56 |
+
$att->orderItem->ticketTier->name,
|
| 57 |
+
$att->ticket_code,
|
| 58 |
+
$att->checkin_at ? $att->checkin_at->format('d/m/Y H:i') : 'Belum',
|
| 59 |
+
]);
|
| 60 |
+
}
|
| 61 |
+
fclose($handle);
|
| 62 |
+
}, $filename, ['Content-Type' => 'text/csv']);
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
private function authorizeOrganizer(Event $event): void
|
| 66 |
+
{
|
| 67 |
+
if ($event->organizer_id !== auth()->id() && !auth()->user()->isAdmin()) {
|
| 68 |
+
abort(403);
|
| 69 |
+
}
|
| 70 |
+
}
|
| 71 |
+
}
|
app/Http/Controllers/Organizer/ScannerController.php
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
|
| 3 |
+
namespace App\Http\Controllers\Organizer;
|
| 4 |
+
|
| 5 |
+
use App\Http\Controllers\Controller;
|
| 6 |
+
use App\Services\TicketService;
|
| 7 |
+
use Illuminate\Http\Request;
|
| 8 |
+
|
| 9 |
+
class ScannerController extends Controller
|
| 10 |
+
{
|
| 11 |
+
public function index()
|
| 12 |
+
{
|
| 13 |
+
return view('organizer.scanner');
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
public function checkIn(Request $request, TicketService $ticketService)
|
| 17 |
+
{
|
| 18 |
+
$request->validate(['ticket_code' => 'required|string|max:16']);
|
| 19 |
+
|
| 20 |
+
$result = $ticketService->checkIn($request->ticket_code);
|
| 21 |
+
|
| 22 |
+
if ($request->wantsJson()) {
|
| 23 |
+
return response()->json($result, $result['success'] ? 200 : 422);
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
return back()->with(
|
| 27 |
+
$result['success'] ? 'scan_success' : 'scan_error',
|
| 28 |
+
$result['message']
|
| 29 |
+
)->with('scan_attendee', $result['attendee']);
|
| 30 |
+
}
|
| 31 |
+
}
|
app/Http/Controllers/Organizer/TicketTierController.php
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
|
| 3 |
+
namespace App\Http\Controllers\Organizer;
|
| 4 |
+
|
| 5 |
+
use App\Http\Controllers\Controller;
|
| 6 |
+
use App\Models\Event;
|
| 7 |
+
use App\Models\TicketTier;
|
| 8 |
+
use Illuminate\Http\Request;
|
| 9 |
+
|
| 10 |
+
class TicketTierController extends Controller
|
| 11 |
+
{
|
| 12 |
+
public function store(Request $request, Event $event)
|
| 13 |
+
{
|
| 14 |
+
$this->authorizeOrganizer($event);
|
| 15 |
+
|
| 16 |
+
$validated = $request->validate([
|
| 17 |
+
'name' => 'required|string|max:255',
|
| 18 |
+
'price' => 'required|numeric|min:0',
|
| 19 |
+
'quota' => 'required|integer|min:1',
|
| 20 |
+
'max_per_order' => 'required|integer|min:1|max:50',
|
| 21 |
+
'is_refundable' => 'boolean',
|
| 22 |
+
'sales_start' => 'nullable|date',
|
| 23 |
+
'sales_end' => 'nullable|date|after:sales_start',
|
| 24 |
+
]);
|
| 25 |
+
|
| 26 |
+
$validated['is_refundable'] = $request->boolean('is_refundable');
|
| 27 |
+
$validated['event_id'] = $event->id;
|
| 28 |
+
|
| 29 |
+
TicketTier::create($validated);
|
| 30 |
+
|
| 31 |
+
return back()->with('success', 'Ticket tier berhasil ditambahkan!');
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
public function update(Request $request, Event $event, TicketTier $tier)
|
| 35 |
+
{
|
| 36 |
+
$this->authorizeOrganizer($event);
|
| 37 |
+
abort_unless($tier->event_id === $event->id, 404);
|
| 38 |
+
|
| 39 |
+
$validated = $request->validate([
|
| 40 |
+
'name' => 'required|string|max:255',
|
| 41 |
+
'price' => 'required|numeric|min:0',
|
| 42 |
+
'quota' => 'required|integer|min:' . $tier->sold_count,
|
| 43 |
+
'max_per_order' => 'required|integer|min:1|max:50',
|
| 44 |
+
'is_refundable' => 'boolean',
|
| 45 |
+
'sales_start' => 'nullable|date',
|
| 46 |
+
'sales_end' => 'nullable|date|after:sales_start',
|
| 47 |
+
]);
|
| 48 |
+
|
| 49 |
+
$validated['is_refundable'] = $request->boolean('is_refundable');
|
| 50 |
+
$tier->update($validated);
|
| 51 |
+
|
| 52 |
+
return back()->with('success', 'Ticket tier berhasil diperbarui!');
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
public function destroy(Event $event, TicketTier $tier)
|
| 56 |
+
{
|
| 57 |
+
$this->authorizeOrganizer($event);
|
| 58 |
+
abort_unless($tier->event_id === $event->id, 404);
|
| 59 |
+
|
| 60 |
+
if ($tier->sold_count > 0) {
|
| 61 |
+
return back()->with('error', 'Tidak bisa menghapus tier yang sudah terjual.');
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
$tier->delete();
|
| 65 |
+
return back()->with('success', 'Ticket tier berhasil dihapus.');
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
private function authorizeOrganizer(Event $event): void
|
| 69 |
+
{
|
| 70 |
+
if ($event->organizer_id !== auth()->id() && !auth()->user()->isAdmin()) {
|
| 71 |
+
abort(403);
|
| 72 |
+
}
|
| 73 |
+
}
|
| 74 |
+
}
|
app/Http/Controllers/ProfileController.php
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
|
| 3 |
+
namespace App\Http\Controllers;
|
| 4 |
+
|
| 5 |
+
use App\Http\Requests\ProfileUpdateRequest;
|
| 6 |
+
use Illuminate\Http\RedirectResponse;
|
| 7 |
+
use Illuminate\Http\Request;
|
| 8 |
+
use Illuminate\Support\Facades\Auth;
|
| 9 |
+
use Illuminate\Support\Facades\Redirect;
|
| 10 |
+
use Illuminate\View\View;
|
| 11 |
+
|
| 12 |
+
class ProfileController extends Controller
|
| 13 |
+
{
|
| 14 |
+
/**
|
| 15 |
+
* Display the user's profile form.
|
| 16 |
+
*/
|
| 17 |
+
public function edit(Request $request): View
|
| 18 |
+
{
|
| 19 |
+
return view('profile.edit', [
|
| 20 |
+
'user' => $request->user(),
|
| 21 |
+
]);
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
/**
|
| 25 |
+
* Update the user's profile information.
|
| 26 |
+
*/
|
| 27 |
+
public function update(ProfileUpdateRequest $request): RedirectResponse
|
| 28 |
+
{
|
| 29 |
+
$request->user()->fill($request->validated());
|
| 30 |
+
|
| 31 |
+
if ($request->user()->isDirty('email')) {
|
| 32 |
+
$request->user()->email_verified_at = null;
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
$request->user()->save();
|
| 36 |
+
|
| 37 |
+
return Redirect::route('profile.edit')->with('status', 'profile-updated');
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
/**
|
| 41 |
+
* Delete the user's account.
|
| 42 |
+
*/
|
| 43 |
+
public function destroy(Request $request): RedirectResponse
|
| 44 |
+
{
|
| 45 |
+
$request->validateWithBag('userDeletion', [
|
| 46 |
+
'password' => ['required', 'current_password'],
|
| 47 |
+
]);
|
| 48 |
+
|
| 49 |
+
$user = $request->user();
|
| 50 |
+
|
| 51 |
+
Auth::logout();
|
| 52 |
+
|
| 53 |
+
$user->delete();
|
| 54 |
+
|
| 55 |
+
$request->session()->invalidate();
|
| 56 |
+
$request->session()->regenerateToken();
|
| 57 |
+
|
| 58 |
+
return Redirect::to('/');
|
| 59 |
+
}
|
| 60 |
+
}
|
app/Http/Middleware/RoleMiddleware.php
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
|
| 3 |
+
namespace App\Http\Middleware;
|
| 4 |
+
|
| 5 |
+
use Closure;
|
| 6 |
+
use Illuminate\Http\Request;
|
| 7 |
+
use Symfony\Component\HttpFoundation\Response;
|
| 8 |
+
|
| 9 |
+
class RoleMiddleware
|
| 10 |
+
{
|
| 11 |
+
public function handle(Request $request, Closure $next, string ...$roles): Response
|
| 12 |
+
{
|
| 13 |
+
if (!auth()->check()) {
|
| 14 |
+
return redirect()->route('login');
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
if (!in_array(auth()->user()->role, $roles)) {
|
| 18 |
+
abort(403, 'Anda tidak memiliki akses ke halaman ini.');
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
return $next($request);
|
| 22 |
+
}
|
| 23 |
+
}
|
app/Http/Requests/Auth/LoginRequest.php
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
|
| 3 |
+
namespace App\Http\Requests\Auth;
|
| 4 |
+
|
| 5 |
+
use Illuminate\Auth\Events\Lockout;
|
| 6 |
+
use Illuminate\Foundation\Http\FormRequest;
|
| 7 |
+
use Illuminate\Support\Facades\Auth;
|
| 8 |
+
use Illuminate\Support\Facades\RateLimiter;
|
| 9 |
+
use Illuminate\Support\Str;
|
| 10 |
+
use Illuminate\Validation\ValidationException;
|
| 11 |
+
|
| 12 |
+
class LoginRequest extends FormRequest
|
| 13 |
+
{
|
| 14 |
+
/**
|
| 15 |
+
* Determine if the user is authorized to make this request.
|
| 16 |
+
*/
|
| 17 |
+
public function authorize(): bool
|
| 18 |
+
{
|
| 19 |
+
return true;
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
/**
|
| 23 |
+
* Get the validation rules that apply to the request.
|
| 24 |
+
*
|
| 25 |
+
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
| 26 |
+
*/
|
| 27 |
+
public function rules(): array
|
| 28 |
+
{
|
| 29 |
+
return [
|
| 30 |
+
'email' => ['required', 'string', 'email'],
|
| 31 |
+
'password' => ['required', 'string'],
|
| 32 |
+
];
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
/**
|
| 36 |
+
* Attempt to authenticate the request's credentials.
|
| 37 |
+
*
|
| 38 |
+
* @throws \Illuminate\Validation\ValidationException
|
| 39 |
+
*/
|
| 40 |
+
public function authenticate(): void
|
| 41 |
+
{
|
| 42 |
+
$this->ensureIsNotRateLimited();
|
| 43 |
+
|
| 44 |
+
if (! Auth::attempt($this->only('email', 'password'), $this->boolean('remember'))) {
|
| 45 |
+
RateLimiter::hit($this->throttleKey());
|
| 46 |
+
|
| 47 |
+
throw ValidationException::withMessages([
|
| 48 |
+
'email' => trans('auth.failed'),
|
| 49 |
+
]);
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
RateLimiter::clear($this->throttleKey());
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
/**
|
| 56 |
+
* Ensure the login request is not rate limited.
|
| 57 |
+
*
|
| 58 |
+
* @throws \Illuminate\Validation\ValidationException
|
| 59 |
+
*/
|
| 60 |
+
public function ensureIsNotRateLimited(): void
|
| 61 |
+
{
|
| 62 |
+
if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) {
|
| 63 |
+
return;
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
event(new Lockout($this));
|
| 67 |
+
|
| 68 |
+
$seconds = RateLimiter::availableIn($this->throttleKey());
|
| 69 |
+
|
| 70 |
+
throw ValidationException::withMessages([
|
| 71 |
+
'email' => trans('auth.throttle', [
|
| 72 |
+
'seconds' => $seconds,
|
| 73 |
+
'minutes' => ceil($seconds / 60),
|
| 74 |
+
]),
|
| 75 |
+
]);
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
/**
|
| 79 |
+
* Get the rate limiting throttle key for the request.
|
| 80 |
+
*/
|
| 81 |
+
public function throttleKey(): string
|
| 82 |
+
{
|
| 83 |
+
return Str::transliterate(Str::lower($this->string('email')).'|'.$this->ip());
|
| 84 |
+
}
|
| 85 |
+
}
|
app/Http/Requests/ProfileUpdateRequest.php
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
|
| 3 |
+
namespace App\Http\Requests;
|
| 4 |
+
|
| 5 |
+
use App\Models\User;
|
| 6 |
+
use Illuminate\Foundation\Http\FormRequest;
|
| 7 |
+
use Illuminate\Validation\Rule;
|
| 8 |
+
|
| 9 |
+
class ProfileUpdateRequest extends FormRequest
|
| 10 |
+
{
|
| 11 |
+
/**
|
| 12 |
+
* Get the validation rules that apply to the request.
|
| 13 |
+
*
|
| 14 |
+
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
| 15 |
+
*/
|
| 16 |
+
public function rules(): array
|
| 17 |
+
{
|
| 18 |
+
return [
|
| 19 |
+
'name' => ['required', 'string', 'max:255'],
|
| 20 |
+
'email' => [
|
| 21 |
+
'required',
|
| 22 |
+
'string',
|
| 23 |
+
'lowercase',
|
| 24 |
+
'email',
|
| 25 |
+
'max:255',
|
| 26 |
+
Rule::unique(User::class)->ignore($this->user()->id),
|
| 27 |
+
],
|
| 28 |
+
];
|
| 29 |
+
}
|
| 30 |
+
}
|
app/Models/Attendee.php
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
|
| 3 |
+
namespace App\Models;
|
| 4 |
+
|
| 5 |
+
use Illuminate\Database\Eloquent\Model;
|
| 6 |
+
use Illuminate\Support\Str;
|
| 7 |
+
|
| 8 |
+
class Attendee extends Model
|
| 9 |
+
{
|
| 10 |
+
protected $fillable = ['order_item_id', 'full_name', 'email', 'phone', 'ticket_code', 'checkin_at'];
|
| 11 |
+
|
| 12 |
+
protected function casts(): array
|
| 13 |
+
{
|
| 14 |
+
return [
|
| 15 |
+
'checkin_at' => 'datetime',
|
| 16 |
+
];
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
protected static function booted(): void
|
| 20 |
+
{
|
| 21 |
+
static::creating(function (Attendee $attendee) {
|
| 22 |
+
if (empty($attendee->ticket_code)) {
|
| 23 |
+
do {
|
| 24 |
+
$code = 'TKT-' . strtoupper(Str::random(8));
|
| 25 |
+
} while (static::where('ticket_code', $code)->exists());
|
| 26 |
+
$attendee->ticket_code = $code;
|
| 27 |
+
}
|
| 28 |
+
});
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
// ── Relationships ────────────────────────────────────
|
| 32 |
+
|
| 33 |
+
public function orderItem()
|
| 34 |
+
{
|
| 35 |
+
return $this->belongsTo(OrderItem::class);
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
public function order()
|
| 39 |
+
{
|
| 40 |
+
return $this->hasOneThrough(Order::class, OrderItem::class, 'id', 'id', 'order_item_id', 'order_id');
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
// ── Helpers ──────────────────────────────────────────
|
| 44 |
+
|
| 45 |
+
public function isCheckedIn(): bool
|
| 46 |
+
{
|
| 47 |
+
return $this->checkin_at !== null;
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
public function checkIn(): bool
|
| 51 |
+
{
|
| 52 |
+
if ($this->isCheckedIn()) return false;
|
| 53 |
+
|
| 54 |
+
$this->update(['checkin_at' => now()]);
|
| 55 |
+
return true;
|
| 56 |
+
}
|
| 57 |
+
}
|
app/Models/Category.php
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
|
| 3 |
+
namespace App\Models;
|
| 4 |
+
|
| 5 |
+
use Illuminate\Database\Eloquent\Model;
|
| 6 |
+
use Illuminate\Support\Str;
|
| 7 |
+
|
| 8 |
+
class Category extends Model
|
| 9 |
+
{
|
| 10 |
+
protected $fillable = ['name', 'slug'];
|
| 11 |
+
|
| 12 |
+
protected static function booted(): void
|
| 13 |
+
{
|
| 14 |
+
static::creating(function (Category $cat) {
|
| 15 |
+
if (empty($cat->slug)) {
|
| 16 |
+
$cat->slug = Str::slug($cat->name);
|
| 17 |
+
}
|
| 18 |
+
});
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
// ── Relationships ────────────────────────────────────
|
| 22 |
+
|
| 23 |
+
public function events()
|
| 24 |
+
{
|
| 25 |
+
return $this->hasMany(Event::class);
|
| 26 |
+
}
|
| 27 |
+
}
|
app/Models/Event.php
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
|
| 3 |
+
namespace App\Models;
|
| 4 |
+
|
| 5 |
+
use Illuminate\Database\Eloquent\Model;
|
| 6 |
+
use Illuminate\Support\Str;
|
| 7 |
+
|
| 8 |
+
class Event extends Model
|
| 9 |
+
{
|
| 10 |
+
protected $fillable = [
|
| 11 |
+
'organizer_id', 'category_id', 'title', 'slug', 'description', 'banner',
|
| 12 |
+
'venue_name', 'venue_address', 'city', 'is_online',
|
| 13 |
+
'start_at', 'end_at', 'status', 'terms', 'refund_policy',
|
| 14 |
+
];
|
| 15 |
+
|
| 16 |
+
protected function casts(): array
|
| 17 |
+
{
|
| 18 |
+
return [
|
| 19 |
+
'start_at' => 'datetime',
|
| 20 |
+
'end_at' => 'datetime',
|
| 21 |
+
'is_online' => 'boolean',
|
| 22 |
+
];
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
protected static function booted(): void
|
| 26 |
+
{
|
| 27 |
+
static::creating(function (Event $event) {
|
| 28 |
+
if (empty($event->slug)) {
|
| 29 |
+
$base = Str::slug($event->title);
|
| 30 |
+
$slug = $base;
|
| 31 |
+
$i = 1;
|
| 32 |
+
while (static::where('slug', $slug)->exists()) {
|
| 33 |
+
$slug = $base . '-' . $i++;
|
| 34 |
+
}
|
| 35 |
+
$event->slug = $slug;
|
| 36 |
+
}
|
| 37 |
+
});
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
// ── Relationships ────────────────────────────────────
|
| 41 |
+
|
| 42 |
+
public function organizer()
|
| 43 |
+
{
|
| 44 |
+
return $this->belongsTo(User::class, 'organizer_id');
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
public function category()
|
| 48 |
+
{
|
| 49 |
+
return $this->belongsTo(Category::class);
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
public function ticketTiers()
|
| 53 |
+
{
|
| 54 |
+
return $this->hasMany(TicketTier::class);
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
public function orders()
|
| 58 |
+
{
|
| 59 |
+
return $this->hasMany(Order::class);
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
// ── Scopes ───────────────────────────────────────────
|
| 63 |
+
|
| 64 |
+
public function scopePublished($query)
|
| 65 |
+
{
|
| 66 |
+
return $query->where('status', 'published');
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
public function scopeUpcoming($query)
|
| 70 |
+
{
|
| 71 |
+
return $query->published()->where('start_at', '>=', now());
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
// ── Helpers ──────────────────────────────────────────
|
| 75 |
+
|
| 76 |
+
public function getMinPrice(): float
|
| 77 |
+
{
|
| 78 |
+
return $this->ticketTiers->min('price') ?? 0;
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
public function getTotalQuota(): int
|
| 82 |
+
{
|
| 83 |
+
return $this->ticketTiers->sum('quota');
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
public function getTotalSold(): int
|
| 87 |
+
{
|
| 88 |
+
return $this->ticketTiers->sum('sold_count');
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
public function getAvailableStock(): int
|
| 92 |
+
{
|
| 93 |
+
return $this->getTotalQuota() - $this->getTotalSold();
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
public function isPublished(): bool
|
| 97 |
+
{
|
| 98 |
+
return $this->status === 'published';
|
| 99 |
+
}
|
| 100 |
+
}
|
app/Models/Order.php
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
|
| 3 |
+
namespace App\Models;
|
| 4 |
+
|
| 5 |
+
use Illuminate\Database\Eloquent\Model;
|
| 6 |
+
use Illuminate\Support\Str;
|
| 7 |
+
|
| 8 |
+
class Order extends Model
|
| 9 |
+
{
|
| 10 |
+
protected $fillable = [
|
| 11 |
+
'user_id', 'event_id', 'order_code', 'subtotal', 'fee', 'tax',
|
| 12 |
+
'discount', 'total', 'status', 'payment_method', 'paid_at', 'expires_at',
|
| 13 |
+
];
|
| 14 |
+
|
| 15 |
+
protected function casts(): array
|
| 16 |
+
{
|
| 17 |
+
return [
|
| 18 |
+
'subtotal' => 'decimal:2',
|
| 19 |
+
'fee' => 'decimal:2',
|
| 20 |
+
'tax' => 'decimal:2',
|
| 21 |
+
'discount' => 'decimal:2',
|
| 22 |
+
'total' => 'decimal:2',
|
| 23 |
+
'paid_at' => 'datetime',
|
| 24 |
+
'expires_at' => 'datetime',
|
| 25 |
+
];
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
protected static function booted(): void
|
| 29 |
+
{
|
| 30 |
+
static::creating(function (Order $order) {
|
| 31 |
+
if (empty($order->order_code)) {
|
| 32 |
+
$order->order_code = 'ORD-' . now()->format('Ymd') . '-' . strtoupper(Str::random(5));
|
| 33 |
+
}
|
| 34 |
+
});
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
// ── Relationships ────────────────────────────────────
|
| 38 |
+
|
| 39 |
+
public function user()
|
| 40 |
+
{
|
| 41 |
+
return $this->belongsTo(User::class);
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
public function event()
|
| 45 |
+
{
|
| 46 |
+
return $this->belongsTo(Event::class);
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
public function items()
|
| 50 |
+
{
|
| 51 |
+
return $this->hasMany(OrderItem::class);
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
public function attendees()
|
| 55 |
+
{
|
| 56 |
+
return $this->hasManyThrough(Attendee::class, OrderItem::class);
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
// ── Status Helpers ───────────────────────────────────
|
| 60 |
+
|
| 61 |
+
public function isPending(): bool { return $this->status === 'pending'; }
|
| 62 |
+
public function isPaid(): bool { return $this->status === 'paid'; }
|
| 63 |
+
public function isExpired(): bool { return $this->status === 'expired'; }
|
| 64 |
+
public function isRefunded(): bool { return $this->status === 'refunded'; }
|
| 65 |
+
|
| 66 |
+
public function isExpirable(): bool
|
| 67 |
+
{
|
| 68 |
+
return $this->isPending() && $this->expires_at && now()->gte($this->expires_at);
|
| 69 |
+
}
|
| 70 |
+
}
|
app/Models/OrderItem.php
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
|
| 3 |
+
namespace App\Models;
|
| 4 |
+
|
| 5 |
+
use Illuminate\Database\Eloquent\Model;
|
| 6 |
+
|
| 7 |
+
class OrderItem extends Model
|
| 8 |
+
{
|
| 9 |
+
protected $fillable = ['order_id', 'ticket_tier_id', 'qty', 'unit_price', 'total'];
|
| 10 |
+
|
| 11 |
+
protected function casts(): array
|
| 12 |
+
{
|
| 13 |
+
return [
|
| 14 |
+
'unit_price' => 'decimal:2',
|
| 15 |
+
'total' => 'decimal:2',
|
| 16 |
+
];
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
// ── Relationships ────────────────────────────────────
|
| 20 |
+
|
| 21 |
+
public function order()
|
| 22 |
+
{
|
| 23 |
+
return $this->belongsTo(Order::class);
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
public function ticketTier()
|
| 27 |
+
{
|
| 28 |
+
return $this->belongsTo(TicketTier::class);
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
public function attendees()
|
| 32 |
+
{
|
| 33 |
+
return $this->hasMany(Attendee::class);
|
| 34 |
+
}
|
| 35 |
+
}
|
app/Models/PromoCode.php
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
|
| 3 |
+
namespace App\Models;
|
| 4 |
+
|
| 5 |
+
use Illuminate\Database\Eloquent\Model;
|
| 6 |
+
|
| 7 |
+
class PromoCode extends Model
|
| 8 |
+
{
|
| 9 |
+
protected $fillable = ['code', 'type', 'value', 'start_at', 'end_at', 'usage_limit', 'used_count'];
|
| 10 |
+
|
| 11 |
+
protected function casts(): array
|
| 12 |
+
{
|
| 13 |
+
return [
|
| 14 |
+
'value' => 'decimal:2',
|
| 15 |
+
'start_at' => 'datetime',
|
| 16 |
+
'end_at' => 'datetime',
|
| 17 |
+
];
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
// ── Helpers ──────────────────────────────────────────
|
| 21 |
+
|
| 22 |
+
public function isValid(): bool
|
| 23 |
+
{
|
| 24 |
+
$now = now();
|
| 25 |
+
|
| 26 |
+
if ($this->start_at && $now->lt($this->start_at)) return false;
|
| 27 |
+
if ($this->end_at && $now->gt($this->end_at)) return false;
|
| 28 |
+
if ($this->usage_limit !== null && $this->used_count >= $this->usage_limit) return false;
|
| 29 |
+
|
| 30 |
+
return true;
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
public function applyDiscount(float $subtotal): float
|
| 34 |
+
{
|
| 35 |
+
if (!$this->isValid()) return 0;
|
| 36 |
+
|
| 37 |
+
if ($this->type === 'percent') {
|
| 38 |
+
return round($subtotal * ($this->value / 100), 2);
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
return min($this->value, $subtotal);
|
| 42 |
+
}
|
| 43 |
+
}
|
app/Models/TicketTier.php
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
|
| 3 |
+
namespace App\Models;
|
| 4 |
+
|
| 5 |
+
use Illuminate\Database\Eloquent\Model;
|
| 6 |
+
|
| 7 |
+
class TicketTier extends Model
|
| 8 |
+
{
|
| 9 |
+
protected $fillable = [
|
| 10 |
+
'event_id', 'name', 'price', 'quota', 'sold_count',
|
| 11 |
+
'sales_start', 'sales_end', 'max_per_order', 'is_refundable',
|
| 12 |
+
];
|
| 13 |
+
|
| 14 |
+
protected function casts(): array
|
| 15 |
+
{
|
| 16 |
+
return [
|
| 17 |
+
'price' => 'decimal:2',
|
| 18 |
+
'sales_start' => 'datetime',
|
| 19 |
+
'sales_end' => 'datetime',
|
| 20 |
+
'is_refundable' => 'boolean',
|
| 21 |
+
];
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
// ── Relationships ────────────────────────────────────
|
| 25 |
+
|
| 26 |
+
public function event()
|
| 27 |
+
{
|
| 28 |
+
return $this->belongsTo(Event::class);
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
public function orderItems()
|
| 32 |
+
{
|
| 33 |
+
return $this->hasMany(OrderItem::class);
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
// ── Helpers ──────────────────────────────────────────
|
| 37 |
+
|
| 38 |
+
public function availableStock(): int
|
| 39 |
+
{
|
| 40 |
+
return max(0, $this->quota - $this->sold_count);
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
public function isOnSale(): bool
|
| 44 |
+
{
|
| 45 |
+
$now = now();
|
| 46 |
+
|
| 47 |
+
if ($this->sales_start && $now->lt($this->sales_start)) return false;
|
| 48 |
+
if ($this->sales_end && $now->gt($this->sales_end)) return false;
|
| 49 |
+
if ($this->availableStock() <= 0) return false;
|
| 50 |
+
|
| 51 |
+
return true;
|
| 52 |
+
}
|
| 53 |
+
}
|
app/Models/User.php
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
|
| 3 |
+
namespace App\Models;
|
| 4 |
+
|
| 5 |
+
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
| 6 |
+
use Illuminate\Foundation\Auth\User as Authenticatable;
|
| 7 |
+
use Illuminate\Notifications\Notifiable;
|
| 8 |
+
|
| 9 |
+
class User extends Authenticatable
|
| 10 |
+
{
|
| 11 |
+
use HasFactory, Notifiable;
|
| 12 |
+
|
| 13 |
+
protected $fillable = ['name', 'email', 'password', 'role', 'phone'];
|
| 14 |
+
|
| 15 |
+
protected $hidden = ['password', 'remember_token'];
|
| 16 |
+
|
| 17 |
+
protected function casts(): array
|
| 18 |
+
{
|
| 19 |
+
return [
|
| 20 |
+
'email_verified_at' => 'datetime',
|
| 21 |
+
'password' => 'hashed',
|
| 22 |
+
];
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
// ── Role Helpers ─────────────────────────────────────
|
| 26 |
+
|
| 27 |
+
public function isAdmin(): bool { return $this->role === 'admin'; }
|
| 28 |
+
public function isOrganizer(): bool { return $this->role === 'organizer'; }
|
| 29 |
+
public function isCustomer(): bool { return $this->role === 'customer'; }
|
| 30 |
+
|
| 31 |
+
public function hasAdminAccess(): bool
|
| 32 |
+
{
|
| 33 |
+
return in_array($this->role, ['admin', 'organizer']);
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
// ── Relationships ────────────────────────────────────
|
| 37 |
+
|
| 38 |
+
public function events()
|
| 39 |
+
{
|
| 40 |
+
return $this->hasMany(Event::class, 'organizer_id');
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
public function orders()
|
| 44 |
+
{
|
| 45 |
+
return $this->hasMany(Order::class);
|
| 46 |
+
}
|
| 47 |
+
}
|
app/Providers/AppServiceProvider.php
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
|
| 3 |
+
namespace App\Providers;
|
| 4 |
+
|
| 5 |
+
use Illuminate\Support\ServiceProvider;
|
| 6 |
+
|
| 7 |
+
class AppServiceProvider extends ServiceProvider
|
| 8 |
+
{
|
| 9 |
+
/**
|
| 10 |
+
* Register any application services.
|
| 11 |
+
*/
|
| 12 |
+
public function register(): void
|
| 13 |
+
{
|
| 14 |
+
//
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
/**
|
| 18 |
+
* Bootstrap any application services.
|
| 19 |
+
*/
|
| 20 |
+
public function boot(): void
|
| 21 |
+
{
|
| 22 |
+
//
|
| 23 |
+
}
|
| 24 |
+
}
|
app/Services/OrderService.php
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
|
| 3 |
+
namespace App\Services;
|
| 4 |
+
|
| 5 |
+
use App\Models\Event;
|
| 6 |
+
use App\Models\Order;
|
| 7 |
+
use App\Models\OrderItem;
|
| 8 |
+
use App\Models\PromoCode;
|
| 9 |
+
use App\Models\TicketTier;
|
| 10 |
+
use App\Models\User;
|
| 11 |
+
use Illuminate\Support\Facades\DB;
|
| 12 |
+
|
| 13 |
+
class OrderService
|
| 14 |
+
{
|
| 15 |
+
const FEE_RATE = 0.03; // 3% service fee
|
| 16 |
+
const TAX_RATE = 0.11; // 11% PPN
|
| 17 |
+
const EXPIRY_MINUTES = 15;
|
| 18 |
+
|
| 19 |
+
/**
|
| 20 |
+
* Create a pending order with race-condition-safe stock deduction.
|
| 21 |
+
*
|
| 22 |
+
* @param User $user
|
| 23 |
+
* @param Event $event
|
| 24 |
+
* @param array $items [['tier_id' => int, 'qty' => int], ...]
|
| 25 |
+
* @param string|null $promoCode
|
| 26 |
+
* @return Order
|
| 27 |
+
*
|
| 28 |
+
* @throws \Exception
|
| 29 |
+
*/
|
| 30 |
+
public function createOrder(User $user, Event $event, array $items, ?string $promoCode = null): Order
|
| 31 |
+
{
|
| 32 |
+
return DB::transaction(function () use ($user, $event, $items, $promoCode) {
|
| 33 |
+
|
| 34 |
+
$subtotal = 0;
|
| 35 |
+
$orderItems = [];
|
| 36 |
+
|
| 37 |
+
foreach ($items as $item) {
|
| 38 |
+
// Lock the tier row to prevent oversell
|
| 39 |
+
$tier = TicketTier::where('id', $item['tier_id'])
|
| 40 |
+
->where('event_id', $event->id)
|
| 41 |
+
->lockForUpdate()
|
| 42 |
+
->firstOrFail();
|
| 43 |
+
|
| 44 |
+
if (!$tier->isOnSale()) {
|
| 45 |
+
throw new \Exception("Tiket \"{$tier->name}\" tidak tersedia untuk dijual saat ini.");
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
$qty = (int) $item['qty'];
|
| 49 |
+
|
| 50 |
+
if ($qty > $tier->max_per_order) {
|
| 51 |
+
throw new \Exception("Maksimal {$tier->max_per_order} tiket per order untuk \"{$tier->name}\".");
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
if ($tier->availableStock() < $qty) {
|
| 55 |
+
throw new \Exception("Kuota tiket \"{$tier->name}\" tidak mencukupi. Sisa: {$tier->availableStock()}.");
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
// Deduct stock
|
| 59 |
+
$tier->increment('sold_count', $qty);
|
| 60 |
+
|
| 61 |
+
$lineTotal = $tier->price * $qty;
|
| 62 |
+
$subtotal += $lineTotal;
|
| 63 |
+
|
| 64 |
+
$orderItems[] = [
|
| 65 |
+
'tier' => $tier,
|
| 66 |
+
'qty' => $qty,
|
| 67 |
+
'unit_price' => $tier->price,
|
| 68 |
+
'total' => $lineTotal,
|
| 69 |
+
];
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
// Calculate fees
|
| 73 |
+
$fee = round($subtotal * self::FEE_RATE, 2);
|
| 74 |
+
$tax = round($subtotal * self::TAX_RATE, 2);
|
| 75 |
+
$discount = 0;
|
| 76 |
+
|
| 77 |
+
// Apply promo code
|
| 78 |
+
if ($promoCode) {
|
| 79 |
+
$promo = PromoCode::where('code', $promoCode)->first();
|
| 80 |
+
if ($promo && $promo->isValid()) {
|
| 81 |
+
$discount = $promo->applyDiscount($subtotal);
|
| 82 |
+
$promo->increment('used_count');
|
| 83 |
+
}
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
$total = max(0, $subtotal + $fee + $tax - $discount);
|
| 87 |
+
|
| 88 |
+
// Create order
|
| 89 |
+
$order = Order::create([
|
| 90 |
+
'user_id' => $user->id,
|
| 91 |
+
'event_id' => $event->id,
|
| 92 |
+
'subtotal' => $subtotal,
|
| 93 |
+
'fee' => $fee,
|
| 94 |
+
'tax' => $tax,
|
| 95 |
+
'discount' => $discount,
|
| 96 |
+
'total' => $total,
|
| 97 |
+
'status' => 'pending',
|
| 98 |
+
'expires_at' => now()->addMinutes(self::EXPIRY_MINUTES),
|
| 99 |
+
]);
|
| 100 |
+
|
| 101 |
+
// Create order items
|
| 102 |
+
foreach ($orderItems as $oi) {
|
| 103 |
+
OrderItem::create([
|
| 104 |
+
'order_id' => $order->id,
|
| 105 |
+
'ticket_tier_id' => $oi['tier']->id,
|
| 106 |
+
'qty' => $oi['qty'],
|
| 107 |
+
'unit_price' => $oi['unit_price'],
|
| 108 |
+
'total' => $oi['total'],
|
| 109 |
+
]);
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
return $order->load('items.ticketTier', 'event');
|
| 113 |
+
});
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
/**
|
| 117 |
+
* Expire a pending order and restore stock.
|
| 118 |
+
*/
|
| 119 |
+
public function expireOrder(Order $order): void
|
| 120 |
+
{
|
| 121 |
+
if (!$order->isPending()) return;
|
| 122 |
+
|
| 123 |
+
DB::transaction(function () use ($order) {
|
| 124 |
+
$order->update(['status' => 'expired']);
|
| 125 |
+
|
| 126 |
+
foreach ($order->items as $item) {
|
| 127 |
+
$item->ticketTier->decrement('sold_count', $item->qty);
|
| 128 |
+
}
|
| 129 |
+
});
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
/**
|
| 133 |
+
* Cancel/refund an order if the event's refund policy allows.
|
| 134 |
+
*/
|
| 135 |
+
public function cancelOrder(Order $order): bool
|
| 136 |
+
{
|
| 137 |
+
if (!$order->isPaid()) return false;
|
| 138 |
+
|
| 139 |
+
// Check refund deadline: must be at least 24h before event start
|
| 140 |
+
$event = $order->event;
|
| 141 |
+
if ($event->start_at->subDay()->isPast()) return false;
|
| 142 |
+
|
| 143 |
+
DB::transaction(function () use ($order) {
|
| 144 |
+
$order->update(['status' => 'refunded']);
|
| 145 |
+
|
| 146 |
+
foreach ($order->items as $item) {
|
| 147 |
+
$item->ticketTier->decrement('sold_count', $item->qty);
|
| 148 |
+
$item->attendees()->delete();
|
| 149 |
+
}
|
| 150 |
+
});
|
| 151 |
+
|
| 152 |
+
return true;
|
| 153 |
+
}
|
| 154 |
+
}
|
app/Services/TicketService.php
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
|
| 3 |
+
namespace App\Services;
|
| 4 |
+
|
| 5 |
+
use App\Models\Attendee;
|
| 6 |
+
use App\Models\Order;
|
| 7 |
+
use Illuminate\Support\Facades\DB;
|
| 8 |
+
|
| 9 |
+
class TicketService
|
| 10 |
+
{
|
| 11 |
+
/**
|
| 12 |
+
* Process payment (mock) and generate attendees + ticket codes.
|
| 13 |
+
*/
|
| 14 |
+
public function processPayment(Order $order, string $paymentMethod = 'mock_gateway'): Order
|
| 15 |
+
{
|
| 16 |
+
if (!$order->isPending()) {
|
| 17 |
+
throw new \Exception('Order tidak dalam status pending.');
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
return DB::transaction(function () use ($order, $paymentMethod) {
|
| 21 |
+
$order->update([
|
| 22 |
+
'status' => 'paid',
|
| 23 |
+
'payment_method' => $paymentMethod,
|
| 24 |
+
'paid_at' => now(),
|
| 25 |
+
]);
|
| 26 |
+
|
| 27 |
+
// Generate attendees for each order item
|
| 28 |
+
foreach ($order->items as $item) {
|
| 29 |
+
for ($i = 0; $i < $item->qty; $i++) {
|
| 30 |
+
Attendee::create([
|
| 31 |
+
'order_item_id' => $item->id,
|
| 32 |
+
'full_name' => $order->user->name,
|
| 33 |
+
'email' => $order->user->email,
|
| 34 |
+
'phone' => $order->user->phone,
|
| 35 |
+
]);
|
| 36 |
+
}
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
return $order->fresh(['items.ticketTier', 'attendees', 'event', 'user']);
|
| 40 |
+
});
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
/**
|
| 44 |
+
* Check in an attendee by ticket code.
|
| 45 |
+
*
|
| 46 |
+
* @return array{success: bool, message: string, attendee: ?Attendee}
|
| 47 |
+
*/
|
| 48 |
+
public function checkIn(string $ticketCode): array
|
| 49 |
+
{
|
| 50 |
+
$attendee = Attendee::where('ticket_code', $ticketCode)
|
| 51 |
+
->with('orderItem.ticketTier', 'orderItem.order.event')
|
| 52 |
+
->first();
|
| 53 |
+
|
| 54 |
+
if (!$attendee) {
|
| 55 |
+
return ['success' => false, 'message' => 'Kode tiket tidak ditemukan.', 'attendee' => null];
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
// Verify order is paid
|
| 59 |
+
$order = $attendee->orderItem->order;
|
| 60 |
+
if (!$order->isPaid()) {
|
| 61 |
+
return ['success' => false, 'message' => 'Order belum dibayar (status: ' . $order->status . ').', 'attendee' => $attendee];
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
// Check if already scanned
|
| 65 |
+
if ($attendee->isCheckedIn()) {
|
| 66 |
+
return [
|
| 67 |
+
'success' => false,
|
| 68 |
+
'message' => 'Tiket sudah di-check-in pada ' . $attendee->checkin_at->format('d M Y H:i') . '.',
|
| 69 |
+
'attendee' => $attendee,
|
| 70 |
+
];
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
$attendee->checkIn();
|
| 74 |
+
|
| 75 |
+
return ['success' => true, 'message' => 'Check-in berhasil! ✅', 'attendee' => $attendee->fresh()];
|
| 76 |
+
}
|
| 77 |
+
}
|
artisan
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env php
|
| 2 |
+
<?php
|
| 3 |
+
|
| 4 |
+
use Illuminate\Foundation\Application;
|
| 5 |
+
use Symfony\Component\Console\Input\ArgvInput;
|
| 6 |
+
|
| 7 |
+
define('LARAVEL_START', microtime(true));
|
| 8 |
+
|
| 9 |
+
// Register the Composer autoloader...
|
| 10 |
+
require __DIR__.'/vendor/autoload.php';
|
| 11 |
+
|
| 12 |
+
// Bootstrap Laravel and handle the command...
|
| 13 |
+
/** @var Application $app */
|
| 14 |
+
$app = require_once __DIR__.'/bootstrap/app.php';
|
| 15 |
+
|
| 16 |
+
$status = $app->handleCommand(new ArgvInput);
|
| 17 |
+
|
| 18 |
+
exit($status);
|