Upload folder using huggingface_hub
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 +28 -35
- .gitignore +57 -0
- .kiro/specs/category-search-fix/design.md +172 -0
- .kiro/specs/category-search-fix/requirements.md +43 -0
- .kiro/specs/category-search-fix/tasks.md +43 -0
- .kiro/specs/console-interface-fix/design.md +161 -0
- .kiro/specs/console-interface-fix/requirements.md +60 -0
- .kiro/specs/console-interface-fix/tasks.md +62 -0
- .kiro/specs/dashboard-analytics-enhancement/design.md +148 -0
- .kiro/specs/dashboard-analytics-enhancement/requirements.md +73 -0
- .kiro/specs/dashboard-analytics-enhancement/tasks.md +124 -0
- .kiro/specs/dashboard-time-filter-fix/design.md +220 -0
- .kiro/specs/dashboard-time-filter-fix/requirements.md +55 -0
- .kiro/specs/dashboard-time-filter-fix/tasks.md +43 -0
- .kiro/specs/search-glassmorphism-fix/design.md +161 -0
- .kiro/specs/search-glassmorphism-fix/requirements.md +50 -0
- .kiro/specs/search-glassmorphism-fix/tasks.md +36 -0
- README.md +61 -3
- README_HF.md +85 -0
- app/Console/Commands/MigrateExistingCustomGames.php +108 -0
- app/Console/Commands/PopulateAnalyticsData.php +59 -0
- app/Console/Commands/TestDashboard.php +57 -0
- app/Http/Controllers/Api/DashboardController.php +263 -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/CartController.php +255 -0
- app/Http/Controllers/Controller.php +8 -0
- app/Http/Controllers/DashboardController.php +39 -0
- app/Http/Controllers/ProductController.php +411 -0
- app/Http/Controllers/ProfileController.php +60 -0
- app/Http/Requests/Auth/LoginRequest.php +85 -0
- app/Http/Requests/ProfileUpdateRequest.php +30 -0
- app/Models/AnalyticsSnapshot.php +63 -0
- app/Models/CustomGame.php +14 -0
- app/Models/Order.php +17 -0
- app/Models/Product.php +48 -0
- app/Models/RevenueTracking.php +59 -0
- app/Models/User.php +48 -0
- app/Providers/AppServiceProvider.php +28 -0
- app/Services/AnalyticsService.php +379 -0
- app/View/Components/AppLayout.php +17 -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 |
+
[docker-compose.yml]
|
| 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,28 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
*.
|
| 4 |
-
*.
|
| 5 |
-
*.
|
| 6 |
-
*.
|
| 7 |
-
*.
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 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
|
| 12 |
+
storage/app/public/payment_slips/Gdx6m0rw4cUSUAufq3NTNAUWyQSKjRRg7eLmIZEA.gif filter=lfs diff=lfs merge=lfs -text
|
| 13 |
+
storage/app/public/payment_slips/seALBjryd95AWErPzTvLGVfMOdvwDQVYK62vIJ5D.png filter=lfs diff=lfs merge=lfs -text
|
| 14 |
+
storage/app/public/payment_slips/YaS0Sd9UUJBNmekUXcgndzeVeGfXGMiTOnxnvEYj.jpg filter=lfs diff=lfs merge=lfs -text
|
| 15 |
+
storage/app/public/products/3Qk5m64c3TAqttQLi39MPeukQNG2rSEhZ8g1l9qI.jpg filter=lfs diff=lfs merge=lfs -text
|
| 16 |
+
storage/app/public/products/6CSKo9nXd4Xj61MEEmXeH3LO0jcVh1HCqowzQoBk.png filter=lfs diff=lfs merge=lfs -text
|
| 17 |
+
storage/app/public/products/cxSKE9LV3Zg1KzpaIFm43T9fCL3UngNSeJevsthU.jpg filter=lfs diff=lfs merge=lfs -text
|
| 18 |
+
storage/app/public/products/D3419EtxxLBwPIZQtYa3FfDXpVrQNQOHtdEfOK8U.png filter=lfs diff=lfs merge=lfs -text
|
| 19 |
+
storage/app/public/products/HdmkT7kc8AjFLIbZMseuNMLuo4MwnbWXzERl2hQQ.png filter=lfs diff=lfs merge=lfs -text
|
| 20 |
+
storage/app/public/products/HxgaBcFWUAVlXmPH4wsasrVE3NDzHqApCC4EJe76.png filter=lfs diff=lfs merge=lfs -text
|
| 21 |
+
storage/app/public/products/I1X5iyRSD2GD1Vi6dwbE37rhzgPFFQf80JDmpkFQ.jpg filter=lfs diff=lfs merge=lfs -text
|
| 22 |
+
storage/app/public/products/iG3yc6NCOj5bWJhVk4JLIUJeg42lmfWyLcPuHQdX.png filter=lfs diff=lfs merge=lfs -text
|
| 23 |
+
storage/app/public/products/Keuu1FkTKIBfFghhZqn7Ly5uw33Yetqk0DlhjDjN.jpg filter=lfs diff=lfs merge=lfs -text
|
| 24 |
+
storage/app/public/products/KOzqgZj7XUkMt2uPDYRXXKFVT1gmfRSh9RwB4Qnp.png filter=lfs diff=lfs merge=lfs -text
|
| 25 |
+
storage/app/public/products/p1IZEaJITbWLSg0A2mS4dg0zfmNzQbsoFk6vEDvy.png filter=lfs diff=lfs merge=lfs -text
|
| 26 |
+
storage/app/public/products/XYixdNVbWsh0WDYV9YiNN99CwXp6nMcoIdp07Gt7.jpg filter=lfs diff=lfs merge=lfs -text
|
| 27 |
+
storage/app/public/products/YBE7KBU74W2XxWwQFDLC8qqH3abpRM9WEFco8tt2.jpg filter=lfs diff=lfs merge=lfs -text
|
| 28 |
+
storage/app/public/products/Zl5tjkxHxKwEE69XgnJSUJgqNH0iQ32TWYv9fb8o.png filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.gitignore
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/.phpunit.cache
|
| 2 |
+
/node_modules
|
| 3 |
+
/public/build
|
| 4 |
+
/public/hot
|
| 5 |
+
/public/storage
|
| 6 |
+
/storage/*.key
|
| 7 |
+
/storage/pail
|
| 8 |
+
/storage/logs
|
| 9 |
+
/vendor
|
| 10 |
+
.env
|
| 11 |
+
.env.backup
|
| 12 |
+
.env.production
|
| 13 |
+
.env.local
|
| 14 |
+
.env.*.local
|
| 15 |
+
.phpactor.json
|
| 16 |
+
.phpunit.result.cache
|
| 17 |
+
Homestead.json
|
| 18 |
+
Homestead.yaml
|
| 19 |
+
npm-debug.log
|
| 20 |
+
yarn-error.log
|
| 21 |
+
/auth.json
|
| 22 |
+
/.fleet
|
| 23 |
+
/.idea
|
| 24 |
+
/.nova
|
| 25 |
+
/.vscode
|
| 26 |
+
/.zed
|
| 27 |
+
|
| 28 |
+
# Database files
|
| 29 |
+
/database/database.sqlite
|
| 30 |
+
*.sqlite
|
| 31 |
+
*.db
|
| 32 |
+
|
| 33 |
+
# Log files
|
| 34 |
+
*.log
|
| 35 |
+
|
| 36 |
+
# Cache and session files
|
| 37 |
+
/bootstrap/cache/*
|
| 38 |
+
/storage/framework/cache/*
|
| 39 |
+
/storage/framework/sessions/*
|
| 40 |
+
/storage/framework/views/*
|
| 41 |
+
|
| 42 |
+
# Sensitive configuration
|
| 43 |
+
/config/database.php.backup
|
| 44 |
+
/config/app.php.backup
|
| 45 |
+
|
| 46 |
+
# IDE and editor files
|
| 47 |
+
.DS_Store
|
| 48 |
+
Thumbs.db
|
| 49 |
+
|
| 50 |
+
# Temporary files
|
| 51 |
+
*.tmp
|
| 52 |
+
*.temp
|
| 53 |
+
*.swp
|
| 54 |
+
*.swo
|
| 55 |
+
|
| 56 |
+
# Kiro IDE files (optional - you might want to keep these)
|
| 57 |
+
# .kiro/
|
.kiro/specs/category-search-fix/design.md
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Design Document
|
| 2 |
+
|
| 3 |
+
## Overview
|
| 4 |
+
|
| 5 |
+
This design addresses the category selection functionality on the my-orders page where users can click "All Categories" to open a dropdown/modal and select a specific category. The current implementation opens a modal but doesn't update the "All Categories" button to reflect the selected category. This design will transform the category button to show the selected category's icon and name while maintaining the dropdown functionality.
|
| 6 |
+
|
| 7 |
+
## Architecture
|
| 8 |
+
|
| 9 |
+
The solution involves modifying the existing category selection system to:
|
| 10 |
+
|
| 11 |
+
1. **Button State Management**: Track the currently selected category and update the button appearance
|
| 12 |
+
2. **Visual Transformation**: Replace the "All Categories" button content with the selected category's icon and name
|
| 13 |
+
3. **Interaction Preservation**: Maintain the existing modal/dropdown functionality for category selection
|
| 14 |
+
4. **Filter Integration**: Ensure the visual changes work seamlessly with the existing filter system
|
| 15 |
+
|
| 16 |
+
## Components and Interfaces
|
| 17 |
+
|
| 18 |
+
### 1. Category Button Component
|
| 19 |
+
|
| 20 |
+
**Current Structure:**
|
| 21 |
+
```html
|
| 22 |
+
<div class="filter-card" data-filter="categories" onclick="showAllCategories()">
|
| 23 |
+
<div class="filter-icon bg-gradient-to-r from-purple-600 to-blue-600">
|
| 24 |
+
<i class="fas fa-th-large text-3xl"></i>
|
| 25 |
+
</div>
|
| 26 |
+
<h3 class="filter-title">All Categories</h3>
|
| 27 |
+
<div class="filter-count" id="categories-count">{{ $totalCategories }}</div>
|
| 28 |
+
<div class="filter-badge">Browse</div>
|
| 29 |
+
</div>
|
| 30 |
+
```
|
| 31 |
+
|
| 32 |
+
**Enhanced Structure:**
|
| 33 |
+
- Add dynamic content areas that can be updated via JavaScript
|
| 34 |
+
- Maintain the same onclick functionality
|
| 35 |
+
- Add data attributes to track selected category state
|
| 36 |
+
|
| 37 |
+
### 2. Category Selection System
|
| 38 |
+
|
| 39 |
+
**Current Flow:**
|
| 40 |
+
1. User clicks "All Categories" → `showAllCategories()` → Opens modal
|
| 41 |
+
2. User selects category → `selectCategory(category)` → Closes modal + `setFilter(category)`
|
| 42 |
+
|
| 43 |
+
**Enhanced Flow:**
|
| 44 |
+
1. User clicks category button → `showAllCategories()` → Opens modal
|
| 45 |
+
2. User selects category → `selectCategory(category)` → Closes modal + `setFilter(category)` + `updateCategoryButton(category)`
|
| 46 |
+
|
| 47 |
+
### 3. Category Configuration
|
| 48 |
+
|
| 49 |
+
**Game Category Mapping:**
|
| 50 |
+
```javascript
|
| 51 |
+
const categoryConfigs = {
|
| 52 |
+
'all': {
|
| 53 |
+
icon: 'fas fa-th-large',
|
| 54 |
+
color: 'from-purple-600 to-blue-600',
|
| 55 |
+
name: 'All Categories',
|
| 56 |
+
badge: 'Browse'
|
| 57 |
+
},
|
| 58 |
+
'Genshin': {
|
| 59 |
+
icon: 'fas fa-star',
|
| 60 |
+
color: 'from-yellow-400 to-orange-500',
|
| 61 |
+
name: 'Genshin Impact',
|
| 62 |
+
badge: 'Orders'
|
| 63 |
+
},
|
| 64 |
+
'Starrail': {
|
| 65 |
+
icon: 'fas fa-rocket',
|
| 66 |
+
color: 'from-purple-500 to-pink-500',
|
| 67 |
+
name: 'Honkai: Star Rail',
|
| 68 |
+
badge: 'Orders'
|
| 69 |
+
},
|
| 70 |
+
'WutheringWave': {
|
| 71 |
+
icon: 'fas fa-water',
|
| 72 |
+
color: 'from-cyan-400 to-blue-500',
|
| 73 |
+
name: 'Wuthering Waves',
|
| 74 |
+
badge: 'Orders'
|
| 75 |
+
}
|
| 76 |
+
};
|
| 77 |
+
```
|
| 78 |
+
|
| 79 |
+
## Data Models
|
| 80 |
+
|
| 81 |
+
### Category State
|
| 82 |
+
```javascript
|
| 83 |
+
{
|
| 84 |
+
selectedCategory: string, // Current selected category key
|
| 85 |
+
categoryConfig: object, // Configuration for the selected category
|
| 86 |
+
orderCount: number // Number of orders for this category
|
| 87 |
+
}
|
| 88 |
+
```
|
| 89 |
+
|
| 90 |
+
### Button Update Data
|
| 91 |
+
```javascript
|
| 92 |
+
{
|
| 93 |
+
icon: string, // Font Awesome icon class
|
| 94 |
+
color: string, // Tailwind gradient classes
|
| 95 |
+
name: string, // Display name
|
| 96 |
+
count: number, // Order count
|
| 97 |
+
badge: string // Badge text
|
| 98 |
+
}
|
| 99 |
+
```
|
| 100 |
+
|
| 101 |
+
## Error Handling
|
| 102 |
+
|
| 103 |
+
### Invalid Category Selection
|
| 104 |
+
- **Issue**: User selects a category that doesn't exist or has no orders
|
| 105 |
+
- **Solution**: Fall back to "All Categories" state and show appropriate message
|
| 106 |
+
|
| 107 |
+
### Missing Category Configuration
|
| 108 |
+
- **Issue**: A game category exists in orders but not in configuration
|
| 109 |
+
- **Solution**: Use default configuration with generic game icon and original game name
|
| 110 |
+
|
| 111 |
+
### Button Update Failures
|
| 112 |
+
- **Issue**: JavaScript fails to update button elements
|
| 113 |
+
- **Solution**: Graceful degradation - filter still works, button shows original state
|
| 114 |
+
|
| 115 |
+
## Testing Strategy
|
| 116 |
+
|
| 117 |
+
### Unit Tests
|
| 118 |
+
1. **Category Configuration Tests**
|
| 119 |
+
- Verify all game categories have proper configuration
|
| 120 |
+
- Test fallback configuration for unknown games
|
| 121 |
+
- Validate icon and color mappings
|
| 122 |
+
|
| 123 |
+
2. **Button Update Tests**
|
| 124 |
+
- Test button transformation for each category
|
| 125 |
+
- Verify icon, name, and color updates
|
| 126 |
+
- Test reset to "All Categories" state
|
| 127 |
+
|
| 128 |
+
3. **Integration Tests**
|
| 129 |
+
- Test category selection flow end-to-end
|
| 130 |
+
- Verify filter functionality works with button updates
|
| 131 |
+
- Test modal interaction with button state
|
| 132 |
+
|
| 133 |
+
### Visual Tests
|
| 134 |
+
1. **Button Appearance**
|
| 135 |
+
- Verify correct icon displays for each category
|
| 136 |
+
- Check color gradients apply correctly
|
| 137 |
+
- Ensure text and count update properly
|
| 138 |
+
|
| 139 |
+
2. **Interaction Tests**
|
| 140 |
+
- Confirm button remains clickable after transformation
|
| 141 |
+
- Test modal opens correctly from transformed button
|
| 142 |
+
- Verify hover states work on transformed button
|
| 143 |
+
|
| 144 |
+
### Edge Cases
|
| 145 |
+
1. **No Orders Scenario**
|
| 146 |
+
- Test behavior when no orders exist for a category
|
| 147 |
+
- Verify button handles zero count gracefully
|
| 148 |
+
|
| 149 |
+
2. **Multiple Rapid Selections**
|
| 150 |
+
- Test rapid category switching
|
| 151 |
+
- Ensure button updates don't conflict
|
| 152 |
+
|
| 153 |
+
3. **Browser Compatibility**
|
| 154 |
+
- Test in different browsers
|
| 155 |
+
- Verify CSS transitions work consistently
|
| 156 |
+
|
| 157 |
+
## Implementation Notes
|
| 158 |
+
|
| 159 |
+
### CSS Considerations
|
| 160 |
+
- Maintain existing hover and active states
|
| 161 |
+
- Ensure smooth transitions between category states
|
| 162 |
+
- Preserve responsive design for different screen sizes
|
| 163 |
+
|
| 164 |
+
### JavaScript Architecture
|
| 165 |
+
- Extend existing `selectCategory()` function
|
| 166 |
+
- Add new `updateCategoryButton()` function
|
| 167 |
+
- Maintain compatibility with existing filter system
|
| 168 |
+
|
| 169 |
+
### Performance
|
| 170 |
+
- Minimize DOM manipulations during button updates
|
| 171 |
+
- Cache category configurations for quick access
|
| 172 |
+
- Ensure smooth animations don't impact performance
|
.kiro/specs/category-search-fix/requirements.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Requirements Document
|
| 2 |
+
|
| 3 |
+
## Introduction
|
| 4 |
+
|
| 5 |
+
This feature addresses the category selection functionality on the my-orders page. Currently, when users click "All Categories" and select a specific category from the dropdown, the search interface does not properly update to reflect the selected category. This fix will ensure that category selection works correctly and updates the search display appropriately.
|
| 6 |
+
|
| 7 |
+
## Requirements
|
| 8 |
+
|
| 9 |
+
### Requirement 1
|
| 10 |
+
|
| 11 |
+
**User Story:** As a user browsing my orders, I want to select a specific category from the "All Categories" dropdown, so that the search interface updates to show my selected category.
|
| 12 |
+
|
| 13 |
+
#### Acceptance Criteria
|
| 14 |
+
|
| 15 |
+
1. WHEN a user clicks on "All Categories" dropdown THEN the system SHALL display a list of available categories
|
| 16 |
+
2. WHEN a user selects a specific category from the dropdown THEN the system SHALL update the search interface to display the selected category name
|
| 17 |
+
3. WHEN a category is selected THEN the system SHALL maintain all other search interface elements unchanged
|
| 18 |
+
4. WHEN a category is selected THEN the system SHALL filter the displayed orders to match the selected category
|
| 19 |
+
|
| 20 |
+
### Requirement 2
|
| 21 |
+
|
| 22 |
+
**User Story:** As a user, I want the category button to transform into the selected category, so that I can clearly see which category is active.
|
| 23 |
+
|
| 24 |
+
#### Acceptance Criteria
|
| 25 |
+
|
| 26 |
+
1. WHEN a category is selected THEN the system SHALL replace the "All Categories" button with the selected category's button
|
| 27 |
+
2. WHEN a category is selected THEN the system SHALL display the selected category's specific icon and name on the button
|
| 28 |
+
3. WHEN no specific category is selected THEN the system SHALL display "All Categories" as the default button with a generic categories icon
|
| 29 |
+
4. WHEN the dropdown is open THEN the system SHALL highlight the currently selected category option
|
| 30 |
+
5. WHEN a category is selected THEN the system SHALL close the dropdown automatically
|
| 31 |
+
6. WHEN a game category is selected THEN the system SHALL transform the button to show that game's icon and category name as the search filter button
|
| 32 |
+
7. WHEN the transformed category button is clicked THEN the system SHALL open the dropdown menu to allow selecting a different category
|
| 33 |
+
|
| 34 |
+
### Requirement 3
|
| 35 |
+
|
| 36 |
+
**User Story:** As a user, I want the category search to work seamlessly with other search functionality, so that I can combine category filtering with other search criteria.
|
| 37 |
+
|
| 38 |
+
#### Acceptance Criteria
|
| 39 |
+
|
| 40 |
+
1. WHEN a category is selected AND other search terms are present THEN the system SHALL apply both filters simultaneously
|
| 41 |
+
2. WHEN a category is selected THEN the system SHALL preserve any existing search query text
|
| 42 |
+
3. WHEN the search is cleared THEN the system SHALL reset the category selection to "All Categories"
|
| 43 |
+
4. WHEN a new category is selected THEN the system SHALL immediately update the search results without requiring additional user action
|
.kiro/specs/category-search-fix/tasks.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Implementation Plan
|
| 2 |
+
|
| 3 |
+
- [x] 1. Create category configuration system
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
- Add JavaScript object with category configurations (icon, color, name, badge)
|
| 7 |
+
- Include configurations for existing games (Genshin, Starrail, WutheringWave)
|
| 8 |
+
- Add fallback configuration for unknown categories
|
| 9 |
+
- _Requirements: 2.1, 2.3, 2.6_
|
| 10 |
+
|
| 11 |
+
- [x] 2. Implement button update functionality
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
- Create `updateCategoryButton()` function to transform the category button
|
| 15 |
+
- Update button icon, name, color, and count based on selected category
|
| 16 |
+
- Ensure smooth CSS transitions during button transformation
|
| 17 |
+
- _Requirements: 2.1, 2.2, 2.6_
|
| 18 |
+
|
| 19 |
+
- [x] 3. Enhance category selection flow
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
- Modify `selectCategory()` function to call button update after filter change
|
| 23 |
+
- Ensure button updates when "All Categories" is selected (reset to default)
|
| 24 |
+
- Maintain existing modal closing and filter setting functionality
|
| 25 |
+
- _Requirements: 1.1, 1.2, 2.7_
|
| 26 |
+
|
| 27 |
+
- [x] 4. Add category count calculation
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
- Create function to calculate order count for each category
|
| 31 |
+
- Update button count display when category is selected
|
| 32 |
+
- Handle zero count scenarios gracefully
|
| 33 |
+
- _Requirements: 1.4, 2.1_
|
| 34 |
+
|
| 35 |
+
- [x] 5. Test and validate functionality
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
- Test category selection and button transformation for all game categories
|
| 40 |
+
- Verify "All Categories" reset functionality works correctly
|
| 41 |
+
- Ensure existing filter and modal functionality remains intact
|
| 42 |
+
- Test responsive behavior and CSS transitions
|
| 43 |
+
- _Requirements: 1.1, 1.2, 1.3, 1.4, 2.1, 2.2, 2.4, 2.5, 2.6, 2.7, 3.1, 3.2, 3.3, 3.4_
|
.kiro/specs/console-interface-fix/design.md
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Console Interface Fix Design
|
| 2 |
+
|
| 3 |
+
## Overview
|
| 4 |
+
|
| 5 |
+
The Luna Console interface needs to be redesigned to fix visual glitches, improve functionality, and ensure consistent styling with the overall site theme. The design will focus on creating a clean, professional administrative terminal that integrates seamlessly with the existing glassmorphic design system.
|
| 6 |
+
|
| 7 |
+
## Architecture
|
| 8 |
+
|
| 9 |
+
### Component Structure
|
| 10 |
+
```
|
| 11 |
+
Console Interface
|
| 12 |
+
├── Overlay Container (backdrop)
|
| 13 |
+
├── Console Window
|
| 14 |
+
│ ├── Header Section
|
| 15 |
+
│ │ ├── Icon + Title
|
| 16 |
+
│ │ └── Close Button
|
| 17 |
+
│ ├── Output Area (scrollable)
|
| 18 |
+
│ └── Input Section
|
| 19 |
+
│ ├── Prompt Indicator
|
| 20 |
+
│ └── Input Field
|
| 21 |
+
└── Animation System
|
| 22 |
+
```
|
| 23 |
+
|
| 24 |
+
### State Management
|
| 25 |
+
- Console visibility state (open/closed)
|
| 26 |
+
- Input state (active/processing/disabled)
|
| 27 |
+
- Output buffer for command results
|
| 28 |
+
- Animation states for smooth transitions
|
| 29 |
+
|
| 30 |
+
## Components and Interfaces
|
| 31 |
+
|
| 32 |
+
### Console Container
|
| 33 |
+
- **Purpose**: Main wrapper with backdrop and positioning
|
| 34 |
+
- **Styling**: Full-screen overlay with blur backdrop
|
| 35 |
+
- **Behavior**: Centers console window, handles click-outside-to-close
|
| 36 |
+
|
| 37 |
+
### Console Window
|
| 38 |
+
- **Dimensions**: Fixed size (900px × 600px) with responsive adjustments
|
| 39 |
+
- **Styling**: Glassmorphic design with consistent border radius and shadows
|
| 40 |
+
- **Layout**: Flexbox with header, content, and input sections
|
| 41 |
+
|
| 42 |
+
### Header Section
|
| 43 |
+
- **Height**: 70px fixed
|
| 44 |
+
- **Content**: Icon, title, subtitle, and close button
|
| 45 |
+
- **Styling**: Gradient background matching site theme
|
| 46 |
+
- **Interactive**: Close button with hover effects
|
| 47 |
+
|
| 48 |
+
### Output Area
|
| 49 |
+
- **Height**: Flexible (450px default)
|
| 50 |
+
- **Scrolling**: Auto-scroll to bottom on new content
|
| 51 |
+
- **Styling**: Dark background with proper text contrast
|
| 52 |
+
- **Content**: Monospace font for terminal-like appearance
|
| 53 |
+
|
| 54 |
+
### Input Section
|
| 55 |
+
- **Height**: 80px fixed
|
| 56 |
+
- **Layout**: Prompt symbol + input field
|
| 57 |
+
- **Styling**: Consistent with site's input styling
|
| 58 |
+
- **Behavior**: Focus management and command processing
|
| 59 |
+
|
| 60 |
+
## Data Models
|
| 61 |
+
|
| 62 |
+
### Console State
|
| 63 |
+
```typescript
|
| 64 |
+
interface ConsoleState {
|
| 65 |
+
isVisible: boolean;
|
| 66 |
+
isProcessing: boolean;
|
| 67 |
+
outputLines: string[];
|
| 68 |
+
currentInput: string;
|
| 69 |
+
animationState: 'entering' | 'visible' | 'exiting' | 'hidden';
|
| 70 |
+
}
|
| 71 |
+
```
|
| 72 |
+
|
| 73 |
+
### Console Configuration
|
| 74 |
+
```typescript
|
| 75 |
+
interface ConsoleConfig {
|
| 76 |
+
dimensions: {
|
| 77 |
+
width: number;
|
| 78 |
+
height: number;
|
| 79 |
+
headerHeight: number;
|
| 80 |
+
inputHeight: number;
|
| 81 |
+
};
|
| 82 |
+
styling: {
|
| 83 |
+
borderRadius: string;
|
| 84 |
+
backdropBlur: string;
|
| 85 |
+
backgroundColor: string;
|
| 86 |
+
};
|
| 87 |
+
animations: {
|
| 88 |
+
enterDuration: number;
|
| 89 |
+
exitDuration: number;
|
| 90 |
+
typingSpeed: number;
|
| 91 |
+
};
|
| 92 |
+
}
|
| 93 |
+
```
|
| 94 |
+
|
| 95 |
+
## Error Handling
|
| 96 |
+
|
| 97 |
+
### Visual Error States
|
| 98 |
+
- **Invalid Commands**: Display error messages in red text
|
| 99 |
+
- **Connection Issues**: Show appropriate status indicators
|
| 100 |
+
- **Processing Errors**: Clear error messaging with recovery options
|
| 101 |
+
|
| 102 |
+
### Fallback Behaviors
|
| 103 |
+
- **Animation Failures**: Graceful degradation to instant show/hide
|
| 104 |
+
- **Styling Issues**: Fallback to basic styling if glassmorphic effects fail
|
| 105 |
+
- **Input Problems**: Basic text input as fallback
|
| 106 |
+
|
| 107 |
+
## Testing Strategy
|
| 108 |
+
|
| 109 |
+
### Visual Testing
|
| 110 |
+
- Cross-browser compatibility testing
|
| 111 |
+
- Responsive design testing on various screen sizes
|
| 112 |
+
- Animation performance testing
|
| 113 |
+
- Color contrast and accessibility testing
|
| 114 |
+
|
| 115 |
+
### Functional Testing
|
| 116 |
+
- Console open/close functionality
|
| 117 |
+
- Input field behavior and command processing
|
| 118 |
+
- Scroll behavior in output area
|
| 119 |
+
- Keyboard navigation and shortcuts
|
| 120 |
+
|
| 121 |
+
### Integration Testing
|
| 122 |
+
- Integration with existing authentication system
|
| 123 |
+
- Compatibility with site's existing CSS framework
|
| 124 |
+
- Performance impact on page load and interactions
|
| 125 |
+
|
| 126 |
+
## Implementation Notes
|
| 127 |
+
|
| 128 |
+
### CSS Improvements
|
| 129 |
+
- Fix any conflicting styles causing visual artifacts
|
| 130 |
+
- Ensure proper z-index layering
|
| 131 |
+
- Optimize animations for smooth performance
|
| 132 |
+
- Implement proper responsive breakpoints
|
| 133 |
+
|
| 134 |
+
### JavaScript Enhancements
|
| 135 |
+
- Improve event handling for better reliability
|
| 136 |
+
- Add proper error handling for edge cases
|
| 137 |
+
- Optimize animation timing and easing
|
| 138 |
+
- Implement proper cleanup on component unmount
|
| 139 |
+
|
| 140 |
+
### Accessibility Considerations
|
| 141 |
+
- Proper ARIA labels for screen readers
|
| 142 |
+
- Keyboard navigation support
|
| 143 |
+
- High contrast mode compatibility
|
| 144 |
+
- Focus management for modal behavior
|
| 145 |
+
|
| 146 |
+
## Design Decisions
|
| 147 |
+
|
| 148 |
+
### Styling Approach
|
| 149 |
+
- **Glassmorphic Theme**: Maintain consistency with site's existing design
|
| 150 |
+
- **Monospace Typography**: Use system monospace fonts for terminal feel
|
| 151 |
+
- **Color Scheme**: Dark theme with white/gray text for readability
|
| 152 |
+
|
| 153 |
+
### Animation Strategy
|
| 154 |
+
- **Smooth Transitions**: Use CSS transitions with appropriate easing
|
| 155 |
+
- **Performance**: Prefer CSS animations over JavaScript for better performance
|
| 156 |
+
- **Accessibility**: Respect user's motion preferences
|
| 157 |
+
|
| 158 |
+
### Responsive Design
|
| 159 |
+
- **Desktop First**: Optimize for desktop admin usage
|
| 160 |
+
- **Mobile Adaptation**: Scale appropriately for smaller screens
|
| 161 |
+
- **Touch Friendly**: Ensure interactive elements are appropriately sized
|
.kiro/specs/console-interface-fix/requirements.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Console Interface Fix Requirements
|
| 2 |
+
|
| 3 |
+
## Introduction
|
| 4 |
+
|
| 5 |
+
The Luna Console interface currently has visual and functional issues that need to be addressed. The console appears to have layout problems, styling inconsistencies, and potentially broken functionality that affects the user experience when accessing the administrative terminal.
|
| 6 |
+
|
| 7 |
+
## Requirements
|
| 8 |
+
|
| 9 |
+
### Requirement 1
|
| 10 |
+
|
| 11 |
+
**User Story:** As an administrator, I want the Luna Console to display properly without visual glitches, so that I can access administrative functions reliably.
|
| 12 |
+
|
| 13 |
+
#### Acceptance Criteria
|
| 14 |
+
|
| 15 |
+
1. WHEN the console is opened THEN the interface SHALL display without visual artifacts or layout issues
|
| 16 |
+
2. WHEN the console header is rendered THEN it SHALL show the correct title "Luna Console" and subtitle "Administrative Terminal" with proper styling
|
| 17 |
+
3. WHEN the console window appears THEN it SHALL have consistent glassmorphic styling matching the site's design theme
|
| 18 |
+
4. WHEN the console is displayed THEN all text SHALL be clearly readable with proper contrast
|
| 19 |
+
|
| 20 |
+
### Requirement 2
|
| 21 |
+
|
| 22 |
+
**User Story:** As an administrator, I want the console close button to work properly, so that I can dismiss the console when needed.
|
| 23 |
+
|
| 24 |
+
#### Acceptance Criteria
|
| 25 |
+
|
| 26 |
+
1. WHEN I click the close button (X) THEN the console SHALL close immediately
|
| 27 |
+
2. WHEN the close button is hovered THEN it SHALL show appropriate hover effects
|
| 28 |
+
3. WHEN the console is open THEN the close button SHALL be clearly visible and accessible
|
| 29 |
+
|
| 30 |
+
### Requirement 3
|
| 31 |
+
|
| 32 |
+
**User Story:** As an administrator, I want the console input area to function correctly, so that I can enter commands without issues.
|
| 33 |
+
|
| 34 |
+
#### Acceptance Criteria
|
| 35 |
+
|
| 36 |
+
1. WHEN the console is open THEN the input field SHALL be properly positioned and styled
|
| 37 |
+
2. WHEN I type in the console input THEN the text SHALL appear clearly without visual artifacts
|
| 38 |
+
3. WHEN the console is in processing state THEN it SHALL show appropriate loading indicators
|
| 39 |
+
4. WHEN commands are entered THEN the console output SHALL display properly formatted text
|
| 40 |
+
|
| 41 |
+
### Requirement 4
|
| 42 |
+
|
| 43 |
+
**User Story:** As an administrator, I want the console animations to work smoothly, so that the interface feels polished and professional.
|
| 44 |
+
|
| 45 |
+
#### Acceptance Criteria
|
| 46 |
+
|
| 47 |
+
1. WHEN the console opens THEN it SHALL animate smoothly into view
|
| 48 |
+
2. WHEN the console closes THEN it SHALL animate smoothly out of view
|
| 49 |
+
3. WHEN console text is being typed THEN animations SHALL be smooth and not cause visual glitches
|
| 50 |
+
4. WHEN the console state changes THEN transitions SHALL be fluid and professional
|
| 51 |
+
|
| 52 |
+
### Requirement 5
|
| 53 |
+
|
| 54 |
+
**User Story:** As an administrator, I want the console to be responsive, so that it works properly on different screen sizes.
|
| 55 |
+
|
| 56 |
+
#### Acceptance Criteria
|
| 57 |
+
|
| 58 |
+
1. WHEN viewed on desktop THEN the console SHALL display at appropriate size with proper proportions
|
| 59 |
+
2. WHEN viewed on mobile devices THEN the console SHALL adapt to smaller screens appropriately
|
| 60 |
+
3. WHEN the browser window is resized THEN the console SHALL maintain proper layout and functionality
|
.kiro/specs/console-interface-fix/tasks.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Console Interface Fix Implementation Plan
|
| 2 |
+
|
| 3 |
+
- [x] 1. Analyze and fix console CSS styling issues
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
- Identify conflicting CSS rules causing visual artifacts
|
| 8 |
+
- Fix glassmorphic styling inconsistencies
|
| 9 |
+
- Ensure proper z-index layering for modal behavior
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
- _Requirements: 1.1, 1.3_
|
| 14 |
+
|
| 15 |
+
- [ ] 2. Fix console header layout and styling
|
| 16 |
+
- Correct header section positioning and dimensions
|
| 17 |
+
- Fix icon and title alignment issues
|
| 18 |
+
- Ensure proper gradient background rendering
|
| 19 |
+
- Style close button with appropriate hover effects
|
| 20 |
+
- _Requirements: 1.2, 2.2_
|
| 21 |
+
|
| 22 |
+
- [ ] 3. Repair console output area functionality
|
| 23 |
+
- Fix output area scrolling behavior
|
| 24 |
+
- Ensure proper text rendering without artifacts
|
| 25 |
+
- Implement auto-scroll to bottom for new content
|
| 26 |
+
- Fix monospace font rendering and line spacing
|
| 27 |
+
- _Requirements: 3.4, 1.4_
|
| 28 |
+
|
| 29 |
+
- [ ] 4. Fix console input section styling and behavior
|
| 30 |
+
- Repair input field positioning and styling
|
| 31 |
+
- Fix prompt indicator alignment
|
| 32 |
+
- Ensure proper focus states and cursor visibility
|
| 33 |
+
- Fix input text rendering and character spacing
|
| 34 |
+
- _Requirements: 3.1, 3.2_
|
| 35 |
+
|
| 36 |
+
- [ ] 5. Optimize console animations and transitions
|
| 37 |
+
- Fix jerky or broken opening/closing animations
|
| 38 |
+
- Smooth out typing animations and state transitions
|
| 39 |
+
- Ensure consistent animation timing and easing
|
| 40 |
+
- Fix any animation-related visual glitches
|
| 41 |
+
- _Requirements: 4.1, 4.2, 4.3, 4.4_
|
| 42 |
+
|
| 43 |
+
- [ ] 6. Implement responsive design fixes
|
| 44 |
+
- Fix console sizing issues on different screen sizes
|
| 45 |
+
- Ensure proper mobile responsiveness
|
| 46 |
+
- Fix layout issues when browser window is resized
|
| 47 |
+
- Implement appropriate breakpoints for different devices
|
| 48 |
+
- _Requirements: 5.1, 5.2, 5.3_
|
| 49 |
+
|
| 50 |
+
- [ ] 7. Fix console close button functionality
|
| 51 |
+
- Ensure close button click events work properly
|
| 52 |
+
- Fix any event propagation issues
|
| 53 |
+
- Implement proper cleanup when console closes
|
| 54 |
+
- Test close functionality across different browsers
|
| 55 |
+
- _Requirements: 2.1, 2.3_
|
| 56 |
+
|
| 57 |
+
- [ ] 8. Test and validate console interface fixes
|
| 58 |
+
- Test console functionality across different browsers
|
| 59 |
+
- Validate visual consistency with site theme
|
| 60 |
+
- Test responsive behavior on various screen sizes
|
| 61 |
+
- Verify all animations work smoothly
|
| 62 |
+
- _Requirements: 1.1, 2.1, 3.1, 4.1, 5.1_
|
.kiro/specs/dashboard-analytics-enhancement/design.md
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Dashboard Analytics Enhancement - Design Document
|
| 2 |
+
|
| 3 |
+
## Overview
|
| 4 |
+
|
| 5 |
+
The enhanced dashboard will feature a modern glassmorphic design with real-time analytics, interactive charts, and comprehensive business metrics. The system will use Chart.js for visualizations, WebSockets for real-time updates, and a responsive grid layout optimized for all devices.
|
| 6 |
+
|
| 7 |
+
## Architecture
|
| 8 |
+
|
| 9 |
+
### Frontend Components
|
| 10 |
+
- **Dashboard Controller**: Main dashboard view with real-time data binding
|
| 11 |
+
- **Chart Components**: Reusable chart widgets (Line, Bar, Doughnut, Area)
|
| 12 |
+
- **Metric Cards**: Glassmorphic cards displaying KPIs with animations
|
| 13 |
+
- **Time Filter**: Interactive period selector (Daily/Weekly/Monthly)
|
| 14 |
+
- **Real-time Updates**: WebSocket integration for live data
|
| 15 |
+
|
| 16 |
+
### Backend Services
|
| 17 |
+
- **Analytics Service**: Calculates revenue, profit, and sales metrics
|
| 18 |
+
- **Chart Data API**: Provides formatted data for different chart types
|
| 19 |
+
- **Real-time Service**: WebSocket server for live updates
|
| 20 |
+
- **Caching Layer**: Redis for performance optimization
|
| 21 |
+
|
| 22 |
+
### Database Schema
|
| 23 |
+
- **analytics_snapshots**: Hourly/daily aggregated data
|
| 24 |
+
- **revenue_tracking**: Real-time revenue calculations
|
| 25 |
+
- **profit_margins**: Product-specific profit calculations
|
| 26 |
+
|
| 27 |
+
## Components and Interfaces
|
| 28 |
+
|
| 29 |
+
### 1. Dashboard Layout
|
| 30 |
+
```
|
| 31 |
+
┌─────────────────────────────────────────────────────────┐
|
| 32 |
+
│ Header: Time Filter (Daily/Weekly/Monthly) │
|
| 33 |
+
├─────────────────────────────────────────────────────────┤
|
| 34 |
+
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
|
| 35 |
+
│ │Revenue │ │ Profit │ │ Orders │ │ Customers│ │
|
| 36 |
+
│ │ Card │ │ Card │ │ Card │ │ Card │ │
|
| 37 |
+
│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │
|
| 38 |
+
├─────────────────────────────────────────────────────────┤
|
| 39 |
+
│ ┌─────────────────────┐ ┌─────────────────────────────┐ │
|
| 40 |
+
│ │ Revenue Chart │ │ Profit Trend Chart │ │
|
| 41 |
+
│ │ (Line/Area) │ │ (Line/Bar) │ │
|
| 42 |
+
│ └─────────────────────┘ └─────────────────────────────┘ │
|
| 43 |
+
├─────────────────────────────────────────────────────────┤
|
| 44 |
+
│ ┌─────────────────────┐ ┌─────────────────────────────┐ │
|
| 45 |
+
│ │ Top Products Chart │ │ Customer Analytics │ │
|
| 46 |
+
│ │ (Doughnut/Bar) │ │ (Mixed Chart) │ │
|
| 47 |
+
│ └─────────────────────┘ └─────────────────────────────┘ │
|
| 48 |
+
└─────────────────────────────────────────────────────────┘
|
| 49 |
+
```
|
| 50 |
+
|
| 51 |
+
### 2. Metric Cards Design
|
| 52 |
+
- **Glassmorphic Background**: `bg-white/5 backdrop-blur-2xl`
|
| 53 |
+
- **Animated Numbers**: CountUp.js for smooth number transitions
|
| 54 |
+
- **Trend Indicators**: Up/down arrows with color coding
|
| 55 |
+
- **Glow Effects**: Subtle shadows and text glows
|
| 56 |
+
|
| 57 |
+
### 3. Chart Configuration
|
| 58 |
+
- **Chart.js Integration**: Modern, interactive charts
|
| 59 |
+
- **Glassmorphic Styling**: Transparent backgrounds, subtle borders
|
| 60 |
+
- **Color Scheme**:
|
| 61 |
+
- Revenue: Emerald gradient (`from-emerald-400 to-emerald-600`)
|
| 62 |
+
- Profit: Purple gradient (`from-purple-400 to-purple-600`)
|
| 63 |
+
- Orders: Blue gradient (`from-blue-400 to-blue-600`)
|
| 64 |
+
- Customers: Orange gradient (`from-orange-400 to-orange-600`)
|
| 65 |
+
|
| 66 |
+
### 4. Real-time Updates
|
| 67 |
+
- **WebSocket Connection**: Live data streaming
|
| 68 |
+
- **Smooth Animations**: Chart data updates with transitions
|
| 69 |
+
- **Performance Optimization**: Throttled updates (max 1/second)
|
| 70 |
+
|
| 71 |
+
## Data Models
|
| 72 |
+
|
| 73 |
+
### Analytics Snapshot
|
| 74 |
+
```php
|
| 75 |
+
class AnalyticsSnapshot extends Model
|
| 76 |
+
{
|
| 77 |
+
protected $fillable = [
|
| 78 |
+
'date',
|
| 79 |
+
'hour',
|
| 80 |
+
'revenue',
|
| 81 |
+
'profit',
|
| 82 |
+
'orders_count',
|
| 83 |
+
'customers_count',
|
| 84 |
+
'avg_order_value',
|
| 85 |
+
'conversion_rate'
|
| 86 |
+
];
|
| 87 |
+
|
| 88 |
+
protected $casts = [
|
| 89 |
+
'date' => 'date',
|
| 90 |
+
'revenue' => 'decimal:2',
|
| 91 |
+
'profit' => 'decimal:2',
|
| 92 |
+
'avg_order_value' => 'decimal:2',
|
| 93 |
+
'conversion_rate' => 'decimal:4'
|
| 94 |
+
];
|
| 95 |
+
}
|
| 96 |
+
```
|
| 97 |
+
|
| 98 |
+
### Revenue Tracking
|
| 99 |
+
```php
|
| 100 |
+
class RevenueTracking extends Model
|
| 101 |
+
{
|
| 102 |
+
protected $fillable = [
|
| 103 |
+
'order_id',
|
| 104 |
+
'amount',
|
| 105 |
+
'profit_margin',
|
| 106 |
+
'created_at'
|
| 107 |
+
];
|
| 108 |
+
|
| 109 |
+
protected $casts = [
|
| 110 |
+
'amount' => 'decimal:2',
|
| 111 |
+
'profit_margin' => 'decimal:4'
|
| 112 |
+
];
|
| 113 |
+
}
|
| 114 |
+
```
|
| 115 |
+
|
| 116 |
+
## Error Handling
|
| 117 |
+
|
| 118 |
+
### Chart Loading States
|
| 119 |
+
- **Loading Skeletons**: Animated placeholders while data loads
|
| 120 |
+
- **Error States**: Graceful fallbacks for failed API calls
|
| 121 |
+
- **Retry Mechanisms**: Automatic retry for failed WebSocket connections
|
| 122 |
+
|
| 123 |
+
### Data Validation
|
| 124 |
+
- **Input Sanitization**: All user inputs validated and sanitized
|
| 125 |
+
- **Date Range Validation**: Ensure valid date ranges for filtering
|
| 126 |
+
- **Performance Limits**: Prevent excessive data requests
|
| 127 |
+
|
| 128 |
+
## Testing Strategy
|
| 129 |
+
|
| 130 |
+
### Unit Tests
|
| 131 |
+
- **Analytics Service**: Test revenue/profit calculations
|
| 132 |
+
- **Chart Data API**: Verify data formatting and aggregation
|
| 133 |
+
- **Real-time Service**: Test WebSocket connections and updates
|
| 134 |
+
|
| 135 |
+
### Integration Tests
|
| 136 |
+
- **Dashboard Loading**: Test complete dashboard initialization
|
| 137 |
+
- **Time Filter**: Verify period switching functionality
|
| 138 |
+
- **Real-time Updates**: Test live data streaming
|
| 139 |
+
|
| 140 |
+
### Performance Tests
|
| 141 |
+
- **Chart Rendering**: Measure chart load times with large datasets
|
| 142 |
+
- **WebSocket Performance**: Test concurrent connection limits
|
| 143 |
+
- **Database Queries**: Optimize analytics query performance
|
| 144 |
+
|
| 145 |
+
### Browser Tests
|
| 146 |
+
- **Cross-browser Compatibility**: Test on Chrome, Firefox, Safari, Edge
|
| 147 |
+
- **Responsive Design**: Verify layout on different screen sizes
|
| 148 |
+
- **Chart Interactions**: Test hover, click, and zoom functionality
|
.kiro/specs/dashboard-analytics-enhancement/requirements.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Dashboard Analytics Enhancement - Requirements Document
|
| 2 |
+
|
| 3 |
+
## Introduction
|
| 4 |
+
|
| 5 |
+
The current dashboard needs a complete overhaul with real-time analytics, revenue tracking, profit analysis, and interactive charts. This feature will provide comprehensive business insights with time-based filtering (daily, weekly, monthly) and modern glassmorphic design to match the existing UI aesthetic.
|
| 6 |
+
|
| 7 |
+
## Requirements
|
| 8 |
+
|
| 9 |
+
### Requirement 1
|
| 10 |
+
|
| 11 |
+
**User Story:** As an admin, I want to see real-time revenue analytics, so that I can monitor business performance instantly.
|
| 12 |
+
|
| 13 |
+
#### Acceptance Criteria
|
| 14 |
+
|
| 15 |
+
1. WHEN I access the dashboard THEN the system SHALL display current day revenue in real-time
|
| 16 |
+
2. WHEN revenue data changes THEN the dashboard SHALL update automatically without page refresh
|
| 17 |
+
3. WHEN viewing revenue metrics THEN the system SHALL show percentage change from previous period
|
| 18 |
+
4. WHEN displaying revenue THEN the system SHALL format currency properly with ฿ symbol
|
| 19 |
+
|
| 20 |
+
### Requirement 2
|
| 21 |
+
|
| 22 |
+
**User Story:** As an admin, I want interactive time-based filtering, so that I can analyze performance across different periods.
|
| 23 |
+
|
| 24 |
+
#### Acceptance Criteria
|
| 25 |
+
|
| 26 |
+
1. WHEN I select daily view THEN the system SHALL show hourly revenue breakdown for today
|
| 27 |
+
2. WHEN I select weekly view THEN the system SHALL show daily revenue for the current week
|
| 28 |
+
3. WHEN I select monthly view THEN the system SHALL show daily revenue for the current month
|
| 29 |
+
4. WHEN switching time periods THEN charts SHALL animate smoothly to new data
|
| 30 |
+
|
| 31 |
+
### Requirement 3
|
| 32 |
+
|
| 33 |
+
**User Story:** As an admin, I want profit analysis with visual charts, so that I can understand business profitability trends.
|
| 34 |
+
|
| 35 |
+
#### Acceptance Criteria
|
| 36 |
+
|
| 37 |
+
1. WHEN viewing profit metrics THEN the system SHALL calculate profit as revenue minus costs
|
| 38 |
+
2. WHEN displaying profit charts THEN the system SHALL use different colors for profit vs revenue
|
| 39 |
+
3. WHEN showing profit trends THEN the system SHALL indicate positive/negative changes clearly
|
| 40 |
+
4. WHEN viewing profit data THEN the system SHALL show profit margins as percentages
|
| 41 |
+
|
| 42 |
+
### Requirement 4
|
| 43 |
+
|
| 44 |
+
**User Story:** As an admin, I want comprehensive sales analytics, so that I can track product performance and customer behavior.
|
| 45 |
+
|
| 46 |
+
#### Acceptance Criteria
|
| 47 |
+
|
| 48 |
+
1. WHEN viewing sales data THEN the system SHALL show top-selling products with quantities
|
| 49 |
+
2. WHEN displaying customer metrics THEN the system SHALL show new vs returning customers
|
| 50 |
+
3. WHEN viewing order analytics THEN the system SHALL show average order value trends
|
| 51 |
+
4. WHEN showing product performance THEN the system SHALL display conversion rates
|
| 52 |
+
|
| 53 |
+
### Requirement 5
|
| 54 |
+
|
| 55 |
+
**User Story:** As an admin, I want a modern glassmorphic dashboard design, so that the interface matches the existing UI aesthetic.
|
| 56 |
+
|
| 57 |
+
#### Acceptance Criteria
|
| 58 |
+
|
| 59 |
+
1. WHEN viewing dashboard cards THEN they SHALL use glassmorphic design with backdrop blur
|
| 60 |
+
2. WHEN displaying charts THEN they SHALL have transparent backgrounds with subtle borders
|
| 61 |
+
3. WHEN showing metrics THEN they SHALL use consistent color scheme with glowing effects
|
| 62 |
+
4. WHEN interacting with elements THEN they SHALL have smooth hover animations
|
| 63 |
+
|
| 64 |
+
### Requirement 6
|
| 65 |
+
|
| 66 |
+
**User Story:** As an admin, I want responsive dashboard layout, so that I can access analytics on different devices.
|
| 67 |
+
|
| 68 |
+
#### Acceptance Criteria
|
| 69 |
+
|
| 70 |
+
1. WHEN viewing on desktop THEN the dashboard SHALL display in multi-column grid layout
|
| 71 |
+
2. WHEN viewing on tablet THEN the dashboard SHALL adapt to smaller screen with proper spacing
|
| 72 |
+
3. WHEN viewing on mobile THEN the dashboard SHALL stack cards vertically for optimal viewing
|
| 73 |
+
4. WHEN resizing browser THEN charts SHALL resize smoothly to fit available space
|
.kiro/specs/dashboard-analytics-enhancement/tasks.md
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Implementation Plan
|
| 2 |
+
|
| 3 |
+
- [x] 1. Set up database structure and analytics models
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
- Create analytics_snapshots migration with date, hour, revenue, profit, orders_count, customers_count columns
|
| 7 |
+
- Create revenue_tracking migration with order_id, amount, profit_margin, timestamps
|
| 8 |
+
- Implement AnalyticsSnapshot model with proper casts and relationships
|
| 9 |
+
- Implement RevenueTracking model with decimal casting for financial data
|
| 10 |
+
- _Requirements: 1.1, 3.1, 4.1_
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
- [ ] 2. Create analytics service for data calculations
|
| 15 |
+
- Implement AnalyticsService class with revenue calculation methods
|
| 16 |
+
- Create profit calculation logic (revenue - costs) with margin percentages
|
| 17 |
+
- Build aggregation methods for daily, weekly, monthly data grouping
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
- Add real-time data update methods for live dashboard updates
|
| 22 |
+
- _Requirements: 1.1, 1.2, 3.1, 3.2, 3.3_
|
| 23 |
+
|
| 24 |
+
- [ ] 3. Build chart data API endpoints
|
| 25 |
+
- Create DashboardController with analytics data endpoints
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
- Implement /api/dashboard/revenue endpoint with time period filtering
|
| 29 |
+
- Create /api/dashboard/profit endpoint with trend calculations
|
| 30 |
+
- Build /api/dashboard/sales endpoint with product performance data
|
| 31 |
+
- Add /api/dashboard/customers endpoint with new vs returning metrics
|
| 32 |
+
- _Requirements: 2.1, 2.2, 2.3, 4.1, 4.2, 4.3_
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
- [ ] 4. Install and configure Chart.js with glassmorphic styling
|
| 37 |
+
- Install Chart.js via npm and configure webpack compilation
|
| 38 |
+
- Create base chart configuration with glassmorphic theme (transparent backgrounds, subtle borders)
|
| 39 |
+
- Implement color scheme constants (emerald for revenue, purple for profit, blue for orders, orange for customers)
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
- Configure chart animations and smooth transitions for data updates
|
| 44 |
+
- _Requirements: 2.4, 5.1, 5.2, 5.3_
|
| 45 |
+
|
| 46 |
+
- [x] 5. Create glassmorphic metric cards component
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
- Build MetricCard blade component with glassmorphic styling (bg-white/5 backdrop-blur-2xl)
|
| 51 |
+
- Implement animated number display using CountUp.js for smooth transitions
|
| 52 |
+
- Add trend indicators with up/down arrows and color-coded percentage changes
|
| 53 |
+
- Create glow effects with text shadows and subtle box shadows
|
| 54 |
+
- _Requirements: 1.3, 3.3, 5.1, 5.3, 5.4_
|
| 55 |
+
|
| 56 |
+
- [ ] 6. Implement time period filter component
|
| 57 |
+
- Create TimeFilter blade component with Daily/Weekly/Monthly buttons
|
| 58 |
+
- Add active state styling with glassmorphic selected appearance
|
| 59 |
+
- Implement JavaScript for period switching with smooth chart transitions
|
| 60 |
+
- Connect filter to chart data updates via AJAX calls
|
| 61 |
+
- _Requirements: 2.1, 2.2, 2.3, 2.4_
|
| 62 |
+
|
| 63 |
+
- [ ] 7. Build main dashboard layout with responsive grid
|
| 64 |
+
- Create enhanced dashboard.blade.php with responsive CSS Grid layout
|
| 65 |
+
- Implement 4-column metric cards row for desktop, 2-column for tablet, 1-column for mobile
|
| 66 |
+
- Add 2-column chart grid for desktop, single column for mobile
|
| 67 |
+
- Ensure proper spacing and glassmorphic card styling throughout
|
| 68 |
+
- _Requirements: 5.1, 5.2, 6.1, 6.2, 6.3_
|
| 69 |
+
|
| 70 |
+
- [ ] 8. Create revenue chart component
|
| 71 |
+
- Build RevenueChart JavaScript class using Chart.js Line/Area chart
|
| 72 |
+
- Implement data fetching from /api/dashboard/revenue endpoint
|
| 73 |
+
- Add smooth animations and hover effects with glassmorphic tooltips
|
| 74 |
+
- Configure responsive chart sizing that adapts to container width
|
| 75 |
+
- _Requirements: 1.1, 1.2, 2.1, 2.4, 6.4_
|
| 76 |
+
|
| 77 |
+
- [ ] 9. Implement profit trend chart
|
| 78 |
+
- Create ProfitChart JavaScript class with Line/Bar chart combination
|
| 79 |
+
- Add profit vs revenue comparison with different colored datasets
|
| 80 |
+
- Implement profit margin percentage display in chart tooltips
|
| 81 |
+
- Configure positive/negative profit indication with green/red colors
|
| 82 |
+
- _Requirements: 3.1, 3.2, 3.3, 2.4_
|
| 83 |
+
|
| 84 |
+
- [ ] 10. Build top products performance chart
|
| 85 |
+
- Create ProductsChart JavaScript class using Doughnut/Bar chart
|
| 86 |
+
- Implement data fetching from sales endpoint with product quantities
|
| 87 |
+
- Add interactive hover effects showing product details
|
| 88 |
+
- Configure chart to show top 10 products with "Others" category for remaining
|
| 89 |
+
- _Requirements: 4.1, 4.4, 5.3, 5.4_
|
| 90 |
+
|
| 91 |
+
- [ ] 11. Create customer analytics chart
|
| 92 |
+
- Build CustomerChart JavaScript class with mixed chart type (Line + Bar)
|
| 93 |
+
- Implement new vs returning customers visualization
|
| 94 |
+
- Add average order value trend line overlay
|
| 95 |
+
- Configure conversion rate display with percentage formatting
|
| 96 |
+
- _Requirements: 4.2, 4.3, 4.4_
|
| 97 |
+
|
| 98 |
+
- [ ] 12. Implement real-time WebSocket updates
|
| 99 |
+
- Install and configure Laravel WebSockets or Pusher for real-time updates
|
| 100 |
+
- Create WebSocket events for revenue, profit, and order updates
|
| 101 |
+
- Implement JavaScript WebSocket client with automatic reconnection
|
| 102 |
+
- Add throttled update mechanism (max 1 update per second) for performance
|
| 103 |
+
- _Requirements: 1.2, 1.3, 5.4_
|
| 104 |
+
|
| 105 |
+
- [ ] 13. Add loading states and error handling
|
| 106 |
+
- Create animated skeleton loaders for charts while data is loading
|
| 107 |
+
- Implement error states with retry buttons for failed API calls
|
| 108 |
+
- Add graceful fallbacks when WebSocket connection fails
|
| 109 |
+
- Create user-friendly error messages with glassmorphic styling
|
| 110 |
+
- _Requirements: 5.4, 6.4_
|
| 111 |
+
|
| 112 |
+
- [ ] 14. Optimize performance and add caching
|
| 113 |
+
- Implement Redis caching for analytics data with 5-minute TTL
|
| 114 |
+
- Add database query optimization with proper indexes
|
| 115 |
+
- Configure chart data pagination for large datasets
|
| 116 |
+
- Implement lazy loading for charts that are not immediately visible
|
| 117 |
+
- _Requirements: 1.2, 6.4_
|
| 118 |
+
|
| 119 |
+
- [ ] 15. Create comprehensive test suite
|
| 120 |
+
- Write unit tests for AnalyticsService revenue and profit calculations
|
| 121 |
+
- Create integration tests for dashboard API endpoints
|
| 122 |
+
- Add browser tests for chart interactions and responsive design
|
| 123 |
+
- Implement performance tests for WebSocket connections and chart rendering
|
| 124 |
+
- _Requirements: 1.1, 2.4, 3.1, 4.1, 6.1, 6.2, 6.3_
|
.kiro/specs/dashboard-time-filter-fix/design.md
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Dashboard Time Filter Fix - Design Document
|
| 2 |
+
|
| 3 |
+
## Overview
|
| 4 |
+
|
| 5 |
+
This design addresses the dashboard time filtering functionality and chart rendering issues. The solution involves updating the time filter component, enhancing the JavaScript dashboard manager, and ensuring proper API data flow for different time periods.
|
| 6 |
+
|
| 7 |
+
## Architecture
|
| 8 |
+
|
| 9 |
+
### Component Structure
|
| 10 |
+
```
|
| 11 |
+
Dashboard View
|
| 12 |
+
├── Time Filter Component (x-time-filter)
|
| 13 |
+
├── Metric Cards (with period-aware comparisons)
|
| 14 |
+
├── Chart Containers (with proper data binding)
|
| 15 |
+
└── Dashboard Manager (Alpine.js component)
|
| 16 |
+
```
|
| 17 |
+
|
| 18 |
+
### Data Flow
|
| 19 |
+
```
|
| 20 |
+
User Clicks Filter → Dashboard Manager → API Request → Data Processing → Chart Update → Metric Card Update
|
| 21 |
+
```
|
| 22 |
+
|
| 23 |
+
## Components and Interfaces
|
| 24 |
+
|
| 25 |
+
### 1. Time Filter Component Enhancement
|
| 26 |
+
|
| 27 |
+
**File:** `resources/views/components/time-filter.blade.php`
|
| 28 |
+
|
| 29 |
+
**Current Issues:**
|
| 30 |
+
- Filter buttons don't properly communicate with dashboard
|
| 31 |
+
- No visual feedback for active state
|
| 32 |
+
- Missing loading states
|
| 33 |
+
|
| 34 |
+
**Design Solution:**
|
| 35 |
+
- Add Alpine.js integration with dashboard manager
|
| 36 |
+
- Implement proper active state styling
|
| 37 |
+
- Add loading indicators during data fetch
|
| 38 |
+
- Use event dispatching for filter changes
|
| 39 |
+
|
| 40 |
+
**Interface:**
|
| 41 |
+
```php
|
| 42 |
+
<div x-data="{ activeFilter: 'daily' }" class="time-filter">
|
| 43 |
+
<button @click="changeFilter('daily')" :class="activeFilter === 'daily' ? 'active' : ''">Daily</button>
|
| 44 |
+
<button @click="changeFilter('weekly')" :class="activeFilter === 'weekly' ? 'active' : ''">Weekly</button>
|
| 45 |
+
<button @click="changeFilter('monthly')" :class="activeFilter === 'monthly' ? 'active' : ''">Monthly</button>
|
| 46 |
+
</div>
|
| 47 |
+
```
|
| 48 |
+
|
| 49 |
+
### 2. Dashboard Manager JavaScript Enhancement
|
| 50 |
+
|
| 51 |
+
**File:** `resources/views/dashboard.blade.php` (JavaScript section)
|
| 52 |
+
|
| 53 |
+
**Current Issues:**
|
| 54 |
+
- Charts not initializing properly
|
| 55 |
+
- No period-aware data fetching
|
| 56 |
+
- Missing chart update functionality
|
| 57 |
+
|
| 58 |
+
**Design Solution:**
|
| 59 |
+
- Fix chart initialization sequence
|
| 60 |
+
- Implement period-aware API calls
|
| 61 |
+
- Add chart update methods for filter changes
|
| 62 |
+
- Proper error handling and loading states
|
| 63 |
+
|
| 64 |
+
**Key Methods:**
|
| 65 |
+
```javascript
|
| 66 |
+
dashboardManager() {
|
| 67 |
+
return {
|
| 68 |
+
currentPeriod: 'daily',
|
| 69 |
+
loading: false,
|
| 70 |
+
charts: {},
|
| 71 |
+
|
| 72 |
+
changeFilter(period) {
|
| 73 |
+
this.currentPeriod = period;
|
| 74 |
+
this.refreshDashboard();
|
| 75 |
+
},
|
| 76 |
+
|
| 77 |
+
async refreshDashboard() {
|
| 78 |
+
this.loading = true;
|
| 79 |
+
await this.updateCharts();
|
| 80 |
+
await this.updateMetrics();
|
| 81 |
+
this.loading = false;
|
| 82 |
+
}
|
| 83 |
+
}
|
| 84 |
+
}
|
| 85 |
+
```
|
| 86 |
+
|
| 87 |
+
### 3. API Controller Enhancement
|
| 88 |
+
|
| 89 |
+
**File:** `app/Http/Controllers/Api/DashboardController.php`
|
| 90 |
+
|
| 91 |
+
**Current Issues:**
|
| 92 |
+
- Period parameter not properly handled
|
| 93 |
+
- Chart data format inconsistent
|
| 94 |
+
- Missing proper date range calculations
|
| 95 |
+
|
| 96 |
+
**Design Solution:**
|
| 97 |
+
- Enhance period handling logic
|
| 98 |
+
- Standardize chart data format
|
| 99 |
+
- Implement proper date range calculations for each period
|
| 100 |
+
- Add data aggregation for weekly/monthly views
|
| 101 |
+
|
| 102 |
+
**Data Format:**
|
| 103 |
+
```php
|
| 104 |
+
// Daily: Hourly data points (0-23)
|
| 105 |
+
// Weekly: Daily data points (7 days)
|
| 106 |
+
// Monthly: Daily data points (30/31 days)
|
| 107 |
+
[
|
| 108 |
+
'chart_data' => [
|
| 109 |
+
['label' => '00:00', 'revenue' => 1000, 'profit' => 300, 'orders' => 5],
|
| 110 |
+
// ... more data points
|
| 111 |
+
],
|
| 112 |
+
'summary' => [
|
| 113 |
+
'current' => 5000,
|
| 114 |
+
'previous' => 4500,
|
| 115 |
+
'percentage_change' => 11.11,
|
| 116 |
+
'trend' => 'up'
|
| 117 |
+
]
|
| 118 |
+
]
|
| 119 |
+
```
|
| 120 |
+
|
| 121 |
+
### 4. Analytics Service Enhancement
|
| 122 |
+
|
| 123 |
+
**File:** `app/Services/AnalyticsService.php`
|
| 124 |
+
|
| 125 |
+
**Current Issues:**
|
| 126 |
+
- getAggregatedData method needs period-specific logic
|
| 127 |
+
- Comparison calculations need previous period data
|
| 128 |
+
- Chart data formatting inconsistent
|
| 129 |
+
|
| 130 |
+
**Design Solution:**
|
| 131 |
+
- Implement period-specific data aggregation
|
| 132 |
+
- Add previous period comparison methods
|
| 133 |
+
- Standardize data formatting across all methods
|
| 134 |
+
- Optimize database queries for different time ranges
|
| 135 |
+
|
| 136 |
+
## Data Models
|
| 137 |
+
|
| 138 |
+
### Chart Data Structure
|
| 139 |
+
```php
|
| 140 |
+
interface ChartDataPoint {
|
| 141 |
+
label: string; // "00:00", "Monday", "Jan 15"
|
| 142 |
+
revenue: number;
|
| 143 |
+
profit: number;
|
| 144 |
+
orders: number;
|
| 145 |
+
customers: number;
|
| 146 |
+
}
|
| 147 |
+
```
|
| 148 |
+
|
| 149 |
+
### Metric Summary Structure
|
| 150 |
+
```php
|
| 151 |
+
interface MetricSummary {
|
| 152 |
+
current: number;
|
| 153 |
+
previous: number;
|
| 154 |
+
percentage_change: number;
|
| 155 |
+
trend: 'up' | 'down' | 'neutral';
|
| 156 |
+
period: 'daily' | 'weekly' | 'monthly';
|
| 157 |
+
}
|
| 158 |
+
```
|
| 159 |
+
|
| 160 |
+
## Error Handling
|
| 161 |
+
|
| 162 |
+
### Frontend Error Handling
|
| 163 |
+
- Chart initialization failures → Show error message in chart container
|
| 164 |
+
- API request failures → Display "Unable to load data" message
|
| 165 |
+
- Network timeouts → Retry mechanism with exponential backoff
|
| 166 |
+
|
| 167 |
+
### Backend Error Handling
|
| 168 |
+
- Database query failures → Return empty data with success: false
|
| 169 |
+
- Invalid period parameters → Default to 'daily' with warning
|
| 170 |
+
- Missing data → Return zeros with appropriate messaging
|
| 171 |
+
|
| 172 |
+
## Testing Strategy
|
| 173 |
+
|
| 174 |
+
### Unit Tests
|
| 175 |
+
- AnalyticsService period calculations
|
| 176 |
+
- API controller response formats
|
| 177 |
+
- Chart data transformation methods
|
| 178 |
+
|
| 179 |
+
### Integration Tests
|
| 180 |
+
- Full dashboard load with different periods
|
| 181 |
+
- Filter switching functionality
|
| 182 |
+
- API endpoint responses for all periods
|
| 183 |
+
|
| 184 |
+
### Manual Testing Scenarios
|
| 185 |
+
1. Load dashboard → Verify daily view works
|
| 186 |
+
2. Click Weekly → Verify charts update with weekly data
|
| 187 |
+
3. Click Monthly → Verify charts update with monthly data
|
| 188 |
+
4. Refresh page → Verify default daily view loads
|
| 189 |
+
5. Network offline → Verify error handling
|
| 190 |
+
|
| 191 |
+
## Implementation Priority
|
| 192 |
+
|
| 193 |
+
### Phase 1: Core Functionality
|
| 194 |
+
1. Fix chart initialization
|
| 195 |
+
2. Implement basic period switching
|
| 196 |
+
3. Update API endpoints for proper period handling
|
| 197 |
+
|
| 198 |
+
### Phase 2: Enhanced Features
|
| 199 |
+
1. Add loading states and animations
|
| 200 |
+
2. Implement proper error handling
|
| 201 |
+
3. Add URL state management
|
| 202 |
+
|
| 203 |
+
### Phase 3: Polish
|
| 204 |
+
1. Smooth transitions between periods
|
| 205 |
+
2. Advanced error recovery
|
| 206 |
+
3. Performance optimizations
|
| 207 |
+
|
| 208 |
+
## Performance Considerations
|
| 209 |
+
|
| 210 |
+
- Cache API responses for 30 seconds to reduce server load
|
| 211 |
+
- Lazy load charts only when visible
|
| 212 |
+
- Debounce filter changes to prevent rapid API calls
|
| 213 |
+
- Use efficient database queries with proper indexing
|
| 214 |
+
|
| 215 |
+
## Security Considerations
|
| 216 |
+
|
| 217 |
+
- Validate period parameters on backend
|
| 218 |
+
- Sanitize date inputs to prevent SQL injection
|
| 219 |
+
- Rate limit API endpoints to prevent abuse
|
| 220 |
+
- Ensure proper authentication for dashboard access
|
.kiro/specs/dashboard-time-filter-fix/requirements.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Dashboard Time Filter Fix - Requirements Document
|
| 2 |
+
|
| 3 |
+
## Introduction
|
| 4 |
+
|
| 5 |
+
The analytics dashboard currently has non-functional time period filtering and chart display issues. Users need to be able to switch between daily, weekly, and monthly views with properly working charts that display real data for each time period.
|
| 6 |
+
|
| 7 |
+
## Requirements
|
| 8 |
+
|
| 9 |
+
### Requirement 1
|
| 10 |
+
|
| 11 |
+
**User Story:** As a dashboard user, I want to switch between daily, weekly, and monthly time periods, so that I can analyze data across different timeframes.
|
| 12 |
+
|
| 13 |
+
#### Acceptance Criteria
|
| 14 |
+
|
| 15 |
+
1. WHEN user clicks "Daily" filter THEN dashboard SHALL display hourly data for the current day
|
| 16 |
+
2. WHEN user clicks "Weekly" filter THEN dashboard SHALL display daily data for the current week
|
| 17 |
+
3. WHEN user clicks "Monthly" filter THEN dashboard SHALL display daily data for the current month
|
| 18 |
+
4. WHEN time period changes THEN all charts SHALL update to reflect the selected timeframe
|
| 19 |
+
5. WHEN time period changes THEN metric cards SHALL show comparison data from previous period (previous day/week/month)
|
| 20 |
+
|
| 21 |
+
### Requirement 2
|
| 22 |
+
|
| 23 |
+
**User Story:** As a dashboard user, I want to see working charts with real data, so that I can visualize my store's performance.
|
| 24 |
+
|
| 25 |
+
#### Acceptance Criteria
|
| 26 |
+
|
| 27 |
+
1. WHEN dashboard loads THEN revenue chart SHALL display with real data points
|
| 28 |
+
2. WHEN dashboard loads THEN profit chart SHALL show revenue vs profit comparison
|
| 29 |
+
3. WHEN dashboard loads THEN products chart SHALL display top products by revenue
|
| 30 |
+
4. WHEN dashboard loads THEN customers chart SHALL show customer activity trends
|
| 31 |
+
5. WHEN time period changes THEN all charts SHALL re-render with appropriate data
|
| 32 |
+
|
| 33 |
+
### Requirement 3
|
| 34 |
+
|
| 35 |
+
**User Story:** As a dashboard user, I want metric cards to show meaningful comparisons, so that I can understand performance trends.
|
| 36 |
+
|
| 37 |
+
#### Acceptance Criteria
|
| 38 |
+
|
| 39 |
+
1. WHEN viewing daily data THEN metric cards SHALL compare with previous day
|
| 40 |
+
2. WHEN viewing weekly data THEN metric cards SHALL compare with previous week
|
| 41 |
+
3. WHEN viewing monthly data THEN metric cards SHALL compare with previous month
|
| 42 |
+
4. WHEN comparison data is available THEN cards SHALL show percentage change and trend direction
|
| 43 |
+
5. WHEN no comparison data exists THEN cards SHALL show "No previous data" message
|
| 44 |
+
|
| 45 |
+
### Requirement 4
|
| 46 |
+
|
| 47 |
+
**User Story:** As a dashboard user, I want the time filter UI to be intuitive and responsive, so that I can easily switch between time periods.
|
| 48 |
+
|
| 49 |
+
#### Acceptance Criteria
|
| 50 |
+
|
| 51 |
+
1. WHEN dashboard loads THEN "Daily" filter SHALL be selected by default
|
| 52 |
+
2. WHEN user clicks a time filter THEN it SHALL become visually active
|
| 53 |
+
3. WHEN time filter is clicked THEN loading state SHALL be shown during data fetch
|
| 54 |
+
4. WHEN data loading completes THEN loading state SHALL be removed
|
| 55 |
+
5. WHEN filter changes THEN URL SHALL update to reflect current selection (optional)
|
.kiro/specs/dashboard-time-filter-fix/tasks.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Implementation Plan
|
| 2 |
+
|
| 3 |
+
- [ ] 1. Fix time filter component integration
|
| 4 |
+
- Update time-filter component to properly communicate with dashboard manager
|
| 5 |
+
- Add Alpine.js event dispatching for filter changes
|
| 6 |
+
- Implement active state styling and loading indicators
|
| 7 |
+
- _Requirements: 4.1, 4.2, 4.3_
|
| 8 |
+
|
| 9 |
+
- [ ] 2. Enhance dashboard JavaScript manager
|
| 10 |
+
- Fix chart initialization sequence and error handling
|
| 11 |
+
- Implement changeFilter method to handle period switching
|
| 12 |
+
- Add refreshDashboard method to update all components
|
| 13 |
+
- Create proper loading states during data fetching
|
| 14 |
+
- _Requirements: 1.4, 2.5, 4.4_
|
| 15 |
+
|
| 16 |
+
- [ ] 3. Update API controller period handling
|
| 17 |
+
- Enhance period parameter processing in all dashboard API methods
|
| 18 |
+
- Implement proper date range calculations for daily/weekly/monthly
|
| 19 |
+
- Standardize chart data format across all endpoints
|
| 20 |
+
- Add previous period comparison data to all responses
|
| 21 |
+
- _Requirements: 1.1, 1.2, 1.3, 3.1, 3.2, 3.3_
|
| 22 |
+
|
| 23 |
+
- [ ] 4. Fix analytics service data aggregation
|
| 24 |
+
- Update getAggregatedData method with period-specific logic
|
| 25 |
+
- Implement previous period calculation methods
|
| 26 |
+
- Add proper data formatting for different time ranges
|
| 27 |
+
- Optimize database queries for weekly and monthly aggregation
|
| 28 |
+
- _Requirements: 1.1, 1.2, 1.3, 3.1, 3.2, 3.3_
|
| 29 |
+
|
| 30 |
+
- [ ] 5. Update metric cards for period-aware comparisons
|
| 31 |
+
- Modify dashboard view to pass period-specific comparison data
|
| 32 |
+
- Update metric card component to handle different comparison types
|
| 33 |
+
- Add proper messaging for missing comparison data
|
| 34 |
+
- Implement percentage change calculations for all periods
|
| 35 |
+
- _Requirements: 3.1, 3.2, 3.3, 3.4, 3.5_
|
| 36 |
+
|
| 37 |
+
- [ ] 6. Test and validate all functionality
|
| 38 |
+
- Test daily, weekly, and monthly filter switching
|
| 39 |
+
- Verify all charts render correctly with real data
|
| 40 |
+
- Validate metric card comparisons for all periods
|
| 41 |
+
- Test error handling and loading states
|
| 42 |
+
- Ensure proper default state (daily) on page load
|
| 43 |
+
- _Requirements: 1.1, 1.2, 1.3, 1.4, 1.5, 2.1, 2.2, 2.3, 2.4, 2.5, 3.1, 3.2, 3.3, 3.4, 3.5, 4.1, 4.2, 4.3, 4.4, 4.5_
|
.kiro/specs/search-glassmorphism-fix/design.md
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Search Results Glassmorphism Fix - Design Document
|
| 2 |
+
|
| 3 |
+
## Overview
|
| 4 |
+
|
| 5 |
+
This design addresses the glassmorphism implementation issues in the search results dropdown by improving background opacity, text contrast, positioning, and visual hierarchy. The solution maintains the modern aesthetic while ensuring excellent readability and user experience.
|
| 6 |
+
|
| 7 |
+
## Architecture
|
| 8 |
+
|
| 9 |
+
The search results dropdown is implemented using Alpine.js with Tailwind CSS classes. The main components that need improvement are:
|
| 10 |
+
|
| 11 |
+
1. **Main Container**: The dropdown wrapper with glassmorphic background
|
| 12 |
+
2. **Content Sections**: Game categories and products sections
|
| 13 |
+
3. **Individual Items**: Game and product result items
|
| 14 |
+
4. **Hover Preview**: Product detail popup on hover
|
| 15 |
+
|
| 16 |
+
## Components and Interfaces
|
| 17 |
+
|
| 18 |
+
### 1. Search Results Container
|
| 19 |
+
**Current Issues:**
|
| 20 |
+
- `bg-black/30` is too transparent (30% opacity)
|
| 21 |
+
- `border-white/10` is barely visible (10% opacity)
|
| 22 |
+
- Positioning offset `translateX(-1.5rem)` causes misalignment
|
| 23 |
+
|
| 24 |
+
**Design Solution:**
|
| 25 |
+
- Increase background opacity to `bg-black/70` for better frosted glass effect
|
| 26 |
+
- Enhance border visibility to `border-white/20`
|
| 27 |
+
- Adjust positioning to `translateX(-0.75rem)` for better alignment
|
| 28 |
+
- Add CSS backdrop-filter for enhanced blur effect
|
| 29 |
+
|
| 30 |
+
### 2. Text Color Hierarchy
|
| 31 |
+
**Current Issues:**
|
| 32 |
+
- Loading text `text-white/60` has poor contrast
|
| 33 |
+
- Section headers `text-purple-300` and `text-green-300` are too dim
|
| 34 |
+
- Product descriptions `text-white/60` are hard to read
|
| 35 |
+
- Secondary text `text-white/50` lacks visibility
|
| 36 |
+
|
| 37 |
+
**Design Solution:**
|
| 38 |
+
```css
|
| 39 |
+
Loading State: text-white/80 (improved from 60%)
|
| 40 |
+
Section Headers: text-purple-200, text-green-200 (brighter variants)
|
| 41 |
+
Primary Text: text-white (full opacity for names)
|
| 42 |
+
Secondary Text: text-white/70 (improved from 50%)
|
| 43 |
+
Description Text: text-white/80 (improved from 60%)
|
| 44 |
+
```
|
| 45 |
+
|
| 46 |
+
### 3. Visual Hierarchy Improvements
|
| 47 |
+
**Current Issues:**
|
| 48 |
+
- Small icons and images reduce visual impact
|
| 49 |
+
- Weak hover states provide poor feedback
|
| 50 |
+
- Insufficient padding creates cramped appearance
|
| 51 |
+
|
| 52 |
+
**Design Solution:**
|
| 53 |
+
- Increase section header icons from `w-3 h-3` to `w-4 h-4`
|
| 54 |
+
- Enhance product images from `w-8 h-8` to `w-10 h-10`
|
| 55 |
+
- Improve hover backgrounds from `hover:bg-white/5` to `hover:bg-white/10`
|
| 56 |
+
- Increase padding on product items from `p-2` to `p-3`
|
| 57 |
+
|
| 58 |
+
### 4. Border and Divider Enhancement
|
| 59 |
+
**Current Issues:**
|
| 60 |
+
- `divide-white/10` dividers are barely visible
|
| 61 |
+
- Item borders `border-white/5` provide no visual separation
|
| 62 |
+
|
| 63 |
+
**Design Solution:**
|
| 64 |
+
- Strengthen dividers to `divide-white/20`
|
| 65 |
+
- Improve item borders to `border-white/10` with hover state `hover:border-white/20`
|
| 66 |
+
|
| 67 |
+
## Data Models
|
| 68 |
+
|
| 69 |
+
No data model changes required. The component works with existing search API response structure:
|
| 70 |
+
|
| 71 |
+
```javascript
|
| 72 |
+
{
|
| 73 |
+
games: [
|
| 74 |
+
{
|
| 75 |
+
name: string,
|
| 76 |
+
slug: string,
|
| 77 |
+
description: string,
|
| 78 |
+
icon: string,
|
| 79 |
+
gradient: string,
|
| 80 |
+
category: string,
|
| 81 |
+
popularity: string,
|
| 82 |
+
count: number
|
| 83 |
+
}
|
| 84 |
+
],
|
| 85 |
+
products: [
|
| 86 |
+
{
|
| 87 |
+
id: number,
|
| 88 |
+
name: string,
|
| 89 |
+
slug: string,
|
| 90 |
+
game_slug: string,
|
| 91 |
+
price: number,
|
| 92 |
+
image: string,
|
| 93 |
+
game: string,
|
| 94 |
+
amount: number,
|
| 95 |
+
stock_color: string
|
| 96 |
+
}
|
| 97 |
+
]
|
| 98 |
+
}
|
| 99 |
+
```
|
| 100 |
+
|
| 101 |
+
## Error Handling
|
| 102 |
+
|
| 103 |
+
The existing error handling remains unchanged:
|
| 104 |
+
- Network errors fall back to empty results
|
| 105 |
+
- Image loading errors use placeholder images
|
| 106 |
+
- Missing data properties are handled gracefully
|
| 107 |
+
|
| 108 |
+
## Testing Strategy
|
| 109 |
+
|
| 110 |
+
### Visual Testing
|
| 111 |
+
1. **Contrast Testing**: Verify all text meets WCAG AA contrast requirements
|
| 112 |
+
2. **Responsive Testing**: Ensure dropdown works across different screen sizes
|
| 113 |
+
3. **Browser Testing**: Test glassmorphism effects across modern browsers
|
| 114 |
+
|
| 115 |
+
### Functional Testing
|
| 116 |
+
1. **Positioning**: Verify dropdown aligns properly with search input
|
| 117 |
+
2. **Hover States**: Test all interactive elements provide clear feedback
|
| 118 |
+
3. **Keyboard Navigation**: Ensure accessibility is maintained
|
| 119 |
+
|
| 120 |
+
### Performance Testing
|
| 121 |
+
1. **Backdrop Blur**: Verify enhanced blur effects don't impact performance
|
| 122 |
+
2. **Animation Smoothness**: Test transition animations remain smooth
|
| 123 |
+
|
| 124 |
+
## Implementation Approach
|
| 125 |
+
|
| 126 |
+
### Phase 1: Core Glassmorphism
|
| 127 |
+
- Update main container background and border opacity
|
| 128 |
+
- Adjust positioning for proper alignment
|
| 129 |
+
- Add enhanced backdrop-filter CSS
|
| 130 |
+
|
| 131 |
+
### Phase 2: Text Contrast
|
| 132 |
+
- Improve all text color values for better readability
|
| 133 |
+
- Enhance section headers and primary text
|
| 134 |
+
- Strengthen secondary text visibility
|
| 135 |
+
|
| 136 |
+
### Phase 3: Visual Polish
|
| 137 |
+
- Increase icon and image sizes
|
| 138 |
+
- Improve hover states and transitions
|
| 139 |
+
- Enhance borders and dividers
|
| 140 |
+
|
| 141 |
+
### Phase 4: Testing & Refinement
|
| 142 |
+
- Cross-browser testing
|
| 143 |
+
- Accessibility validation
|
| 144 |
+
- Performance optimization if needed
|
| 145 |
+
|
| 146 |
+
## CSS Implementation Details
|
| 147 |
+
|
| 148 |
+
### Enhanced Backdrop Filter
|
| 149 |
+
```css
|
| 150 |
+
backdrop-filter: blur(20px) saturate(180%);
|
| 151 |
+
-webkit-backdrop-filter: blur(20px) saturate(180%);
|
| 152 |
+
```
|
| 153 |
+
|
| 154 |
+
### Improved Container Styling
|
| 155 |
+
```css
|
| 156 |
+
background: rgba(0, 0, 0, 0.7);
|
| 157 |
+
border: 1px solid rgba(255, 255, 255, 0.2);
|
| 158 |
+
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
| 159 |
+
```
|
| 160 |
+
|
| 161 |
+
This design ensures the search results dropdown maintains its modern glassmorphic aesthetic while providing excellent readability and user experience.
|
.kiro/specs/search-glassmorphism-fix/requirements.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Search Results Glassmorphism Fix - Requirements Document
|
| 2 |
+
|
| 3 |
+
## Introduction
|
| 4 |
+
|
| 5 |
+
The current search results dropdown has poor glassmorphism implementation with excessive transparency that makes text hard to read and lacks proper frosted glass aesthetics. This feature will improve the visual design and readability of the search results while maintaining the modern glassmorphic design language.
|
| 6 |
+
|
| 7 |
+
## Requirements
|
| 8 |
+
|
| 9 |
+
### Requirement 1
|
| 10 |
+
|
| 11 |
+
**User Story:** As a user, I want the search results dropdown to have proper frosted glass appearance, so that I can easily read the content without strain.
|
| 12 |
+
|
| 13 |
+
#### Acceptance Criteria
|
| 14 |
+
|
| 15 |
+
1. WHEN the search dropdown appears THEN the background SHALL have appropriate opacity (not too transparent)
|
| 16 |
+
2. WHEN viewing the search results THEN the backdrop blur SHALL create a proper frosted glass effect
|
| 17 |
+
3. WHEN text is displayed in the dropdown THEN it SHALL have sufficient contrast for readability
|
| 18 |
+
4. WHEN the dropdown is visible THEN borders SHALL be clearly defined with appropriate opacity
|
| 19 |
+
|
| 20 |
+
### Requirement 2
|
| 21 |
+
|
| 22 |
+
**User Story:** As a user, I want the search results to be properly positioned relative to the search bar, so that the interface looks aligned and professional.
|
| 23 |
+
|
| 24 |
+
#### Acceptance Criteria
|
| 25 |
+
|
| 26 |
+
1. WHEN the search dropdown opens THEN it SHALL align properly with the search input field
|
| 27 |
+
2. WHEN viewing the dropdown THEN the positioning SHALL not cause layout shifts or misalignment
|
| 28 |
+
3. WHEN the dropdown is displayed THEN it SHALL maintain consistent spacing from the search bar
|
| 29 |
+
|
| 30 |
+
### Requirement 3
|
| 31 |
+
|
| 32 |
+
**User Story:** As a user, I want improved text colors and contrast in search results, so that I can easily distinguish between different elements and read all information clearly.
|
| 33 |
+
|
| 34 |
+
#### Acceptance Criteria
|
| 35 |
+
|
| 36 |
+
1. WHEN viewing game categories THEN section headers SHALL have high contrast colors
|
| 37 |
+
2. WHEN viewing product information THEN product names SHALL be clearly readable
|
| 38 |
+
3. WHEN viewing metadata THEN secondary text SHALL have appropriate contrast while maintaining hierarchy
|
| 39 |
+
4. WHEN hovering over items THEN color changes SHALL provide clear visual feedback
|
| 40 |
+
|
| 41 |
+
### Requirement 4
|
| 42 |
+
|
| 43 |
+
**User Story:** As a user, I want consistent visual hierarchy in search results, so that I can quickly scan and find relevant information.
|
| 44 |
+
|
| 45 |
+
#### Acceptance Criteria
|
| 46 |
+
|
| 47 |
+
1. WHEN viewing search results THEN different content types SHALL be visually distinct
|
| 48 |
+
2. WHEN scanning results THEN important information SHALL stand out through proper typography
|
| 49 |
+
3. WHEN viewing product images THEN they SHALL be appropriately sized and styled
|
| 50 |
+
4. WHEN interacting with results THEN hover states SHALL provide clear feedback
|
.kiro/specs/search-glassmorphism-fix/tasks.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Implementation Plan
|
| 2 |
+
|
| 3 |
+
- [ ] 1. Enhance main search results container glassmorphism
|
| 4 |
+
- Update background opacity from `bg-black/30` to `bg-black/70` for better frosted glass effect
|
| 5 |
+
- Improve border visibility from `border-white/10` to `border-white/20`
|
| 6 |
+
- Adjust positioning from `translateX(-1.5rem)` to `translateX(-0.75rem)` for proper alignment
|
| 7 |
+
- Add enhanced backdrop-filter CSS for better blur effect
|
| 8 |
+
- _Requirements: 1.1, 1.2, 1.3, 1.4, 2.1, 2.2_
|
| 9 |
+
|
| 10 |
+
- [ ] 2. Improve text colors and contrast throughout search results
|
| 11 |
+
- Enhance loading state text from `text-white/60` to `text-white/80`
|
| 12 |
+
- Brighten section headers from `text-purple-300` to `text-purple-200` and `text-green-300` to `text-green-200`
|
| 13 |
+
- Improve secondary text visibility from `text-white/50` to `text-white/70`
|
| 14 |
+
- Enhance description text from `text-white/60` to `text-white/80`
|
| 15 |
+
- _Requirements: 3.1, 3.2, 3.3, 3.4_
|
| 16 |
+
|
| 17 |
+
- [ ] 3. Enhance visual hierarchy and interactive elements
|
| 18 |
+
- Increase section header icons from `w-3 h-3` to `w-4 h-4` with proper margin adjustments
|
| 19 |
+
- Improve product images from `w-8 h-8` to `w-10 h-10` with better border styling
|
| 20 |
+
- Strengthen hover backgrounds from `hover:bg-white/5` to `hover:bg-white/10`
|
| 21 |
+
- Increase product item padding from `p-2` to `p-3` for better spacing
|
| 22 |
+
- _Requirements: 4.1, 4.2, 4.3, 4.4_
|
| 23 |
+
|
| 24 |
+
- [ ] 4. Strengthen borders and dividers for better visual separation
|
| 25 |
+
- Improve main dividers from `divide-white/10` to `divide-white/20`
|
| 26 |
+
- Enhance item borders from `border-white/5` to `border-white/10`
|
| 27 |
+
- Add better hover border states `hover:border-white/20`
|
| 28 |
+
- Update product image borders from `border-white/10` to `border-white/20`
|
| 29 |
+
- _Requirements: 4.1, 4.2_
|
| 30 |
+
|
| 31 |
+
- [ ] 5. Enhance hover preview popup styling
|
| 32 |
+
- Improve popup background from `bg-black/95` to `bg-black/90` with enhanced backdrop-filter
|
| 33 |
+
- Strengthen popup borders from `border-white/20` to `border-white/30`
|
| 34 |
+
- Improve internal text colors and contrast for better readability
|
| 35 |
+
- Add proper backdrop-filter CSS for consistent glassmorphism
|
| 36 |
+
- _Requirements: 1.1, 1.3, 3.1, 3.2_
|
README.md
CHANGED
|
@@ -1,3 +1,61 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
| 27 |
+
|
| 28 |
+
You may also try the [Laravel Bootcamp](https://bootcamp.laravel.com), where you will be guided through building a modern Laravel application from scratch.
|
| 29 |
+
|
| 30 |
+
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.
|
| 31 |
+
|
| 32 |
+
## Laravel Sponsors
|
| 33 |
+
|
| 34 |
+
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).
|
| 35 |
+
|
| 36 |
+
### Premium Partners
|
| 37 |
+
|
| 38 |
+
- **[Vehikl](https://vehikl.com/)**
|
| 39 |
+
- **[Tighten Co.](https://tighten.co)**
|
| 40 |
+
- **[Kirschbaum Development Group](https://kirschbaumdevelopment.com)**
|
| 41 |
+
- **[64 Robots](https://64robots.com)**
|
| 42 |
+
- **[Curotec](https://www.curotec.com/services/technologies/laravel/)**
|
| 43 |
+
- **[DevSquad](https://devsquad.com/hire-laravel-developers)**
|
| 44 |
+
- **[Redberry](https://redberry.international/laravel-development/)**
|
| 45 |
+
- **[Active Logic](https://activelogic.com)**
|
| 46 |
+
|
| 47 |
+
## Contributing
|
| 48 |
+
|
| 49 |
+
Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions).
|
| 50 |
+
|
| 51 |
+
## Code of Conduct
|
| 52 |
+
|
| 53 |
+
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).
|
| 54 |
+
|
| 55 |
+
## Security Vulnerabilities
|
| 56 |
+
|
| 57 |
+
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.
|
| 58 |
+
|
| 59 |
+
## License
|
| 60 |
+
|
| 61 |
+
The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).
|
README_HF.md
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Laravel E-commerce Dashboard
|
| 2 |
+
|
| 3 |
+
A modern Laravel-based e-commerce platform with an advanced analytics dashboard featuring real-time metrics, interactive charts, and comprehensive product management.
|
| 4 |
+
|
| 5 |
+
## Features
|
| 6 |
+
|
| 7 |
+
### 🎯 Analytics Dashboard
|
| 8 |
+
- **Real-time Metrics**: Revenue, orders, customers, and product statistics
|
| 9 |
+
- **Interactive Charts**: Revenue trends, top products, and purchase patterns
|
| 10 |
+
- **Time Period Filtering**: Daily, weekly, and monthly views
|
| 11 |
+
- **Product Trends**: Shows what products people buy at different time periods
|
| 12 |
+
- **Glassmorphism UI**: Modern, beautiful interface with smooth animations
|
| 13 |
+
|
| 14 |
+
### 🛍️ E-commerce Features
|
| 15 |
+
- Product catalog with categories
|
| 16 |
+
- Order management system
|
| 17 |
+
- Customer tracking
|
| 18 |
+
- Search functionality with glassmorphism effects
|
| 19 |
+
- Responsive design
|
| 20 |
+
|
| 21 |
+
### 📊 Advanced Analytics
|
| 22 |
+
- Revenue tracking with period comparisons
|
| 23 |
+
- Product performance metrics
|
| 24 |
+
- Customer behavior analysis
|
| 25 |
+
- Purchase trend visualization
|
| 26 |
+
- Detailed modal views with comprehensive data
|
| 27 |
+
|
| 28 |
+
## Technology Stack
|
| 29 |
+
|
| 30 |
+
- **Backend**: Laravel 10+ with PHP 8.1+
|
| 31 |
+
- **Frontend**: Blade templates with Alpine.js
|
| 32 |
+
- **Styling**: Tailwind CSS with custom glassmorphism effects
|
| 33 |
+
- **Charts**: Chart.js for interactive visualizations
|
| 34 |
+
- **Database**: MySQL/SQLite support
|
| 35 |
+
- **Build Tools**: Vite for asset compilation
|
| 36 |
+
|
| 37 |
+
## Key Components
|
| 38 |
+
|
| 39 |
+
### Dashboard Features
|
| 40 |
+
- Time-sensitive metric cards
|
| 41 |
+
- Interactive revenue and product charts
|
| 42 |
+
- Product purchase trends by time period
|
| 43 |
+
- Modal details with comprehensive analytics
|
| 44 |
+
- Auto-refreshing data every 30 seconds
|
| 45 |
+
|
| 46 |
+
### UI/UX Highlights
|
| 47 |
+
- Glassmorphism design language
|
| 48 |
+
- Smooth transitions and animations
|
| 49 |
+
- Responsive grid layouts
|
| 50 |
+
- Custom time filter buttons with natural glow effects
|
| 51 |
+
- Enhanced contrast for better readability
|
| 52 |
+
|
| 53 |
+
## Installation
|
| 54 |
+
|
| 55 |
+
1. Clone the repository
|
| 56 |
+
2. Install dependencies: `composer install && npm install`
|
| 57 |
+
3. Set up environment: `cp .env.example .env`
|
| 58 |
+
4. Generate key: `php artisan key:generate`
|
| 59 |
+
5. Run migrations: `php artisan migrate`
|
| 60 |
+
6. Build assets: `npm run build`
|
| 61 |
+
7. Start server: `php artisan serve`
|
| 62 |
+
|
| 63 |
+
## API Endpoints
|
| 64 |
+
|
| 65 |
+
- `/api/dashboard/summary` - Overall dashboard metrics
|
| 66 |
+
- `/api/dashboard/revenue` - Revenue data with time filtering
|
| 67 |
+
- `/api/dashboard/sales` - Sales and product performance
|
| 68 |
+
- `/api/dashboard/customers` - Customer analytics
|
| 69 |
+
- `/api/dashboard/product-trends` - Product purchase trends by period
|
| 70 |
+
|
| 71 |
+
## Recent Enhancements
|
| 72 |
+
|
| 73 |
+
- Fixed color contrast issues in metric cards
|
| 74 |
+
- Enhanced time filter button styling with natural glow effects
|
| 75 |
+
- Added product purchase trends chart showing buying patterns
|
| 76 |
+
- Improved glassmorphism effects throughout the interface
|
| 77 |
+
- Optimized database queries for better performance
|
| 78 |
+
|
| 79 |
+
## Screenshots
|
| 80 |
+
|
| 81 |
+
The dashboard features a modern dark theme with glassmorphism effects, interactive charts, and comprehensive analytics views.
|
| 82 |
+
|
| 83 |
+
## License
|
| 84 |
+
|
| 85 |
+
This project is open source and available under the MIT License.
|
app/Console/Commands/MigrateExistingCustomGames.php
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
|
| 3 |
+
namespace App\Console\Commands;
|
| 4 |
+
|
| 5 |
+
use Illuminate\Console\Command;
|
| 6 |
+
use App\Models\Product;
|
| 7 |
+
use App\Models\CustomGame;
|
| 8 |
+
|
| 9 |
+
class MigrateExistingCustomGames extends Command
|
| 10 |
+
{
|
| 11 |
+
/**
|
| 12 |
+
* The name and signature of the console command.
|
| 13 |
+
*
|
| 14 |
+
* @var string
|
| 15 |
+
*/
|
| 16 |
+
protected $signature = 'migrate:custom-games';
|
| 17 |
+
|
| 18 |
+
/**
|
| 19 |
+
* The console command description.
|
| 20 |
+
*
|
| 21 |
+
* @var string
|
| 22 |
+
*/
|
| 23 |
+
protected $description = 'Migrate existing custom games from products to custom_games table';
|
| 24 |
+
|
| 25 |
+
/**
|
| 26 |
+
* Execute the console command.
|
| 27 |
+
*/
|
| 28 |
+
public function handle()
|
| 29 |
+
{
|
| 30 |
+
$this->info('Starting migration of existing custom games...');
|
| 31 |
+
|
| 32 |
+
// Get all unique custom game categories from existing products
|
| 33 |
+
$existingCustomGames = Product::select('game')
|
| 34 |
+
->distinct()
|
| 35 |
+
->whereNotNull('game')
|
| 36 |
+
->whereNotIn('game', ['Genshin', 'Starrail', 'WutheringWave', 'ZenlessZoneZero', 'Arknights', 'AzurLane'])
|
| 37 |
+
->pluck('game');
|
| 38 |
+
|
| 39 |
+
if ($existingCustomGames->isEmpty()) {
|
| 40 |
+
$this->info('No existing custom games found to migrate.');
|
| 41 |
+
return;
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
$this->info("Found {$existingCustomGames->count()} custom games to migrate:");
|
| 45 |
+
|
| 46 |
+
$customGameColors = [
|
| 47 |
+
'from-red-500 to-pink-500',
|
| 48 |
+
'from-orange-500 to-red-500',
|
| 49 |
+
'from-yellow-500 to-orange-500',
|
| 50 |
+
'from-green-500 to-teal-500',
|
| 51 |
+
'from-teal-500 to-cyan-500',
|
| 52 |
+
'from-blue-500 to-indigo-500',
|
| 53 |
+
'from-indigo-500 to-purple-500',
|
| 54 |
+
'from-purple-500 to-pink-500',
|
| 55 |
+
'from-pink-500 to-rose-500',
|
| 56 |
+
'from-emerald-500 to-green-500',
|
| 57 |
+
'from-cyan-500 to-blue-500',
|
| 58 |
+
'from-violet-500 to-purple-500'
|
| 59 |
+
];
|
| 60 |
+
|
| 61 |
+
$customGameIcons = [
|
| 62 |
+
'fas fa-gamepad',
|
| 63 |
+
'fas fa-dice',
|
| 64 |
+
'fas fa-chess',
|
| 65 |
+
'fas fa-puzzle-piece',
|
| 66 |
+
'fas fa-trophy',
|
| 67 |
+
'fas fa-crown',
|
| 68 |
+
'fas fa-gem',
|
| 69 |
+
'fas fa-fire',
|
| 70 |
+
'fas fa-bolt',
|
| 71 |
+
'fas fa-magic',
|
| 72 |
+
'fas fa-dragon',
|
| 73 |
+
'fas fa-shield',
|
| 74 |
+
'fas fa-sword',
|
| 75 |
+
'fas fa-heart',
|
| 76 |
+
'fas fa-star',
|
| 77 |
+
'fas fa-moon',
|
| 78 |
+
'fas fa-sun',
|
| 79 |
+
'fas fa-leaf',
|
| 80 |
+
'fas fa-snowflake',
|
| 81 |
+
'fas fa-mountain'
|
| 82 |
+
];
|
| 83 |
+
|
| 84 |
+
foreach ($existingCustomGames as $gameName) {
|
| 85 |
+
// Check if it already exists in custom_games table
|
| 86 |
+
$existingCustomGame = CustomGame::where('name', $gameName)->first();
|
| 87 |
+
|
| 88 |
+
if (!$existingCustomGame) {
|
| 89 |
+
// Generate consistent styling based on game name
|
| 90 |
+
$hash = crc32($gameName);
|
| 91 |
+
$colorIndex = abs($hash) % count($customGameColors);
|
| 92 |
+
$iconIndex = abs($hash >> 8) % count($customGameIcons);
|
| 93 |
+
|
| 94 |
+
CustomGame::create([
|
| 95 |
+
'name' => $gameName,
|
| 96 |
+
'icon' => $customGameIcons[$iconIndex],
|
| 97 |
+
'color_gradient' => $customGameColors[$colorIndex]
|
| 98 |
+
]);
|
| 99 |
+
|
| 100 |
+
$this->info("✓ Migrated: {$gameName}");
|
| 101 |
+
} else {
|
| 102 |
+
$this->info("- Already exists: {$gameName}");
|
| 103 |
+
}
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
$this->info('Migration completed successfully!');
|
| 107 |
+
}
|
| 108 |
+
}
|
app/Console/Commands/PopulateAnalyticsData.php
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
|
| 3 |
+
namespace App\Console\Commands;
|
| 4 |
+
|
| 5 |
+
use Illuminate\Console\Command;
|
| 6 |
+
use App\Models\Order;
|
| 7 |
+
use App\Models\RevenueTracking;
|
| 8 |
+
use Carbon\Carbon;
|
| 9 |
+
|
| 10 |
+
class PopulateAnalyticsData extends Command
|
| 11 |
+
{
|
| 12 |
+
protected $signature = 'analytics:populate';
|
| 13 |
+
protected $description = 'Populate analytics data from existing orders';
|
| 14 |
+
|
| 15 |
+
public function handle()
|
| 16 |
+
{
|
| 17 |
+
$this->info('Populating analytics data from existing orders...');
|
| 18 |
+
|
| 19 |
+
// Get all orders and create revenue tracking entries
|
| 20 |
+
$orders = Order::where('status', '!=', 'cancelled')->get();
|
| 21 |
+
|
| 22 |
+
foreach ($orders as $order) {
|
| 23 |
+
// Parse cart data if it exists
|
| 24 |
+
$cartData = json_decode($order->cart_data, true) ?? [];
|
| 25 |
+
|
| 26 |
+
if (!empty($cartData)) {
|
| 27 |
+
foreach ($cartData as $item) {
|
| 28 |
+
RevenueTracking::updateOrCreate([
|
| 29 |
+
'order_id' => $order->id,
|
| 30 |
+
'product_name' => $item['name'] ?? 'Unknown Product'
|
| 31 |
+
], [
|
| 32 |
+
'amount' => $item['price'] ?? 0,
|
| 33 |
+
'cost' => ($item['price'] ?? 0) * 0.7, // 70% cost, 30% profit
|
| 34 |
+
'profit_margin' => 0.30,
|
| 35 |
+
'quantity' => $item['quantity'] ?? 1,
|
| 36 |
+
'created_at' => $order->created_at,
|
| 37 |
+
'updated_at' => $order->updated_at
|
| 38 |
+
]);
|
| 39 |
+
}
|
| 40 |
+
} else {
|
| 41 |
+
// If no cart data, create a single entry for the order
|
| 42 |
+
RevenueTracking::updateOrCreate([
|
| 43 |
+
'order_id' => $order->id,
|
| 44 |
+
'product_name' => 'Order #' . $order->id
|
| 45 |
+
], [
|
| 46 |
+
'amount' => $order->total_amount,
|
| 47 |
+
'cost' => $order->total_amount * 0.7,
|
| 48 |
+
'profit_margin' => 0.30,
|
| 49 |
+
'quantity' => 1,
|
| 50 |
+
'created_at' => $order->created_at,
|
| 51 |
+
'updated_at' => $order->updated_at
|
| 52 |
+
]);
|
| 53 |
+
}
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
$this->info('Analytics data populated successfully!');
|
| 57 |
+
$this->info('Total revenue tracking entries: ' . RevenueTracking::count());
|
| 58 |
+
}
|
| 59 |
+
}
|
app/Console/Commands/TestDashboard.php
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
|
| 3 |
+
namespace App\Console\Commands;
|
| 4 |
+
|
| 5 |
+
use App\Services\AnalyticsService;
|
| 6 |
+
use Illuminate\Console\Command;
|
| 7 |
+
|
| 8 |
+
class TestDashboard extends Command
|
| 9 |
+
{
|
| 10 |
+
/**
|
| 11 |
+
* The name and signature of the console command.
|
| 12 |
+
*
|
| 13 |
+
* @var string
|
| 14 |
+
*/
|
| 15 |
+
protected $signature = 'test:dashboard';
|
| 16 |
+
|
| 17 |
+
/**
|
| 18 |
+
* The console command description.
|
| 19 |
+
*
|
| 20 |
+
* @var string
|
| 21 |
+
*/
|
| 22 |
+
protected $description = 'Test dashboard analytics service';
|
| 23 |
+
|
| 24 |
+
/**
|
| 25 |
+
* Execute the console command.
|
| 26 |
+
*/
|
| 27 |
+
public function handle()
|
| 28 |
+
{
|
| 29 |
+
$this->info('Testing Analytics Service...');
|
| 30 |
+
|
| 31 |
+
try {
|
| 32 |
+
$analyticsService = app(AnalyticsService::class);
|
| 33 |
+
|
| 34 |
+
$this->info('Testing getTodayRevenue...');
|
| 35 |
+
$todayRevenue = $analyticsService->getTodayRevenue();
|
| 36 |
+
$this->info('Today Revenue: ' . json_encode($todayRevenue));
|
| 37 |
+
|
| 38 |
+
$this->info('Testing getProfitData...');
|
| 39 |
+
$profitData = $analyticsService->getProfitData('today');
|
| 40 |
+
$this->info('Profit Data: ' . json_encode($profitData));
|
| 41 |
+
|
| 42 |
+
$this->info('Testing getCustomerAnalytics...');
|
| 43 |
+
$customerAnalytics = $analyticsService->getCustomerAnalytics();
|
| 44 |
+
$this->info('Customer Analytics: ' . json_encode($customerAnalytics));
|
| 45 |
+
|
| 46 |
+
$this->info('Testing getOrderStats...');
|
| 47 |
+
$orderStats = $analyticsService->getOrderStats();
|
| 48 |
+
$this->info('Order Stats: ' . json_encode($orderStats));
|
| 49 |
+
|
| 50 |
+
$this->info('✅ All tests passed!');
|
| 51 |
+
|
| 52 |
+
} catch (\Exception $e) {
|
| 53 |
+
$this->error('❌ Error: ' . $e->getMessage());
|
| 54 |
+
$this->error('File: ' . $e->getFile() . ':' . $e->getLine());
|
| 55 |
+
}
|
| 56 |
+
}
|
| 57 |
+
}
|
app/Http/Controllers/Api/DashboardController.php
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
|
| 3 |
+
namespace App\Http\Controllers\Api;
|
| 4 |
+
|
| 5 |
+
use App\Http\Controllers\Controller;
|
| 6 |
+
use App\Services\AnalyticsService;
|
| 7 |
+
use Illuminate\Http\Request;
|
| 8 |
+
use Illuminate\Http\JsonResponse;
|
| 9 |
+
use Carbon\Carbon;
|
| 10 |
+
|
| 11 |
+
class DashboardController extends Controller
|
| 12 |
+
{
|
| 13 |
+
protected AnalyticsService $analyticsService;
|
| 14 |
+
|
| 15 |
+
public function __construct(AnalyticsService $analyticsService)
|
| 16 |
+
{
|
| 17 |
+
$this->analyticsService = $analyticsService;
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
/**
|
| 21 |
+
* Get revenue data with time period filtering
|
| 22 |
+
*/
|
| 23 |
+
public function revenue(Request $request): JsonResponse
|
| 24 |
+
{
|
| 25 |
+
$period = $request->get('period', 'daily');
|
| 26 |
+
$date = $request->get('date') ? Carbon::parse($request->get('date')) : Carbon::now();
|
| 27 |
+
|
| 28 |
+
$data = $this->analyticsService->getAggregatedData($period, $date);
|
| 29 |
+
$todayRevenue = $this->analyticsService->getTodayRevenue();
|
| 30 |
+
|
| 31 |
+
return response()->json([
|
| 32 |
+
'success' => true,
|
| 33 |
+
'data' => [
|
| 34 |
+
'chart_data' => $data,
|
| 35 |
+
'summary' => $todayRevenue,
|
| 36 |
+
'period' => $period,
|
| 37 |
+
'date' => $date->format('Y-m-d')
|
| 38 |
+
]
|
| 39 |
+
]);
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
/**
|
| 43 |
+
* Get profit data with trend calculations
|
| 44 |
+
*/
|
| 45 |
+
public function profit(Request $request): JsonResponse
|
| 46 |
+
{
|
| 47 |
+
$period = $request->get('period', 'today');
|
| 48 |
+
$date = $request->get('date') ? Carbon::parse($request->get('date')) : Carbon::now();
|
| 49 |
+
|
| 50 |
+
$profitData = $this->analyticsService->getProfitData($period);
|
| 51 |
+
$chartData = $this->analyticsService->getAggregatedData($period === 'today' ? 'daily' : $period, $date);
|
| 52 |
+
|
| 53 |
+
// Format chart data for profit visualization
|
| 54 |
+
$formattedChartData = array_map(function ($item) {
|
| 55 |
+
return [
|
| 56 |
+
'label' => $item['hour'] ?? $item['day_name'] ?? $item['day'] ?? $item['date'],
|
| 57 |
+
'revenue' => $item['revenue'],
|
| 58 |
+
'profit' => $item['profit'],
|
| 59 |
+
'profit_margin' => $item['revenue'] > 0 ? (($item['profit'] / $item['revenue']) * 100) : 0
|
| 60 |
+
];
|
| 61 |
+
}, $chartData);
|
| 62 |
+
|
| 63 |
+
return response()->json([
|
| 64 |
+
'success' => true,
|
| 65 |
+
'data' => [
|
| 66 |
+
'chart_data' => $formattedChartData,
|
| 67 |
+
'summary' => $profitData,
|
| 68 |
+
'period' => $period,
|
| 69 |
+
'date' => $date->format('Y-m-d')
|
| 70 |
+
]
|
| 71 |
+
]);
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
/**
|
| 75 |
+
* Get sales analytics with product performance
|
| 76 |
+
*/
|
| 77 |
+
public function sales(Request $request): JsonResponse
|
| 78 |
+
{
|
| 79 |
+
$limit = $request->get('limit', 10);
|
| 80 |
+
|
| 81 |
+
$topProducts = $this->analyticsService->getTopProducts($limit);
|
| 82 |
+
$period = $request->get('period', 'daily');
|
| 83 |
+
$date = $request->get('date') ? Carbon::parse($request->get('date')) : Carbon::now();
|
| 84 |
+
|
| 85 |
+
$salesData = $this->analyticsService->getAggregatedData($period, $date);
|
| 86 |
+
|
| 87 |
+
// Format sales chart data
|
| 88 |
+
$formattedSalesData = array_map(function ($item) {
|
| 89 |
+
return [
|
| 90 |
+
'label' => $item['hour'] ?? $item['day_name'] ?? $item['day'] ?? $item['date'],
|
| 91 |
+
'orders' => $item['orders'],
|
| 92 |
+
'revenue' => $item['revenue'],
|
| 93 |
+
'avg_order_value' => $item['orders'] > 0 ? ($item['revenue'] / $item['orders']) : 0
|
| 94 |
+
];
|
| 95 |
+
}, $salesData);
|
| 96 |
+
|
| 97 |
+
return response()->json([
|
| 98 |
+
'success' => true,
|
| 99 |
+
'data' => [
|
| 100 |
+
'top_products' => $topProducts,
|
| 101 |
+
'sales_chart' => $formattedSalesData,
|
| 102 |
+
'period' => $period,
|
| 103 |
+
'date' => $date->format('Y-m-d')
|
| 104 |
+
]
|
| 105 |
+
]);
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
/**
|
| 109 |
+
* Get customer analytics with new vs returning metrics
|
| 110 |
+
*/
|
| 111 |
+
public function customers(Request $request): JsonResponse
|
| 112 |
+
{
|
| 113 |
+
$customerAnalytics = $this->analyticsService->getCustomerAnalytics();
|
| 114 |
+
$period = $request->get('period', 'daily');
|
| 115 |
+
$date = $request->get('date') ? Carbon::parse($request->get('date')) : Carbon::now();
|
| 116 |
+
|
| 117 |
+
$customerData = $this->analyticsService->getAggregatedData($period, $date);
|
| 118 |
+
|
| 119 |
+
// Format customer chart data
|
| 120 |
+
$formattedCustomerData = array_map(function ($item) {
|
| 121 |
+
return [
|
| 122 |
+
'label' => $item['hour'] ?? $item['day_name'] ?? $item['day'] ?? $item['date'],
|
| 123 |
+
'customers' => $item['customers'],
|
| 124 |
+
'orders' => $item['orders'],
|
| 125 |
+
'conversion_rate' => $item['customers'] > 0 ? (($item['orders'] / $item['customers']) * 100) : 0
|
| 126 |
+
];
|
| 127 |
+
}, $customerData);
|
| 128 |
+
|
| 129 |
+
return response()->json([
|
| 130 |
+
'success' => true,
|
| 131 |
+
'data' => [
|
| 132 |
+
'analytics' => $customerAnalytics,
|
| 133 |
+
'chart_data' => $formattedCustomerData,
|
| 134 |
+
'period' => $period,
|
| 135 |
+
'date' => $date->format('Y-m-d')
|
| 136 |
+
]
|
| 137 |
+
]);
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
/**
|
| 141 |
+
* Get all dashboard metrics summary with period support
|
| 142 |
+
*/
|
| 143 |
+
public function summary(Request $request): JsonResponse
|
| 144 |
+
{
|
| 145 |
+
$period = $request->get('period', 'daily');
|
| 146 |
+
$date = $request->get('date') ? Carbon::parse($request->get('date')) : Carbon::now();
|
| 147 |
+
|
| 148 |
+
// Get period-specific data
|
| 149 |
+
$revenueData = $this->getRevenueForPeriod($period, $date);
|
| 150 |
+
$profitData = $this->analyticsService->getProfitData($period);
|
| 151 |
+
$customerAnalytics = $this->analyticsService->getCustomerAnalytics();
|
| 152 |
+
$orderStats = $this->getOrdersForPeriod($period, $date);
|
| 153 |
+
$productStats = $this->analyticsService->getProductStats();
|
| 154 |
+
$topProducts = $this->analyticsService->getTopProducts(5);
|
| 155 |
+
|
| 156 |
+
return response()->json([
|
| 157 |
+
'success' => true,
|
| 158 |
+
'data' => [
|
| 159 |
+
'revenue' => $revenueData,
|
| 160 |
+
'profit' => $profitData,
|
| 161 |
+
'orders' => $orderStats,
|
| 162 |
+
'customers' => $customerAnalytics,
|
| 163 |
+
'products' => $productStats,
|
| 164 |
+
'top_products' => $topProducts,
|
| 165 |
+
'period' => $period,
|
| 166 |
+
'date' => $date->format('Y-m-d'),
|
| 167 |
+
'last_updated' => Carbon::now()->toISOString()
|
| 168 |
+
]
|
| 169 |
+
]);
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
/**
|
| 173 |
+
* Get revenue data for specific period
|
| 174 |
+
*/
|
| 175 |
+
private function getRevenueForPeriod(string $period, Carbon $date): array
|
| 176 |
+
{
|
| 177 |
+
switch ($period) {
|
| 178 |
+
case 'weekly':
|
| 179 |
+
$current = \App\Models\Order::whereBetween('created_at', [
|
| 180 |
+
$date->copy()->startOfWeek(),
|
| 181 |
+
$date->copy()->endOfWeek()
|
| 182 |
+
])->sum('total_amount');
|
| 183 |
+
|
| 184 |
+
$previous = \App\Models\Order::whereBetween('created_at', [
|
| 185 |
+
$date->copy()->subWeek()->startOfWeek(),
|
| 186 |
+
$date->copy()->subWeek()->endOfWeek()
|
| 187 |
+
])->sum('total_amount');
|
| 188 |
+
break;
|
| 189 |
+
|
| 190 |
+
case 'monthly':
|
| 191 |
+
$current = \App\Models\Order::whereBetween('created_at', [
|
| 192 |
+
$date->copy()->startOfMonth(),
|
| 193 |
+
$date->copy()->endOfMonth()
|
| 194 |
+
])->sum('total_amount');
|
| 195 |
+
|
| 196 |
+
$previous = \App\Models\Order::whereBetween('created_at', [
|
| 197 |
+
$date->copy()->subMonth()->startOfMonth(),
|
| 198 |
+
$date->copy()->subMonth()->endOfMonth()
|
| 199 |
+
])->sum('total_amount');
|
| 200 |
+
break;
|
| 201 |
+
|
| 202 |
+
default: // daily
|
| 203 |
+
$current = \App\Models\Order::whereDate('created_at', $date)->sum('total_amount');
|
| 204 |
+
$previous = \App\Models\Order::whereDate('created_at', $date->copy()->subDay())->sum('total_amount');
|
| 205 |
+
break;
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
$percentageChange = $previous > 0 ? (($current - $previous) / $previous) * 100 : 0;
|
| 209 |
+
|
| 210 |
+
return [
|
| 211 |
+
'current' => $current,
|
| 212 |
+
'previous' => $previous,
|
| 213 |
+
'percentage_change' => round($percentageChange, 2),
|
| 214 |
+
'trend' => $percentageChange >= 0 ? 'up' : 'down'
|
| 215 |
+
];
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
/**
|
| 219 |
+
* Get orders data for specific period
|
| 220 |
+
*/
|
| 221 |
+
private function getOrdersForPeriod(string $period, Carbon $date): array
|
| 222 |
+
{
|
| 223 |
+
switch ($period) {
|
| 224 |
+
case 'weekly':
|
| 225 |
+
$current = \App\Models\Order::whereBetween('created_at', [
|
| 226 |
+
$date->copy()->startOfWeek(),
|
| 227 |
+
$date->copy()->endOfWeek()
|
| 228 |
+
])->count();
|
| 229 |
+
|
| 230 |
+
$previous = \App\Models\Order::whereBetween('created_at', [
|
| 231 |
+
$date->copy()->subWeek()->startOfWeek(),
|
| 232 |
+
$date->copy()->subWeek()->endOfWeek()
|
| 233 |
+
])->count();
|
| 234 |
+
break;
|
| 235 |
+
|
| 236 |
+
case 'monthly':
|
| 237 |
+
$current = \App\Models\Order::whereBetween('created_at', [
|
| 238 |
+
$date->copy()->startOfMonth(),
|
| 239 |
+
$date->copy()->endOfMonth()
|
| 240 |
+
])->count();
|
| 241 |
+
|
| 242 |
+
$previous = \App\Models\Order::whereBetween('created_at', [
|
| 243 |
+
$date->copy()->subMonth()->startOfMonth(),
|
| 244 |
+
$date->copy()->subMonth()->endOfMonth()
|
| 245 |
+
])->count();
|
| 246 |
+
break;
|
| 247 |
+
|
| 248 |
+
default: // daily
|
| 249 |
+
$current = \App\Models\Order::whereDate('created_at', $date)->count();
|
| 250 |
+
$previous = \App\Models\Order::whereDate('created_at', $date->copy()->subDay())->count();
|
| 251 |
+
break;
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
$percentageChange = $previous > 0 ? (($current - $previous) / $previous) * 100 : 0;
|
| 255 |
+
|
| 256 |
+
return [
|
| 257 |
+
'current' => $current,
|
| 258 |
+
'previous' => $previous,
|
| 259 |
+
'percentage_change' => round($percentageChange, 2),
|
| 260 |
+
'trend' => $percentageChange >= 0 ? 'up' : 'down'
|
| 261 |
+
];
|
| 262 |
+
}
|
| 263 |
+
}
|
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/CartController.php
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
|
| 3 |
+
namespace App\Http\Controllers;
|
| 4 |
+
|
| 5 |
+
use Illuminate\Http\Request;
|
| 6 |
+
use Illuminate\Support\Facades\Storage;
|
| 7 |
+
use App\Models\Product;
|
| 8 |
+
use App\Models\Order;
|
| 9 |
+
|
| 10 |
+
class CartController extends Controller
|
| 11 |
+
{
|
| 12 |
+
public function index()
|
| 13 |
+
{
|
| 14 |
+
$cart = session()->get('cart', []);
|
| 15 |
+
return view('cart', compact('cart'));
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
public function add(Request $request, $id)
|
| 19 |
+
{
|
| 20 |
+
$product = Product::findOrFail($id);
|
| 21 |
+
$quantity = (int) $request->input('quantity', 1);
|
| 22 |
+
|
| 23 |
+
// Check if product has enough stock
|
| 24 |
+
if ($product->Amount < $quantity) {
|
| 25 |
+
if ($request->ajax()) {
|
| 26 |
+
return response()->json([
|
| 27 |
+
'success' => false,
|
| 28 |
+
'message' => 'Insufficient stock. Available: ' . $product->Amount
|
| 29 |
+
]);
|
| 30 |
+
}
|
| 31 |
+
return back()->with('error', 'Insufficient stock. Available: ' . $product->Amount);
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
$cart = session()->get('cart', []);
|
| 35 |
+
|
| 36 |
+
if (isset($cart[$id])) {
|
| 37 |
+
$newQuantity = $cart[$id]['quantity'] + $quantity;
|
| 38 |
+
if ($newQuantity > $product->Amount) {
|
| 39 |
+
if ($request->ajax()) {
|
| 40 |
+
return response()->json([
|
| 41 |
+
'success' => false,
|
| 42 |
+
'message' => 'Cannot add more items. Stock limit: ' . $product->Amount
|
| 43 |
+
]);
|
| 44 |
+
}
|
| 45 |
+
return back()->with('error', 'Cannot add more items. Stock limit: ' . $product->Amount);
|
| 46 |
+
}
|
| 47 |
+
$cart[$id]['quantity'] = $newQuantity;
|
| 48 |
+
} else {
|
| 49 |
+
$cart[$id] = [
|
| 50 |
+
'id' => $product->id,
|
| 51 |
+
'name' => $product->name,
|
| 52 |
+
'image' => $product->image,
|
| 53 |
+
'price' => $product->price,
|
| 54 |
+
'quantity' => $quantity,
|
| 55 |
+
'game' => $product->game,
|
| 56 |
+
];
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
session()->put('cart', $cart);
|
| 60 |
+
|
| 61 |
+
// Return JSON response for AJAX requests
|
| 62 |
+
if ($request->ajax()) {
|
| 63 |
+
return response()->json([
|
| 64 |
+
'success' => true,
|
| 65 |
+
'message' => 'Item added to cart successfully',
|
| 66 |
+
'cart_count' => array_sum(array_column($cart, 'quantity'))
|
| 67 |
+
]);
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
return redirect()->route('cart.index')->with('success', 'เพิ่มสินค้าลงตะกร้าเรียบร้อยแล้ว');
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
public function remove($id)
|
| 74 |
+
{
|
| 75 |
+
$cart = session()->get('cart', []);
|
| 76 |
+
|
| 77 |
+
if (isset($cart[$id])) {
|
| 78 |
+
unset($cart[$id]);
|
| 79 |
+
session()->put('cart', $cart);
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
return redirect()->route('cart.index')->with('success', 'ลบสินค้าออกจากตะกร้าเรียบร้อยแล้ว');
|
| 83 |
+
}
|
| 84 |
+
public function checkout(Request $request)
|
| 85 |
+
{
|
| 86 |
+
$request->validate([
|
| 87 |
+
'customer_name' => 'required|string|max:255',
|
| 88 |
+
'phone' => 'required|string|max:20',
|
| 89 |
+
'payment_slip' => 'required|image|mimes:jpeg,png,jpg,gif|max:10240', // 10MB limit
|
| 90 |
+
]);
|
| 91 |
+
|
| 92 |
+
try {
|
| 93 |
+
$path = $request->file('payment_slip')->store('payment_slips', 'public');
|
| 94 |
+
|
| 95 |
+
// Get cart data
|
| 96 |
+
$cart = session()->get('cart', []);
|
| 97 |
+
|
| 98 |
+
if (empty($cart)) {
|
| 99 |
+
return back()->with('error', 'Your cart is empty');
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
// Calculate total amount and validate stock
|
| 103 |
+
$totalAmount = 0;
|
| 104 |
+
foreach ($cart as $item) {
|
| 105 |
+
$product = Product::find($item['id']);
|
| 106 |
+
if (!$product) {
|
| 107 |
+
return back()->with('error', "Product {$item['name']} no longer exists");
|
| 108 |
+
}
|
| 109 |
+
if ($product->Amount < $item['quantity']) {
|
| 110 |
+
return back()->with('error', "Insufficient stock for {$product->name}. Available: {$product->Amount}, Requested: {$item['quantity']}");
|
| 111 |
+
}
|
| 112 |
+
$totalAmount += $item['price'] * $item['quantity'];
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
// Create order
|
| 116 |
+
$order = Order::create([
|
| 117 |
+
'customer_name' => $request->customer_name,
|
| 118 |
+
'phone' => $request->phone,
|
| 119 |
+
'payment_slip_path' => $path,
|
| 120 |
+
'cart_data' => json_encode($cart),
|
| 121 |
+
'total_amount' => $totalAmount,
|
| 122 |
+
'status' => 'pending',
|
| 123 |
+
]);
|
| 124 |
+
|
| 125 |
+
// Update product stock
|
| 126 |
+
foreach ($cart as $item) {
|
| 127 |
+
$product = Product::find($item['id']);
|
| 128 |
+
if ($product) {
|
| 129 |
+
$product->Amount = max(0, $product->Amount - $item['quantity']);
|
| 130 |
+
$product->save();
|
| 131 |
+
}
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
// Clear cart
|
| 135 |
+
session()->forget('cart');
|
| 136 |
+
|
| 137 |
+
return redirect()->route('cart.index')->with('success', 'Thank you for your order! We have received your order successfully.');
|
| 138 |
+
|
| 139 |
+
} catch (\Exception $e) {
|
| 140 |
+
return back()->with('error', 'Failed to process order: ' . $e->getMessage());
|
| 141 |
+
}
|
| 142 |
+
}
|
| 143 |
+
public function order()
|
| 144 |
+
{
|
| 145 |
+
$orders = Order::latest()->get(); // ดึงทั้งหมด เรียงล่าสุดก่อน
|
| 146 |
+
return view('order', compact('orders'));
|
| 147 |
+
}
|
| 148 |
+
public function deleteOrder($id)
|
| 149 |
+
{
|
| 150 |
+
try {
|
| 151 |
+
$order = Order::findOrFail($id);
|
| 152 |
+
|
| 153 |
+
// Delete payment slip file if it exists
|
| 154 |
+
if ($order->payment_slip_path && Storage::disk('public')->exists($order->payment_slip_path)) {
|
| 155 |
+
Storage::disk('public')->delete($order->payment_slip_path);
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
$order->delete();
|
| 159 |
+
|
| 160 |
+
return back()->with('success', 'Order deleted successfully');
|
| 161 |
+
} catch (\Exception $e) {
|
| 162 |
+
return back()->with('error', 'Failed to delete order: ' . $e->getMessage());
|
| 163 |
+
}
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
public function show($id)
|
| 167 |
+
{
|
| 168 |
+
try {
|
| 169 |
+
$order = Order::findOrFail($id);
|
| 170 |
+
return view('order-detail', compact('order'));
|
| 171 |
+
} catch (\Exception $e) {
|
| 172 |
+
return redirect()->route('orders.index')->with('error', 'Order not found');
|
| 173 |
+
}
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
public function completeOrder($id)
|
| 177 |
+
{
|
| 178 |
+
try {
|
| 179 |
+
$order = Order::findOrFail($id);
|
| 180 |
+
|
| 181 |
+
// Update order status to completed
|
| 182 |
+
$order->status = 'completed';
|
| 183 |
+
$order->save();
|
| 184 |
+
|
| 185 |
+
return redirect()->route('orders.index')->with('success', 'Order #' . str_pad($order->id, 4, '0', STR_PAD_LEFT) . ' has been ACCEPTED successfully!');
|
| 186 |
+
} catch (\Exception $e) {
|
| 187 |
+
return redirect()->route('orders.index')->with('error', 'Failed to accept order: ' . $e->getMessage());
|
| 188 |
+
}
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
public function rejectOrder($id)
|
| 192 |
+
{
|
| 193 |
+
try {
|
| 194 |
+
$order = Order::findOrFail($id);
|
| 195 |
+
|
| 196 |
+
// Update order status to cancelled (rejected)
|
| 197 |
+
$order->status = 'cancelled';
|
| 198 |
+
$order->save();
|
| 199 |
+
|
| 200 |
+
return redirect()->route('orders.index')->with('success', 'Order #' . str_pad($order->id, 4, '0', STR_PAD_LEFT) . ' has been REJECTED. Customer will see it as cancelled.');
|
| 201 |
+
} catch (\Exception $e) {
|
| 202 |
+
return redirect()->route('orders.index')->with('error', 'Failed to reject order: ' . $e->getMessage());
|
| 203 |
+
}
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
public function customerOrders(Request $request)
|
| 207 |
+
{
|
| 208 |
+
// For now, we'll show all orders since we don't have user authentication for customers
|
| 209 |
+
// In a real app, you'd filter by authenticated customer
|
| 210 |
+
$orders = Order::latest()->get();
|
| 211 |
+
|
| 212 |
+
// If it's an AJAX request for load more functionality
|
| 213 |
+
if ($request->ajax()) {
|
| 214 |
+
$page = $request->get('page', 1);
|
| 215 |
+
$perPage = 3;
|
| 216 |
+
$offset = ($page - 1) * $perPage;
|
| 217 |
+
$status = $request->get('status');
|
| 218 |
+
$game = $request->get('game');
|
| 219 |
+
|
| 220 |
+
// Filter orders based on status
|
| 221 |
+
$filteredOrders = $orders;
|
| 222 |
+
|
| 223 |
+
if ($status && $status !== 'recently') {
|
| 224 |
+
$filteredOrders = $orders->where('status', $status);
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
// Filter by game if specified
|
| 228 |
+
if ($game && $game !== 'all') {
|
| 229 |
+
$filteredOrders = $filteredOrders->filter(function($order) use ($game) {
|
| 230 |
+
$items = json_decode($order->cart_data, true);
|
| 231 |
+
if (is_array($items)) {
|
| 232 |
+
foreach ($items as $item) {
|
| 233 |
+
$product = \App\Models\Product::find($item['id'] ?? null);
|
| 234 |
+
if ($product && $product->game === $game) {
|
| 235 |
+
return true;
|
| 236 |
+
}
|
| 237 |
+
}
|
| 238 |
+
}
|
| 239 |
+
return false;
|
| 240 |
+
});
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
$paginatedOrders = $filteredOrders->slice($offset, $perPage);
|
| 244 |
+
$hasMore = $filteredOrders->count() > ($offset + $perPage);
|
| 245 |
+
|
| 246 |
+
return response()->json([
|
| 247 |
+
'orders' => $paginatedOrders->values(),
|
| 248 |
+
'hasMore' => $hasMore,
|
| 249 |
+
'currentPage' => $page
|
| 250 |
+
]);
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
return view('customer-orders', compact('orders'));
|
| 254 |
+
}
|
| 255 |
+
}
|
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,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
|
| 3 |
+
namespace App\Http\Controllers;
|
| 4 |
+
|
| 5 |
+
use App\Services\AnalyticsService;
|
| 6 |
+
use Illuminate\Http\Request;
|
| 7 |
+
|
| 8 |
+
class DashboardController extends Controller
|
| 9 |
+
{
|
| 10 |
+
protected AnalyticsService $analyticsService;
|
| 11 |
+
|
| 12 |
+
public function __construct(AnalyticsService $analyticsService)
|
| 13 |
+
{
|
| 14 |
+
$this->analyticsService = $analyticsService;
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
public function index()
|
| 18 |
+
{
|
| 19 |
+
try {
|
| 20 |
+
$todayRevenue = $this->analyticsService->getTodayRevenue();
|
| 21 |
+
$profitData = $this->analyticsService->getProfitData('today');
|
| 22 |
+
$customerAnalytics = $this->analyticsService->getCustomerAnalytics();
|
| 23 |
+
$orderStats = $this->analyticsService->getOrderStats();
|
| 24 |
+
} catch (\Exception $e) {
|
| 25 |
+
// Fallback values if service fails
|
| 26 |
+
$todayRevenue = ['current' => 0, 'previous' => 0, 'percentage_change' => 0, 'trend' => 'up'];
|
| 27 |
+
$profitData = ['profit_margin' => 0, 'percentage_change' => 0, 'trend' => 'up'];
|
| 28 |
+
$customerAnalytics = ['total_customers' => 0, 'active_today' => 0];
|
| 29 |
+
$orderStats = ['current' => 0, 'previous' => 0, 'percentage_change' => 0, 'trend' => 'up'];
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
return view('dashboard', compact(
|
| 33 |
+
'todayRevenue',
|
| 34 |
+
'profitData',
|
| 35 |
+
'customerAnalytics',
|
| 36 |
+
'orderStats'
|
| 37 |
+
));
|
| 38 |
+
}
|
| 39 |
+
}
|
app/Http/Controllers/ProductController.php
ADDED
|
@@ -0,0 +1,411 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
|
| 3 |
+
namespace App\Http\Controllers;
|
| 4 |
+
use App\Models\Product;
|
| 5 |
+
use App\Models\CustomGame;
|
| 6 |
+
use Illuminate\Http\Request;
|
| 7 |
+
use Illuminate\Support\Facades\Storage;
|
| 8 |
+
|
| 9 |
+
class ProductController extends Controller
|
| 10 |
+
{
|
| 11 |
+
// Products index method removed
|
| 12 |
+
public function store(Request $request)
|
| 13 |
+
{
|
| 14 |
+
$request->validate([
|
| 15 |
+
'name' => 'required',
|
| 16 |
+
'price' => 'required|numeric',
|
| 17 |
+
'description' => 'required',
|
| 18 |
+
'image' => 'nullable|image|mimes:jpeg,png,jpg,gif,svg|max:2048',
|
| 19 |
+
'Amount' => 'required|numeric',
|
| 20 |
+
'game' => 'nullable|string'
|
| 21 |
+
]);
|
| 22 |
+
|
| 23 |
+
$product = new Product($request->only(['name', 'price', 'description', 'Amount', 'game']));
|
| 24 |
+
|
| 25 |
+
if ($request->hasFile('image')) {
|
| 26 |
+
$imagePath = $request->file('image')->store('products', 'public');
|
| 27 |
+
$product->image = $imagePath;
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
$product->save();
|
| 31 |
+
|
| 32 |
+
return redirect()->back()->with('success', 'Product created successfully');
|
| 33 |
+
}
|
| 34 |
+
public function show($category, $product_name)
|
| 35 |
+
{
|
| 36 |
+
$product = Product::where('game', $category)
|
| 37 |
+
->where('slug', $product_name)
|
| 38 |
+
->firstOrFail();
|
| 39 |
+
return view('show', compact('product'));
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
public function categoryProducts($category)
|
| 43 |
+
{
|
| 44 |
+
$products = Product::where('game', $category)->get();
|
| 45 |
+
|
| 46 |
+
// Get all custom games for navigation
|
| 47 |
+
$customGames = CustomGame::orderBy('name')->get();
|
| 48 |
+
|
| 49 |
+
return view('category_products', compact('products', 'category', 'customGames'));
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
public function edit($id)
|
| 53 |
+
{
|
| 54 |
+
$product = Product::findOrFail($id);
|
| 55 |
+
return response()->json($product);
|
| 56 |
+
}
|
| 57 |
+
public function listOFproduct(Request $request)
|
| 58 |
+
{
|
| 59 |
+
$query = Product::query();
|
| 60 |
+
|
| 61 |
+
// Game filter
|
| 62 |
+
if ($request->filled('game')) {
|
| 63 |
+
$query->where('game', $request->input('game'));
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
// Stock filter
|
| 67 |
+
if ($request->filled('stock')) {
|
| 68 |
+
$stock = $request->input('stock');
|
| 69 |
+
switch ($stock) {
|
| 70 |
+
case 'in_stock':
|
| 71 |
+
$query->where('Amount', '>', 5);
|
| 72 |
+
break;
|
| 73 |
+
case 'low_stock':
|
| 74 |
+
$query->whereBetween('Amount', [1, 5]);
|
| 75 |
+
break;
|
| 76 |
+
case 'out_of_stock':
|
| 77 |
+
$query->where('Amount', 0);
|
| 78 |
+
break;
|
| 79 |
+
}
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
// Search filter
|
| 83 |
+
if ($request->filled('search')) {
|
| 84 |
+
$search = $request->input('search');
|
| 85 |
+
$query->where(function($q) use ($search) {
|
| 86 |
+
$q->where('name', 'like', '%' . $search . '%')
|
| 87 |
+
->orWhere('description', 'like', '%' . $search . '%');
|
| 88 |
+
});
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
$products = $query->orderBy('created_at', 'desc')->get();
|
| 92 |
+
|
| 93 |
+
// Get all custom games for the view
|
| 94 |
+
$customGames = CustomGame::orderBy('name')->get();
|
| 95 |
+
|
| 96 |
+
return view('table_product', compact('products', 'customGames'));
|
| 97 |
+
}
|
| 98 |
+
public function update(Request $request, $id)
|
| 99 |
+
{
|
| 100 |
+
$request->validate([
|
| 101 |
+
'name' => 'required',
|
| 102 |
+
'price' => 'required|numeric',
|
| 103 |
+
'description' => 'required',
|
| 104 |
+
'image' => 'nullable|image|mimes:jpeg,png,jpg,gif,svg|max:2048',
|
| 105 |
+
'Amount' => 'required|numeric',
|
| 106 |
+
'game' => 'nullable|string'
|
| 107 |
+
]);
|
| 108 |
+
|
| 109 |
+
$product = Product::findOrFail($id);
|
| 110 |
+
$product->update($request->only(['name', 'price', 'description', 'Amount', 'game']));
|
| 111 |
+
|
| 112 |
+
if ($request->hasFile('image')) {
|
| 113 |
+
// Delete old image if exists
|
| 114 |
+
if ($product->image && Storage::disk('public')->exists($product->image)) {
|
| 115 |
+
Storage::disk('public')->delete($product->image);
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
$imagePath = $request->file('image')->store('products', 'public');
|
| 119 |
+
$product->image = $imagePath;
|
| 120 |
+
$product->save();
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
return redirect()->back()->with('success', 'Product updated successfully');
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
public function destroy($id)
|
| 127 |
+
{
|
| 128 |
+
try {
|
| 129 |
+
$product = Product::findOrFail($id);
|
| 130 |
+
|
| 131 |
+
// Delete image file if exists
|
| 132 |
+
if ($product->image && Storage::disk('public')->exists($product->image)) {
|
| 133 |
+
Storage::disk('public')->delete($product->image);
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
$product->delete();
|
| 137 |
+
|
| 138 |
+
return back()->with('success', 'Product deleted successfully');
|
| 139 |
+
} catch (\Exception $e) {
|
| 140 |
+
return back()->with('error', 'Failed to delete product: ' . $e->getMessage());
|
| 141 |
+
}
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
public function categories()
|
| 145 |
+
{
|
| 146 |
+
// Get count of products for each category
|
| 147 |
+
$categoryCounts = [
|
| 148 |
+
'Genshin' => Product::where('game', 'Genshin')->count(),
|
| 149 |
+
'Starrail' => Product::where('game', 'Starrail')->count(),
|
| 150 |
+
'WutheringWave' => Product::where('game', 'WutheringWave')->count(),
|
| 151 |
+
];
|
| 152 |
+
|
| 153 |
+
// Get all custom games
|
| 154 |
+
$customGames = CustomGame::orderBy('name')->get();
|
| 155 |
+
|
| 156 |
+
return view('categories', compact('categoryCounts', 'customGames'));
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
public function getByCategory($game)
|
| 160 |
+
{
|
| 161 |
+
$products = Product::where('game', $game)->get();
|
| 162 |
+
|
| 163 |
+
// Add sales count to each product - only count completed orders
|
| 164 |
+
$productsWithSales = $products->map(function ($product) {
|
| 165 |
+
$salesCount = \App\Models\Order::where('cart_data', 'LIKE', '%"product_id":' . $product->id . '%')
|
| 166 |
+
->where('status', 'completed')
|
| 167 |
+
->count();
|
| 168 |
+
$product->sales_count = $salesCount;
|
| 169 |
+
return $product;
|
| 170 |
+
});
|
| 171 |
+
|
| 172 |
+
return response()->json($productsWithSales);
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
public function storeCustomGame(Request $request)
|
| 176 |
+
{
|
| 177 |
+
try {
|
| 178 |
+
$request->validate([
|
| 179 |
+
'name' => 'required|string|max:50|unique:custom_games,name',
|
| 180 |
+
'icon' => 'nullable|string',
|
| 181 |
+
'color_gradient' => 'nullable|string'
|
| 182 |
+
]);
|
| 183 |
+
|
| 184 |
+
$customGame = CustomGame::create([
|
| 185 |
+
'name' => $request->name,
|
| 186 |
+
'icon' => $request->icon ?? 'fas fa-gamepad',
|
| 187 |
+
'color_gradient' => $request->color_gradient ?? 'from-purple-500 to-blue-500'
|
| 188 |
+
]);
|
| 189 |
+
|
| 190 |
+
return response()->json([
|
| 191 |
+
'success' => true,
|
| 192 |
+
'message' => "Custom game '{$request->name}' has been created successfully!",
|
| 193 |
+
'data' => $customGame
|
| 194 |
+
]);
|
| 195 |
+
|
| 196 |
+
} catch (\Exception $e) {
|
| 197 |
+
return response()->json([
|
| 198 |
+
'success' => false,
|
| 199 |
+
'message' => 'Failed to create custom game: ' . $e->getMessage()
|
| 200 |
+
], 500);
|
| 201 |
+
}
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
public function deleteCustomGame($gameName)
|
| 205 |
+
{
|
| 206 |
+
try {
|
| 207 |
+
// Check if there are any products using this custom game
|
| 208 |
+
$productsCount = Product::where('game', $gameName)->count();
|
| 209 |
+
|
| 210 |
+
if ($productsCount > 0) {
|
| 211 |
+
return response()->json([
|
| 212 |
+
'success' => false,
|
| 213 |
+
'message' => "Cannot delete '{$gameName}' because {$productsCount} product(s) are still using this category. Please reassign or delete those products first."
|
| 214 |
+
], 400);
|
| 215 |
+
}
|
| 216 |
+
|
| 217 |
+
// Delete from database
|
| 218 |
+
$customGame = CustomGame::where('name', $gameName)->first();
|
| 219 |
+
if ($customGame) {
|
| 220 |
+
$customGame->delete();
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
return response()->json([
|
| 224 |
+
'success' => true,
|
| 225 |
+
'message' => "Custom game '{$gameName}' has been deleted successfully!"
|
| 226 |
+
]);
|
| 227 |
+
|
| 228 |
+
} catch (\Exception $e) {
|
| 229 |
+
return response()->json([
|
| 230 |
+
'success' => false,
|
| 231 |
+
'message' => 'Failed to delete custom game: ' . $e->getMessage()
|
| 232 |
+
], 500);
|
| 233 |
+
}
|
| 234 |
+
}
|
| 235 |
+
|
| 236 |
+
public function search(Request $request)
|
| 237 |
+
{
|
| 238 |
+
$query = $request->get('q', '');
|
| 239 |
+
|
| 240 |
+
if (strlen($query) < 2) {
|
| 241 |
+
return response()->json(['games' => [], 'products' => []]);
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
// Search games with rich data including descriptions and gradients
|
| 245 |
+
$mainGames = [
|
| 246 |
+
[
|
| 247 |
+
'name' => 'Genshin Impact',
|
| 248 |
+
'slug' => 'Genshin',
|
| 249 |
+
'icon' => 'fas fa-star',
|
| 250 |
+
'count' => Product::where('game', 'Genshin')->count(),
|
| 251 |
+
'description' => 'Discover premium digital content for your favorite games',
|
| 252 |
+
'gradient' => 'from-yellow-500 to-orange-500',
|
| 253 |
+
'logo' => asset('images/games/genshin-logo.png'), // You can add actual logos later
|
| 254 |
+
'category' => 'RPG',
|
| 255 |
+
'popularity' => 'Popular'
|
| 256 |
+
],
|
| 257 |
+
[
|
| 258 |
+
'name' => 'Honkai: Star Rail',
|
| 259 |
+
'slug' => 'Starrail',
|
| 260 |
+
'icon' => 'fas fa-rocket',
|
| 261 |
+
'count' => Product::where('game', 'Starrail')->count(),
|
| 262 |
+
'description' => 'Space fantasy RPG with strategic combat',
|
| 263 |
+
'gradient' => 'from-purple-500 to-pink-500',
|
| 264 |
+
'logo' => asset('images/games/starrail-logo.png'),
|
| 265 |
+
'category' => 'RPG',
|
| 266 |
+
'popularity' => 'Featured'
|
| 267 |
+
],
|
| 268 |
+
[
|
| 269 |
+
'name' => 'Wuthering Waves',
|
| 270 |
+
'slug' => 'WutheringWave',
|
| 271 |
+
'icon' => 'fas fa-wave-square',
|
| 272 |
+
'count' => Product::where('game', 'WutheringWave')->count(),
|
| 273 |
+
'description' => 'Open-world action RPG with stunning visuals',
|
| 274 |
+
'gradient' => 'from-cyan-500 to-blue-500',
|
| 275 |
+
'logo' => asset('images/games/wuthering-logo.png'),
|
| 276 |
+
'category' => 'Action RPG',
|
| 277 |
+
'popularity' => 'New'
|
| 278 |
+
],
|
| 279 |
+
[
|
| 280 |
+
'name' => 'Zenless Zone Zero',
|
| 281 |
+
'slug' => 'ZenlessZoneZero',
|
| 282 |
+
'icon' => 'fas fa-city',
|
| 283 |
+
'count' => Product::where('game', 'ZenlessZoneZero')->count(),
|
| 284 |
+
'description' => 'Urban fantasy action game',
|
| 285 |
+
'gradient' => 'from-red-500 to-pink-500',
|
| 286 |
+
'logo' => asset('images/games/zenless-logo.png'),
|
| 287 |
+
'category' => 'Action',
|
| 288 |
+
'popularity' => 'Trending'
|
| 289 |
+
],
|
| 290 |
+
[
|
| 291 |
+
'name' => 'Arknights',
|
| 292 |
+
'slug' => 'Arknights',
|
| 293 |
+
'icon' => 'fas fa-chess-knight',
|
| 294 |
+
'count' => Product::where('game', 'Arknights')->count(),
|
| 295 |
+
'description' => 'Strategic tower defense with anime characters',
|
| 296 |
+
'gradient' => 'from-indigo-500 to-purple-500',
|
| 297 |
+
'logo' => asset('images/games/arknights-logo.png'),
|
| 298 |
+
'category' => 'Strategy',
|
| 299 |
+
'popularity' => 'Classic'
|
| 300 |
+
],
|
| 301 |
+
[
|
| 302 |
+
'name' => 'Azur Lane',
|
| 303 |
+
'slug' => 'AzurLane',
|
| 304 |
+
'icon' => 'fas fa-ship',
|
| 305 |
+
'count' => Product::where('game', 'AzurLane')->count(),
|
| 306 |
+
'description' => 'Naval warfare with anthropomorphic ships',
|
| 307 |
+
'gradient' => 'from-blue-500 to-teal-500',
|
| 308 |
+
'logo' => asset('images/games/azurlane-logo.png'),
|
| 309 |
+
'category' => 'Strategy',
|
| 310 |
+
'popularity' => 'Popular'
|
| 311 |
+
],
|
| 312 |
+
];
|
| 313 |
+
|
| 314 |
+
// Add custom games with their stored styling
|
| 315 |
+
$customGames = CustomGame::all()->map(function($game) {
|
| 316 |
+
return [
|
| 317 |
+
'name' => $game->name,
|
| 318 |
+
'slug' => $game->name,
|
| 319 |
+
'icon' => $game->icon,
|
| 320 |
+
'count' => Product::where('game', $game->name)->count(),
|
| 321 |
+
'description' => 'Custom game category with unique items',
|
| 322 |
+
'gradient' => $game->color_gradient,
|
| 323 |
+
'logo' => asset('images/games/custom-logo.png'),
|
| 324 |
+
'category' => 'Custom',
|
| 325 |
+
'popularity' => 'Special'
|
| 326 |
+
];
|
| 327 |
+
});
|
| 328 |
+
|
| 329 |
+
$allGames = collect($mainGames)->concat($customGames);
|
| 330 |
+
|
| 331 |
+
// Filter games by query
|
| 332 |
+
$matchingGames = $allGames->filter(function($game) use ($query) {
|
| 333 |
+
return stripos($game['name'], $query) !== false;
|
| 334 |
+
})->take(5)->values();
|
| 335 |
+
|
| 336 |
+
// Search products with enhanced data including real sales count
|
| 337 |
+
$products = Product::where('name', 'like', "%{$query}%")
|
| 338 |
+
->orWhere('description', 'like', "%{$query}%")
|
| 339 |
+
->orWhere('game', 'like', "%{$query}%")
|
| 340 |
+
->take(8)
|
| 341 |
+
->get()
|
| 342 |
+
->map(function($product) {
|
| 343 |
+
// Calculate real sales count from completed orders
|
| 344 |
+
$salesCount = \App\Models\Order::where('status', 'completed')
|
| 345 |
+
->where('cart_data', 'LIKE', '%"id":' . $product->id . '%')
|
| 346 |
+
->get()
|
| 347 |
+
->sum(function($order) use ($product) {
|
| 348 |
+
$cartData = json_decode($order->cart_data, true);
|
| 349 |
+
$quantity = 0;
|
| 350 |
+
if (is_array($cartData)) {
|
| 351 |
+
foreach ($cartData as $item) {
|
| 352 |
+
if (isset($item['id']) && $item['id'] == $product->id) {
|
| 353 |
+
$quantity += $item['quantity'] ?? 1;
|
| 354 |
+
}
|
| 355 |
+
}
|
| 356 |
+
}
|
| 357 |
+
return $quantity;
|
| 358 |
+
});
|
| 359 |
+
|
| 360 |
+
// Calculate average rating from orders (mock for now, can be enhanced later)
|
| 361 |
+
$rating = $salesCount > 0 ? min(5.0, 3.5 + ($salesCount / 100)) : 4.0;
|
| 362 |
+
|
| 363 |
+
return [
|
| 364 |
+
'id' => $product->id,
|
| 365 |
+
'name' => $product->name,
|
| 366 |
+
'slug' => $product->slug ?? strtolower(str_replace(' ', '-', $product->name)),
|
| 367 |
+
'game' => $product->game,
|
| 368 |
+
'game_slug' => $product->game,
|
| 369 |
+
'price' => number_format($product->price, 2),
|
| 370 |
+
'original_price' => $product->price,
|
| 371 |
+
'image' => $product->image ? asset($product->image) : 'https://via.placeholder.com/150x150/374151/ffffff?text=No+Image',
|
| 372 |
+
'description' => $product->description,
|
| 373 |
+
'short_description' => strlen($product->description) > 100 ? substr($product->description, 0, 100) . '...' : $product->description,
|
| 374 |
+
'amount' => $product->Amount,
|
| 375 |
+
'stock_status' => $product->Amount > 10 ? 'In Stock (' . $product->Amount . ')' :
|
| 376 |
+
($product->Amount > 0 ? 'Low Stock (' . $product->Amount . ')' : 'Out of Stock'),
|
| 377 |
+
'stock_color' => $product->Amount > 10 ? 'text-green-400' :
|
| 378 |
+
($product->Amount > 0 ? 'text-yellow-400' : 'text-red-400'),
|
| 379 |
+
'stock_bg' => $product->Amount > 10 ? 'bg-green-500/20' :
|
| 380 |
+
($product->Amount > 0 ? 'bg-yellow-500/20' : 'bg-red-500/20'),
|
| 381 |
+
'created_at' => $product->created_at->format('M d, Y'),
|
| 382 |
+
'is_new' => $product->created_at->diffInDays(now()) <= 7,
|
| 383 |
+
'rating' => round($rating, 1),
|
| 384 |
+
'sales_count' => $salesCount,
|
| 385 |
+
'total_revenue' => $salesCount * $product->price,
|
| 386 |
+
'availability' => $product->Amount > 0 ? 'Available' : 'Sold Out',
|
| 387 |
+
'category_icon' => $this->getGameIcon($product->game),
|
| 388 |
+
'popularity_score' => $salesCount > 50 ? 'Hot' : ($salesCount > 20 ? 'Popular' : 'New')
|
| 389 |
+
];
|
| 390 |
+
});
|
| 391 |
+
|
| 392 |
+
return response()->json([
|
| 393 |
+
'games' => $matchingGames,
|
| 394 |
+
'products' => $products
|
| 395 |
+
]);
|
| 396 |
+
}
|
| 397 |
+
|
| 398 |
+
private function getGameIcon($game)
|
| 399 |
+
{
|
| 400 |
+
$icons = [
|
| 401 |
+
'Genshin' => 'fas fa-star',
|
| 402 |
+
'Starrail' => 'fas fa-rocket',
|
| 403 |
+
'WutheringWave' => 'fas fa-wave-square',
|
| 404 |
+
'ZenlessZoneZero' => 'fas fa-city',
|
| 405 |
+
'Arknights' => 'fas fa-chess-knight',
|
| 406 |
+
'AzurLane' => 'fas fa-ship',
|
| 407 |
+
];
|
| 408 |
+
|
| 409 |
+
return $icons[$game] ?? 'fas fa-gamepad';
|
| 410 |
+
}
|
| 411 |
+
}
|
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/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/AnalyticsSnapshot.php
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
|
| 3 |
+
namespace App\Models;
|
| 4 |
+
|
| 5 |
+
use Illuminate\Database\Eloquent\Model;
|
| 6 |
+
use Carbon\Carbon;
|
| 7 |
+
|
| 8 |
+
class AnalyticsSnapshot extends Model
|
| 9 |
+
{
|
| 10 |
+
protected $fillable = [
|
| 11 |
+
'date',
|
| 12 |
+
'hour',
|
| 13 |
+
'revenue',
|
| 14 |
+
'profit',
|
| 15 |
+
'orders_count',
|
| 16 |
+
'customers_count',
|
| 17 |
+
'avg_order_value',
|
| 18 |
+
'conversion_rate'
|
| 19 |
+
];
|
| 20 |
+
|
| 21 |
+
protected $casts = [
|
| 22 |
+
'date' => 'date',
|
| 23 |
+
'revenue' => 'decimal:2',
|
| 24 |
+
'profit' => 'decimal:2',
|
| 25 |
+
'avg_order_value' => 'decimal:2',
|
| 26 |
+
'conversion_rate' => 'decimal:4'
|
| 27 |
+
];
|
| 28 |
+
|
| 29 |
+
// Scopes for different time periods
|
| 30 |
+
public function scopeDaily($query, Carbon $date)
|
| 31 |
+
{
|
| 32 |
+
return $query->where('date', $date->format('Y-m-d'))
|
| 33 |
+
->whereNotNull('hour')
|
| 34 |
+
->orderBy('hour');
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
public function scopeWeekly($query, Carbon $startDate)
|
| 38 |
+
{
|
| 39 |
+
return $query->whereBetween('date', [
|
| 40 |
+
$startDate->format('Y-m-d'),
|
| 41 |
+
$startDate->copy()->addDays(6)->format('Y-m-d')
|
| 42 |
+
])
|
| 43 |
+
->whereNull('hour')
|
| 44 |
+
->orderBy('date');
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
public function scopeMonthly($query, Carbon $date)
|
| 48 |
+
{
|
| 49 |
+
return $query->whereYear('date', $date->year)
|
| 50 |
+
->whereMonth('date', $date->month)
|
| 51 |
+
->whereNull('hour')
|
| 52 |
+
->orderBy('date');
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
// Helper method to get profit margin percentage
|
| 56 |
+
public function getProfitMarginAttribute()
|
| 57 |
+
{
|
| 58 |
+
if ($this->revenue > 0) {
|
| 59 |
+
return ($this->profit / $this->revenue) * 100;
|
| 60 |
+
}
|
| 61 |
+
return 0;
|
| 62 |
+
}
|
| 63 |
+
}
|
app/Models/CustomGame.php
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
|
| 3 |
+
namespace App\Models;
|
| 4 |
+
|
| 5 |
+
use Illuminate\Database\Eloquent\Model;
|
| 6 |
+
|
| 7 |
+
class CustomGame extends Model
|
| 8 |
+
{
|
| 9 |
+
protected $fillable = [
|
| 10 |
+
'name',
|
| 11 |
+
'icon',
|
| 12 |
+
'color_gradient'
|
| 13 |
+
];
|
| 14 |
+
}
|
app/Models/Order.php
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
|
| 3 |
+
namespace App\Models;
|
| 4 |
+
|
| 5 |
+
use Illuminate\Database\Eloquent\Model;
|
| 6 |
+
|
| 7 |
+
class Order extends Model
|
| 8 |
+
{
|
| 9 |
+
protected $fillable = [
|
| 10 |
+
'customer_name',
|
| 11 |
+
'phone',
|
| 12 |
+
'payment_slip_path',
|
| 13 |
+
'cart_data',
|
| 14 |
+
'total_amount',
|
| 15 |
+
'status',
|
| 16 |
+
];
|
| 17 |
+
}
|
app/Models/Product.php
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
|
| 3 |
+
namespace App\Models;
|
| 4 |
+
|
| 5 |
+
use Illuminate\Database\Eloquent\Model;
|
| 6 |
+
use Illuminate\Support\Str;
|
| 7 |
+
|
| 8 |
+
class Product extends Model
|
| 9 |
+
{
|
| 10 |
+
protected $fillable = ['name', 'price', 'description', 'image', 'Amount', 'game', 'slug'];
|
| 11 |
+
|
| 12 |
+
protected static function boot()
|
| 13 |
+
{
|
| 14 |
+
parent::boot();
|
| 15 |
+
|
| 16 |
+
static::creating(function ($product) {
|
| 17 |
+
if (empty($product->slug)) {
|
| 18 |
+
$product->slug = Str::slug($product->name);
|
| 19 |
+
}
|
| 20 |
+
});
|
| 21 |
+
|
| 22 |
+
static::updating(function ($product) {
|
| 23 |
+
if ($product->isDirty('name') && empty($product->slug)) {
|
| 24 |
+
$product->slug = Str::slug($product->name);
|
| 25 |
+
}
|
| 26 |
+
});
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
public function getImageAttribute($value)
|
| 30 |
+
{
|
| 31 |
+
if (!$value) {
|
| 32 |
+
return null;
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
// If it's already a full URL, return as is
|
| 36 |
+
if (str_starts_with($value, 'http')) {
|
| 37 |
+
return $value;
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
// If it starts with 'storage/', it's already formatted
|
| 41 |
+
if (str_starts_with($value, 'storage/')) {
|
| 42 |
+
return asset($value);
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
// Otherwise, prepend 'storage/'
|
| 46 |
+
return asset('storage/' . $value);
|
| 47 |
+
}
|
| 48 |
+
}
|
app/Models/RevenueTracking.php
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
|
| 3 |
+
namespace App\Models;
|
| 4 |
+
|
| 5 |
+
use Illuminate\Database\Eloquent\Model;
|
| 6 |
+
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
| 7 |
+
|
| 8 |
+
class RevenueTracking extends Model
|
| 9 |
+
{
|
| 10 |
+
protected $table = 'revenue_tracking';
|
| 11 |
+
|
| 12 |
+
protected $fillable = [
|
| 13 |
+
'order_id',
|
| 14 |
+
'amount',
|
| 15 |
+
'cost',
|
| 16 |
+
'profit_margin',
|
| 17 |
+
'product_name',
|
| 18 |
+
'quantity'
|
| 19 |
+
];
|
| 20 |
+
|
| 21 |
+
protected $casts = [
|
| 22 |
+
'amount' => 'decimal:2',
|
| 23 |
+
'cost' => 'decimal:2',
|
| 24 |
+
'profit_margin' => 'decimal:4'
|
| 25 |
+
];
|
| 26 |
+
|
| 27 |
+
// Relationships
|
| 28 |
+
public function order(): BelongsTo
|
| 29 |
+
{
|
| 30 |
+
return $this->belongsTo(Order::class);
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
// Calculate profit amount
|
| 34 |
+
public function getProfitAttribute()
|
| 35 |
+
{
|
| 36 |
+
return $this->amount - $this->cost;
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
// Calculate profit margin percentage
|
| 40 |
+
public function getProfitMarginPercentageAttribute()
|
| 41 |
+
{
|
| 42 |
+
if ($this->amount > 0) {
|
| 43 |
+
return (($this->amount - $this->cost) / $this->amount) * 100;
|
| 44 |
+
}
|
| 45 |
+
return 0;
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
// Scope for today's revenue
|
| 49 |
+
public function scopeToday($query)
|
| 50 |
+
{
|
| 51 |
+
return $query->whereDate('created_at', today());
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
// Scope for date range
|
| 55 |
+
public function scopeDateRange($query, $startDate, $endDate)
|
| 56 |
+
{
|
| 57 |
+
return $query->whereBetween('created_at', [$startDate, $endDate]);
|
| 58 |
+
}
|
| 59 |
+
}
|
app/Models/User.php
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
|
| 3 |
+
namespace App\Models;
|
| 4 |
+
|
| 5 |
+
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
| 6 |
+
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
| 7 |
+
use Illuminate\Foundation\Auth\User as Authenticatable;
|
| 8 |
+
use Illuminate\Notifications\Notifiable;
|
| 9 |
+
|
| 10 |
+
class User extends Authenticatable
|
| 11 |
+
{
|
| 12 |
+
/** @use HasFactory<\Database\Factories\UserFactory> */
|
| 13 |
+
use HasFactory, Notifiable;
|
| 14 |
+
|
| 15 |
+
/**
|
| 16 |
+
* The attributes that are mass assignable.
|
| 17 |
+
*
|
| 18 |
+
* @var list<string>
|
| 19 |
+
*/
|
| 20 |
+
protected $fillable = [
|
| 21 |
+
'name',
|
| 22 |
+
'email',
|
| 23 |
+
'password',
|
| 24 |
+
];
|
| 25 |
+
|
| 26 |
+
/**
|
| 27 |
+
* The attributes that should be hidden for serialization.
|
| 28 |
+
*
|
| 29 |
+
* @var list<string>
|
| 30 |
+
*/
|
| 31 |
+
protected $hidden = [
|
| 32 |
+
'password',
|
| 33 |
+
'remember_token',
|
| 34 |
+
];
|
| 35 |
+
|
| 36 |
+
/**
|
| 37 |
+
* Get the attributes that should be cast.
|
| 38 |
+
*
|
| 39 |
+
* @return array<string, string>
|
| 40 |
+
*/
|
| 41 |
+
protected function casts(): array
|
| 42 |
+
{
|
| 43 |
+
return [
|
| 44 |
+
'email_verified_at' => 'datetime',
|
| 45 |
+
'password' => 'hashed',
|
| 46 |
+
];
|
| 47 |
+
}
|
| 48 |
+
}
|
app/Providers/AppServiceProvider.php
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
|
| 3 |
+
namespace App\Providers;
|
| 4 |
+
|
| 5 |
+
use Illuminate\Support\ServiceProvider;
|
| 6 |
+
use Illuminate\Support\Facades\URL;
|
| 7 |
+
|
| 8 |
+
class AppServiceProvider extends ServiceProvider
|
| 9 |
+
{
|
| 10 |
+
/**
|
| 11 |
+
* Register any application services.
|
| 12 |
+
*/
|
| 13 |
+
public function register(): void
|
| 14 |
+
{
|
| 15 |
+
//
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
/**
|
| 19 |
+
* Bootstrap any application services.
|
| 20 |
+
*/
|
| 21 |
+
public function boot(): void
|
| 22 |
+
{
|
| 23 |
+
// Force HTTPS when using ngrok or in production
|
| 24 |
+
if (config('app.env') !== 'local' || request()->header('x-forwarded-proto') === 'https') {
|
| 25 |
+
URL::forceScheme('https');
|
| 26 |
+
}
|
| 27 |
+
}
|
| 28 |
+
}
|
app/Services/AnalyticsService.php
ADDED
|
@@ -0,0 +1,379 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
|
| 3 |
+
namespace App\Services;
|
| 4 |
+
|
| 5 |
+
use App\Models\AnalyticsSnapshot;
|
| 6 |
+
use App\Models\RevenueTracking;
|
| 7 |
+
use App\Models\Order;
|
| 8 |
+
use App\Models\User;
|
| 9 |
+
use App\Models\Product;
|
| 10 |
+
use Carbon\Carbon;
|
| 11 |
+
use Illuminate\Support\Facades\DB;
|
| 12 |
+
|
| 13 |
+
class AnalyticsService
|
| 14 |
+
{
|
| 15 |
+
/**
|
| 16 |
+
* Calculate real-time revenue for today using real Order data
|
| 17 |
+
*/
|
| 18 |
+
public function getTodayRevenue(): array
|
| 19 |
+
{
|
| 20 |
+
$today = Carbon::today();
|
| 21 |
+
$yesterday = Carbon::yesterday();
|
| 22 |
+
|
| 23 |
+
// Use real Order data
|
| 24 |
+
$todayRevenue = Order::whereDate('created_at', $today)
|
| 25 |
+
->where('status', '!=', 'cancelled')
|
| 26 |
+
->sum('total_amount');
|
| 27 |
+
|
| 28 |
+
$yesterdayRevenue = Order::whereDate('created_at', $yesterday)
|
| 29 |
+
->where('status', '!=', 'cancelled')
|
| 30 |
+
->sum('total_amount');
|
| 31 |
+
|
| 32 |
+
$percentageChange = $yesterdayRevenue > 0
|
| 33 |
+
? (($todayRevenue - $yesterdayRevenue) / $yesterdayRevenue) * 100
|
| 34 |
+
: ($todayRevenue > 0 ? 100 : 0);
|
| 35 |
+
|
| 36 |
+
return [
|
| 37 |
+
'current' => (float) $todayRevenue,
|
| 38 |
+
'previous' => (float) $yesterdayRevenue,
|
| 39 |
+
'percentage_change' => round($percentageChange, 2),
|
| 40 |
+
'trend' => $percentageChange >= 0 ? 'up' : 'down'
|
| 41 |
+
];
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
/**
|
| 45 |
+
* Calculate profit data with margins using real Order data
|
| 46 |
+
*/
|
| 47 |
+
public function getProfitData(string $period = 'today'): array
|
| 48 |
+
{
|
| 49 |
+
$query = Order::where('status', '!=', 'cancelled');
|
| 50 |
+
$previousQuery = Order::where('status', '!=', 'cancelled');
|
| 51 |
+
|
| 52 |
+
switch ($period) {
|
| 53 |
+
case 'today':
|
| 54 |
+
$query->whereDate('created_at', Carbon::today());
|
| 55 |
+
$previousQuery->whereDate('created_at', Carbon::yesterday());
|
| 56 |
+
break;
|
| 57 |
+
case 'week':
|
| 58 |
+
$query->whereBetween('created_at', [Carbon::now()->startOfWeek(), Carbon::now()->endOfWeek()]);
|
| 59 |
+
$previousQuery->whereBetween('created_at', [
|
| 60 |
+
Carbon::now()->subWeek()->startOfWeek(),
|
| 61 |
+
Carbon::now()->subWeek()->endOfWeek()
|
| 62 |
+
]);
|
| 63 |
+
break;
|
| 64 |
+
case 'month':
|
| 65 |
+
$query->whereMonth('created_at', Carbon::now()->month);
|
| 66 |
+
$previousQuery->whereMonth('created_at', Carbon::now()->subMonth()->month);
|
| 67 |
+
break;
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
$revenue = $query->sum('total_amount');
|
| 71 |
+
$previousRevenue = $previousQuery->sum('total_amount');
|
| 72 |
+
|
| 73 |
+
// Estimate profit as 30% of revenue (you can adjust this based on your business model)
|
| 74 |
+
$estimatedProfitMargin = 0.30;
|
| 75 |
+
$profit = $revenue * $estimatedProfitMargin;
|
| 76 |
+
$previousProfit = $previousRevenue * $estimatedProfitMargin;
|
| 77 |
+
$cost = $revenue - $profit;
|
| 78 |
+
|
| 79 |
+
$profitMarginPercentage = $revenue > 0 ? ($profit / $revenue) * 100 : 0;
|
| 80 |
+
$percentageChange = $previousProfit > 0
|
| 81 |
+
? (($profit - $previousProfit) / $previousProfit) * 100
|
| 82 |
+
: ($profit > 0 ? 100 : 0);
|
| 83 |
+
|
| 84 |
+
return [
|
| 85 |
+
'profit' => (float) $profit,
|
| 86 |
+
'revenue' => (float) $revenue,
|
| 87 |
+
'cost' => (float) $cost,
|
| 88 |
+
'profit_margin' => round($profitMarginPercentage, 2),
|
| 89 |
+
'percentage_change' => round($percentageChange, 2),
|
| 90 |
+
'trend' => $percentageChange >= 0 ? 'up' : 'down'
|
| 91 |
+
];
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
/**
|
| 95 |
+
* Get aggregated data for different time periods
|
| 96 |
+
*/
|
| 97 |
+
public function getAggregatedData(string $period, Carbon $date = null): array
|
| 98 |
+
{
|
| 99 |
+
$date = $date ?? Carbon::now();
|
| 100 |
+
|
| 101 |
+
switch ($period) {
|
| 102 |
+
case 'daily':
|
| 103 |
+
return $this->getDailyHourlyData($date);
|
| 104 |
+
case 'weekly':
|
| 105 |
+
return $this->getWeeklyDailyData($date);
|
| 106 |
+
case 'monthly':
|
| 107 |
+
return $this->getMonthlyDailyData($date);
|
| 108 |
+
default:
|
| 109 |
+
return [];
|
| 110 |
+
}
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
/**
|
| 114 |
+
* Get hourly data for a specific day using real Order data
|
| 115 |
+
*/
|
| 116 |
+
private function getDailyHourlyData(Carbon $date): array
|
| 117 |
+
{
|
| 118 |
+
$hourlyData = [];
|
| 119 |
+
|
| 120 |
+
for ($hour = 0; $hour < 24; $hour++) {
|
| 121 |
+
$startTime = $date->copy()->hour($hour)->minute(0)->second(0);
|
| 122 |
+
$endTime = $startTime->copy()->addHour();
|
| 123 |
+
|
| 124 |
+
$orders = Order::where('status', '!=', 'cancelled')
|
| 125 |
+
->whereBetween('created_at', [$startTime, $endTime])
|
| 126 |
+
->get();
|
| 127 |
+
|
| 128 |
+
$revenue = $orders->sum('total_amount');
|
| 129 |
+
$profit = $revenue * 0.30; // 30% profit margin estimate
|
| 130 |
+
$ordersCount = $orders->count();
|
| 131 |
+
$customersCount = $orders->pluck('customer_name')->unique()->count();
|
| 132 |
+
|
| 133 |
+
$hourlyData[] = [
|
| 134 |
+
'hour' => $hour,
|
| 135 |
+
'revenue' => (float) $revenue,
|
| 136 |
+
'profit' => (float) $profit,
|
| 137 |
+
'orders' => $ordersCount,
|
| 138 |
+
'customers' => $customersCount,
|
| 139 |
+
];
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
return $hourlyData;
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
/**
|
| 146 |
+
* Get daily data for a week using real Order data
|
| 147 |
+
*/
|
| 148 |
+
private function getWeeklyDailyData(Carbon $date): array
|
| 149 |
+
{
|
| 150 |
+
$startOfWeek = $date->copy()->startOfWeek();
|
| 151 |
+
$dailyData = [];
|
| 152 |
+
|
| 153 |
+
for ($day = 0; $day < 7; $day++) {
|
| 154 |
+
$currentDate = $startOfWeek->copy()->addDays($day);
|
| 155 |
+
|
| 156 |
+
$orders = Order::where('status', '!=', 'cancelled')
|
| 157 |
+
->whereDate('created_at', $currentDate)
|
| 158 |
+
->get();
|
| 159 |
+
|
| 160 |
+
$revenue = $orders->sum('total_amount');
|
| 161 |
+
$profit = $revenue * 0.30;
|
| 162 |
+
$ordersCount = $orders->count();
|
| 163 |
+
$customersCount = $orders->pluck('customer_name')->unique()->count();
|
| 164 |
+
|
| 165 |
+
$dailyData[] = [
|
| 166 |
+
'date' => $currentDate->format('Y-m-d'),
|
| 167 |
+
'day_name' => $currentDate->format('l'),
|
| 168 |
+
'revenue' => (float) $revenue,
|
| 169 |
+
'profit' => (float) $profit,
|
| 170 |
+
'orders' => $ordersCount,
|
| 171 |
+
'customers' => $customersCount,
|
| 172 |
+
];
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
return $dailyData;
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
/**
|
| 179 |
+
* Get daily data for a month using real Order data
|
| 180 |
+
*/
|
| 181 |
+
private function getMonthlyDailyData(Carbon $date): array
|
| 182 |
+
{
|
| 183 |
+
$daysInMonth = $date->daysInMonth;
|
| 184 |
+
$dailyData = [];
|
| 185 |
+
|
| 186 |
+
for ($day = 1; $day <= $daysInMonth; $day++) {
|
| 187 |
+
$currentDate = $date->copy()->day($day);
|
| 188 |
+
|
| 189 |
+
$orders = Order::where('status', '!=', 'cancelled')
|
| 190 |
+
->whereDate('created_at', $currentDate)
|
| 191 |
+
->get();
|
| 192 |
+
|
| 193 |
+
$revenue = $orders->sum('total_amount');
|
| 194 |
+
$profit = $revenue * 0.30;
|
| 195 |
+
$ordersCount = $orders->count();
|
| 196 |
+
$customersCount = $orders->pluck('customer_name')->unique()->count();
|
| 197 |
+
|
| 198 |
+
$dailyData[] = [
|
| 199 |
+
'date' => $currentDate->format('Y-m-d'),
|
| 200 |
+
'day' => $day,
|
| 201 |
+
'revenue' => (float) $revenue,
|
| 202 |
+
'profit' => (float) $profit,
|
| 203 |
+
'orders' => $ordersCount,
|
| 204 |
+
'customers' => $customersCount,
|
| 205 |
+
];
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
return $dailyData;
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
/**
|
| 212 |
+
* Update analytics snapshots (called by scheduled job)
|
| 213 |
+
*/
|
| 214 |
+
public function updateAnalyticsSnapshots(): void
|
| 215 |
+
{
|
| 216 |
+
$this->updateHourlySnapshots();
|
| 217 |
+
$this->updateDailySnapshots();
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
/**
|
| 221 |
+
* Update hourly snapshots for today
|
| 222 |
+
*/
|
| 223 |
+
private function updateHourlySnapshots(): void
|
| 224 |
+
{
|
| 225 |
+
$today = Carbon::today();
|
| 226 |
+
$currentHour = Carbon::now()->hour;
|
| 227 |
+
|
| 228 |
+
for ($hour = 0; $hour <= $currentHour; $hour++) {
|
| 229 |
+
$startTime = $today->copy()->hour($hour);
|
| 230 |
+
$endTime = $startTime->copy()->addHour();
|
| 231 |
+
|
| 232 |
+
$revenue = RevenueTracking::whereBetween('created_at', [$startTime, $endTime])->sum('amount');
|
| 233 |
+
$cost = RevenueTracking::whereBetween('created_at', [$startTime, $endTime])->sum('cost');
|
| 234 |
+
$profit = $revenue - $cost;
|
| 235 |
+
$ordersCount = Order::whereBetween('created_at', [$startTime, $endTime])->count();
|
| 236 |
+
$customersCount = Order::whereBetween('created_at', [$startTime, $endTime])
|
| 237 |
+
->distinct('user_id')->count('user_id');
|
| 238 |
+
|
| 239 |
+
AnalyticsSnapshot::updateOrCreate(
|
| 240 |
+
['date' => $today->format('Y-m-d'), 'hour' => $hour],
|
| 241 |
+
[
|
| 242 |
+
'revenue' => $revenue,
|
| 243 |
+
'profit' => $profit,
|
| 244 |
+
'orders_count' => $ordersCount,
|
| 245 |
+
'customers_count' => $customersCount,
|
| 246 |
+
'avg_order_value' => $ordersCount > 0 ? $revenue / $ordersCount : 0,
|
| 247 |
+
'conversion_rate' => 0 // Will be calculated separately
|
| 248 |
+
]
|
| 249 |
+
);
|
| 250 |
+
}
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
/**
|
| 254 |
+
* Update daily snapshots
|
| 255 |
+
*/
|
| 256 |
+
private function updateDailySnapshots(): void
|
| 257 |
+
{
|
| 258 |
+
$yesterday = Carbon::yesterday();
|
| 259 |
+
|
| 260 |
+
$revenue = RevenueTracking::whereDate('created_at', $yesterday)->sum('amount');
|
| 261 |
+
$cost = RevenueTracking::whereDate('created_at', $yesterday)->sum('cost');
|
| 262 |
+
$profit = $revenue - $cost;
|
| 263 |
+
$ordersCount = Order::whereDate('created_at', $yesterday)->count();
|
| 264 |
+
$customersCount = Order::whereDate('created_at', $yesterday)
|
| 265 |
+
->distinct('user_id')->count('user_id');
|
| 266 |
+
|
| 267 |
+
AnalyticsSnapshot::updateOrCreate(
|
| 268 |
+
['date' => $yesterday->format('Y-m-d'), 'hour' => null],
|
| 269 |
+
[
|
| 270 |
+
'revenue' => $revenue,
|
| 271 |
+
'profit' => $profit,
|
| 272 |
+
'orders_count' => $ordersCount,
|
| 273 |
+
'customers_count' => $customersCount,
|
| 274 |
+
'avg_order_value' => $ordersCount > 0 ? $revenue / $ordersCount : 0,
|
| 275 |
+
'conversion_rate' => 0 // Will be calculated separately
|
| 276 |
+
]
|
| 277 |
+
);
|
| 278 |
+
}
|
| 279 |
+
|
| 280 |
+
/**
|
| 281 |
+
* Get top selling products using real Product and Order data
|
| 282 |
+
*/
|
| 283 |
+
public function getTopProducts(int $limit = 10): array
|
| 284 |
+
{
|
| 285 |
+
// Get all products with their sales data
|
| 286 |
+
$products = Product::select('id', 'name', 'price', 'Amount')
|
| 287 |
+
->get()
|
| 288 |
+
->map(function ($product) {
|
| 289 |
+
// Calculate estimated sales based on stock reduction
|
| 290 |
+
// This is a simple estimation - you might want to implement proper sales tracking
|
| 291 |
+
$estimatedSold = max(0, 100 - $product->Amount); // Assuming initial stock was 100
|
| 292 |
+
$totalRevenue = $estimatedSold * $product->price;
|
| 293 |
+
|
| 294 |
+
return [
|
| 295 |
+
'product_name' => $product->name,
|
| 296 |
+
'total_quantity' => $estimatedSold,
|
| 297 |
+
'total_revenue' => (float) $totalRevenue,
|
| 298 |
+
'current_stock' => $product->Amount,
|
| 299 |
+
'price' => (float) $product->price
|
| 300 |
+
];
|
| 301 |
+
})
|
| 302 |
+
->sortByDesc('total_revenue')
|
| 303 |
+
->take($limit)
|
| 304 |
+
->values()
|
| 305 |
+
->toArray();
|
| 306 |
+
|
| 307 |
+
return $products;
|
| 308 |
+
}
|
| 309 |
+
|
| 310 |
+
/**
|
| 311 |
+
* Get customer analytics using real User and Order data
|
| 312 |
+
*/
|
| 313 |
+
public function getCustomerAnalytics(): array
|
| 314 |
+
{
|
| 315 |
+
$totalCustomers = User::count();
|
| 316 |
+
$newCustomersToday = User::whereDate('created_at', Carbon::today())->count();
|
| 317 |
+
|
| 318 |
+
// Count unique customers who made orders today
|
| 319 |
+
$customersWithOrdersToday = Order::whereDate('created_at', Carbon::today())
|
| 320 |
+
->distinct('customer_name')
|
| 321 |
+
->count('customer_name');
|
| 322 |
+
|
| 323 |
+
// Total orders today
|
| 324 |
+
$ordersToday = Order::whereDate('created_at', Carbon::today())->count();
|
| 325 |
+
|
| 326 |
+
return [
|
| 327 |
+
'total_customers' => $totalCustomers,
|
| 328 |
+
'new_today' => $newCustomersToday,
|
| 329 |
+
'active_today' => $customersWithOrdersToday,
|
| 330 |
+
'orders_today' => $ordersToday,
|
| 331 |
+
'new_percentage' => $totalCustomers > 0 ? ($newCustomersToday / $totalCustomers) * 100 : 0,
|
| 332 |
+
'conversion_rate' => $customersWithOrdersToday > 0 ? ($ordersToday / $customersWithOrdersToday) * 100 : 0
|
| 333 |
+
];
|
| 334 |
+
}
|
| 335 |
+
|
| 336 |
+
/**
|
| 337 |
+
* Get real-time order statistics
|
| 338 |
+
*/
|
| 339 |
+
public function getOrderStats(): array
|
| 340 |
+
{
|
| 341 |
+
$today = Carbon::today();
|
| 342 |
+
$yesterday = Carbon::yesterday();
|
| 343 |
+
|
| 344 |
+
$todayOrders = Order::whereDate('created_at', $today)->count();
|
| 345 |
+
$yesterdayOrders = Order::whereDate('created_at', $yesterday)->count();
|
| 346 |
+
|
| 347 |
+
$percentageChange = $yesterdayOrders > 0
|
| 348 |
+
? (($todayOrders - $yesterdayOrders) / $yesterdayOrders) * 100
|
| 349 |
+
: ($todayOrders > 0 ? 100 : 0);
|
| 350 |
+
|
| 351 |
+
return [
|
| 352 |
+
'current' => $todayOrders,
|
| 353 |
+
'previous' => $yesterdayOrders,
|
| 354 |
+
'percentage_change' => round($percentageChange, 2),
|
| 355 |
+
'trend' => $percentageChange >= 0 ? 'up' : 'down',
|
| 356 |
+
'total_orders' => Order::count(),
|
| 357 |
+
'pending_orders' => Order::where('status', 'pending')->count(),
|
| 358 |
+
'completed_orders' => Order::where('status', 'completed')->count()
|
| 359 |
+
];
|
| 360 |
+
}
|
| 361 |
+
|
| 362 |
+
/**
|
| 363 |
+
* Get real-time product statistics
|
| 364 |
+
*/
|
| 365 |
+
public function getProductStats(): array
|
| 366 |
+
{
|
| 367 |
+
$totalProducts = Product::count();
|
| 368 |
+
$lowStockProducts = Product::where('Amount', '<', 10)->count();
|
| 369 |
+
$outOfStockProducts = Product::where('Amount', 0)->count();
|
| 370 |
+
|
| 371 |
+
return [
|
| 372 |
+
'total_products' => $totalProducts,
|
| 373 |
+
'low_stock' => $lowStockProducts,
|
| 374 |
+
'out_of_stock' => $outOfStockProducts,
|
| 375 |
+
'in_stock' => $totalProducts - $outOfStockProducts,
|
| 376 |
+
'average_price' => (float) Product::avg('price')
|
| 377 |
+
];
|
| 378 |
+
}
|
| 379 |
+
}
|
app/View/Components/AppLayout.php
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
|
| 3 |
+
namespace App\View\Components;
|
| 4 |
+
|
| 5 |
+
use Illuminate\View\Component;
|
| 6 |
+
use Illuminate\View\View;
|
| 7 |
+
|
| 8 |
+
class AppLayout extends Component
|
| 9 |
+
{
|
| 10 |
+
/**
|
| 11 |
+
* Get the view / contents that represents the component.
|
| 12 |
+
*/
|
| 13 |
+
public function render(): View
|
| 14 |
+
{
|
| 15 |
+
return view('layouts.app');
|
| 16 |
+
}
|
| 17 |
+
}
|