Compare commits
2 commits
main
...
feature/2-
| Author | SHA1 | Date | |
|---|---|---|---|
| fd05ffd06d | |||
| ed27f4bb49 |
567 changed files with 7609 additions and 12084 deletions
109
.docker/development/README.md
Normal file
109
.docker/development/README.md
Normal file
|
|
@ -0,0 +1,109 @@
|
||||||
|
# Development Docker Setup
|
||||||
|
|
||||||
|
This directory contains Docker Compose files for local development using Laravel Sail.
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
- **docker-compose.yml** - Base Sail configuration for backend (Laravel) and MySQL
|
||||||
|
- **docker-compose.override.yml** - Development overrides:
|
||||||
|
- Podman/SELinux compatibility (`:Z` volume flags)
|
||||||
|
- Standard MySQL image (instead of mysql-server)
|
||||||
|
- Frontend container (Node.js development server)
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
Use the automated setup script from the project root:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./bin/start-dev
|
||||||
|
```
|
||||||
|
|
||||||
|
This script will:
|
||||||
|
1. Create `.env` if it doesn't exist
|
||||||
|
2. Install backend dependencies (composer)
|
||||||
|
3. Start all containers (backend, MySQL, frontend)
|
||||||
|
4. Run database migrations
|
||||||
|
5. Optionally seed the database
|
||||||
|
|
||||||
|
## Manual Usage
|
||||||
|
|
||||||
|
### Start all services
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose -f .docker/development/docker-compose.yml \
|
||||||
|
-f .docker/development/docker-compose.override.yml \
|
||||||
|
up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### View logs
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# All services
|
||||||
|
docker compose -f .docker/development/docker-compose.yml \
|
||||||
|
-f .docker/development/docker-compose.override.yml \
|
||||||
|
logs -f
|
||||||
|
|
||||||
|
# Specific service
|
||||||
|
docker compose -f .docker/development/docker-compose.yml \
|
||||||
|
-f .docker/development/docker-compose.override.yml \
|
||||||
|
logs -f frontend
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run backend commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run migrations
|
||||||
|
docker compose -f .docker/development/docker-compose.yml \
|
||||||
|
-f .docker/development/docker-compose.override.yml \
|
||||||
|
exec backend php artisan migrate
|
||||||
|
|
||||||
|
# Run tinker
|
||||||
|
docker compose -f .docker/development/docker-compose.yml \
|
||||||
|
-f .docker/development/docker-compose.override.yml \
|
||||||
|
exec backend php artisan tinker
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
docker compose -f .docker/development/docker-compose.yml \
|
||||||
|
-f .docker/development/docker-compose.override.yml \
|
||||||
|
exec backend php artisan test
|
||||||
|
```
|
||||||
|
|
||||||
|
### Stop all services
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose -f .docker/development/docker-compose.yml \
|
||||||
|
-f .docker/development/docker-compose.override.yml \
|
||||||
|
down
|
||||||
|
```
|
||||||
|
|
||||||
|
## Services
|
||||||
|
|
||||||
|
| Service | Port | Description |
|
||||||
|
|---------|------|-------------|
|
||||||
|
| **backend** | 8000 | Laravel API (PHP 8.4 + Sail) |
|
||||||
|
| **mysql** | 3306 | MySQL 8.0 database |
|
||||||
|
| **frontend** | 5173 | Vite dev server (React Router) |
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
The backend reads from `backend/.env`. Key variables:
|
||||||
|
|
||||||
|
- `APP_PORT` - Backend port (default: 80, set to 8000 for Podman)
|
||||||
|
- `DB_*` - Database connection settings
|
||||||
|
- `FORWARD_DB_PORT` - Exposed MySQL port (default: 3306)
|
||||||
|
|
||||||
|
## Volume Mounts
|
||||||
|
|
||||||
|
- `backend/` → `/var/www/html` (backend container)
|
||||||
|
- `frontend/` → `/app` (frontend container)
|
||||||
|
- MySQL data persists in named volume `sail-mysql`
|
||||||
|
|
||||||
|
## Podman Compatibility
|
||||||
|
|
||||||
|
The `:Z` flag on volumes enables SELinux compatibility for Podman users. This is automatically included in the override file.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- The frontend container automatically runs `npm install` and `npm run dev` on startup
|
||||||
|
- Laravel Sail handles PHP-FPM, supervisor, and other backend services
|
||||||
|
- Hot reload is enabled for both frontend (Vite HMR) and backend (file watchers)
|
||||||
25
.docker/development/docker-compose.override.yml
Normal file
25
.docker/development/docker-compose.override.yml
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
services:
|
||||||
|
backend:
|
||||||
|
volumes:
|
||||||
|
- '../../backend:/var/www/html:Z'
|
||||||
|
|
||||||
|
mysql:
|
||||||
|
image: 'docker.io/library/mysql:8.0'
|
||||||
|
environment:
|
||||||
|
MYSQL_USER: '${DB_USERNAME}'
|
||||||
|
MYSQL_PASSWORD: '${DB_PASSWORD}'
|
||||||
|
volumes:
|
||||||
|
- 'sail-mysql:/var/lib/mysql:Z'
|
||||||
|
|
||||||
|
frontend:
|
||||||
|
image: 'docker.io/library/node:22-alpine'
|
||||||
|
working_dir: /app
|
||||||
|
command: sh -c "npm install && npm run dev -- --host"
|
||||||
|
volumes:
|
||||||
|
- '../../frontend:/app:Z'
|
||||||
|
ports:
|
||||||
|
- '5173:5173'
|
||||||
|
networks:
|
||||||
|
- sail
|
||||||
|
depends_on:
|
||||||
|
- backend
|
||||||
54
.docker/development/docker-compose.yml
Normal file
54
.docker/development/docker-compose.yml
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
services:
|
||||||
|
backend:
|
||||||
|
build:
|
||||||
|
context: '../../backend/vendor/laravel/sail/runtimes/8.4'
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
args:
|
||||||
|
WWWGROUP: '${WWWGROUP}'
|
||||||
|
image: 'sail-8.4/app'
|
||||||
|
extra_hosts:
|
||||||
|
- 'host.docker.internal:host-gateway'
|
||||||
|
ports:
|
||||||
|
- '${APP_PORT:-80}:80'
|
||||||
|
- '${VITE_PORT:-5173}:${VITE_PORT:-5173}'
|
||||||
|
environment:
|
||||||
|
WWWUSER: '${WWWUSER}'
|
||||||
|
LARAVEL_SAIL: 1
|
||||||
|
XDEBUG_MODE: '${SAIL_XDEBUG_MODE:-off}'
|
||||||
|
XDEBUG_CONFIG: '${SAIL_XDEBUG_CONFIG:-client_host=host.docker.internal}'
|
||||||
|
IGNITION_LOCAL_SITES_PATH: '${PWD}'
|
||||||
|
volumes:
|
||||||
|
- '../../backend:/var/www/html:Z'
|
||||||
|
networks:
|
||||||
|
- sail
|
||||||
|
depends_on:
|
||||||
|
- mysql
|
||||||
|
mysql:
|
||||||
|
image: 'docker.io/mysql/mysql-server:8.0'
|
||||||
|
ports:
|
||||||
|
- '${FORWARD_DB_PORT:-3306}:3306'
|
||||||
|
environment:
|
||||||
|
MYSQL_ROOT_PASSWORD: '${DB_PASSWORD}'
|
||||||
|
MYSQL_ROOT_HOST: '%'
|
||||||
|
MYSQL_DATABASE: '${DB_DATABASE}'
|
||||||
|
MYSQL_USER: '${DB_USERNAME}'
|
||||||
|
MYSQL_PASSWORD: '${DB_PASSWORD}'
|
||||||
|
MYSQL_ALLOW_EMPTY_PASSWORD: 1
|
||||||
|
volumes:
|
||||||
|
- 'sail-mysql:/var/lib/mysql:Z'
|
||||||
|
networks:
|
||||||
|
- sail
|
||||||
|
healthcheck:
|
||||||
|
test:
|
||||||
|
- CMD
|
||||||
|
- mysqladmin
|
||||||
|
- ping
|
||||||
|
- '-p${DB_PASSWORD}'
|
||||||
|
retries: 3
|
||||||
|
timeout: 5s
|
||||||
|
networks:
|
||||||
|
sail:
|
||||||
|
driver: bridge
|
||||||
|
volumes:
|
||||||
|
sail-mysql:
|
||||||
|
driver: local
|
||||||
29
.docker/production/.dockerignore
Normal file
29
.docker/production/.dockerignore
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
# Ignore .env files - they should be created at runtime
|
||||||
|
backend/.env
|
||||||
|
backend/.env.production
|
||||||
|
|
||||||
|
# Ignore node_modules - we install dependencies during build
|
||||||
|
frontend/node_modules
|
||||||
|
backend/vendor
|
||||||
|
|
||||||
|
# Ignore git
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
|
||||||
|
# Ignore build artifacts
|
||||||
|
frontend/build
|
||||||
|
backend/bootstrap/cache/*
|
||||||
|
backend/storage/logs/*
|
||||||
|
backend/storage/framework/cache/*
|
||||||
|
backend/storage/framework/sessions/*
|
||||||
|
backend/storage/framework/views/*
|
||||||
|
|
||||||
|
# Ignore test files
|
||||||
|
backend/tests
|
||||||
|
frontend/tests
|
||||||
|
|
||||||
|
# Ignore development files
|
||||||
|
.vscode
|
||||||
|
.idea
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
151
.docker/production/Dockerfile
Normal file
151
.docker/production/Dockerfile
Normal file
|
|
@ -0,0 +1,151 @@
|
||||||
|
# ============================================
|
||||||
|
# Dish Planner - All-in-One Production Image
|
||||||
|
# ============================================
|
||||||
|
# This Dockerfile creates a single container with:
|
||||||
|
# - MySQL 8.0 database
|
||||||
|
# - PHP 8.2-FPM (Laravel backend)
|
||||||
|
# - Node.js 20 (React Router frontend)
|
||||||
|
# - Nginx (reverse proxy)
|
||||||
|
# - Supervisor (process manager)
|
||||||
|
#
|
||||||
|
# Build: docker build -f .docker/production/Dockerfile -t codeberg.org/lvl0/dish-planner:latest .
|
||||||
|
# Deploy: See .docker/production/README.md for deployment instructions
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
FROM ubuntu:22.04
|
||||||
|
|
||||||
|
# Prevent interactive prompts during build
|
||||||
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
ENV TZ=UTC
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Install System Dependencies
|
||||||
|
# ============================================
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
# Basic utilities
|
||||||
|
curl \
|
||||||
|
wget \
|
||||||
|
git \
|
||||||
|
unzip \
|
||||||
|
ca-certificates \
|
||||||
|
gnupg \
|
||||||
|
supervisor \
|
||||||
|
software-properties-common \
|
||||||
|
# Nginx
|
||||||
|
nginx \
|
||||||
|
# MySQL Server
|
||||||
|
mysql-server \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Install PHP 8.2 from ondrej/php PPA
|
||||||
|
# ============================================
|
||||||
|
RUN add-apt-repository ppa:ondrej/php -y \
|
||||||
|
&& apt-get update \
|
||||||
|
&& apt-get install -y \
|
||||||
|
php8.2-fpm \
|
||||||
|
php8.2-cli \
|
||||||
|
php8.2-mysql \
|
||||||
|
php8.2-mbstring \
|
||||||
|
php8.2-xml \
|
||||||
|
php8.2-bcmath \
|
||||||
|
php8.2-curl \
|
||||||
|
php8.2-zip \
|
||||||
|
php8.2-gd \
|
||||||
|
php8.2-intl \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Install Node.js 20
|
||||||
|
# ============================================
|
||||||
|
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
|
||||||
|
&& apt-get install -y nodejs \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Install Composer
|
||||||
|
# ============================================
|
||||||
|
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Set up working directories
|
||||||
|
# ============================================
|
||||||
|
WORKDIR /var/www
|
||||||
|
|
||||||
|
# Create necessary directories
|
||||||
|
RUN mkdir -p /var/www/backend \
|
||||||
|
/var/www/frontend \
|
||||||
|
/var/www/storage/logs \
|
||||||
|
/var/lib/mysql \
|
||||||
|
/run/php
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Copy and Build Backend (Laravel)
|
||||||
|
# ============================================
|
||||||
|
COPY backend /var/www/backend
|
||||||
|
WORKDIR /var/www/backend
|
||||||
|
|
||||||
|
# Remove any existing .env files - they will be created at runtime
|
||||||
|
RUN rm -f .env .env.production
|
||||||
|
|
||||||
|
# Install PHP dependencies (production)
|
||||||
|
RUN composer install --no-dev --optimize-autoloader --no-interaction
|
||||||
|
|
||||||
|
# Set permissions for Laravel
|
||||||
|
RUN chown -R www-data:www-data /var/www/backend/storage /var/www/backend/bootstrap/cache \
|
||||||
|
&& chmod -R 775 /var/www/backend/storage /var/www/backend/bootstrap/cache
|
||||||
|
|
||||||
|
# Add www-data to mysql group so it can access MySQL socket
|
||||||
|
RUN usermod -a -G mysql www-data
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Copy and Build Frontend (React Router)
|
||||||
|
# ============================================
|
||||||
|
COPY frontend /var/www/frontend
|
||||||
|
WORKDIR /var/www/frontend
|
||||||
|
|
||||||
|
# Install all dependencies (including dev for build), build, then remove dev deps
|
||||||
|
RUN npm ci \
|
||||||
|
&& npm run build \
|
||||||
|
&& npm prune --production
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Configure Nginx
|
||||||
|
# ============================================
|
||||||
|
COPY .docker/production/nginx.conf /etc/nginx/sites-available/default
|
||||||
|
RUN ln -sf /etc/nginx/sites-available/default /etc/nginx/sites-enabled/default \
|
||||||
|
&& rm -f /etc/nginx/sites-enabled/default.old
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Configure PHP-FPM
|
||||||
|
# ============================================
|
||||||
|
RUN sed -i 's/listen = \/run\/php\/php8.2-fpm.sock/listen = 127.0.0.1:9000/' /etc/php/8.2/fpm/pool.d/www.conf
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Configure MySQL
|
||||||
|
# ============================================
|
||||||
|
# Allow MySQL to bind to all interfaces (for easier debugging if needed)
|
||||||
|
RUN sed -i 's/bind-address.*/bind-address = 127.0.0.1/' /etc/mysql/mysql.conf.d/mysqld.cnf || true
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Copy Configuration Files
|
||||||
|
# ============================================
|
||||||
|
COPY .docker/production/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
|
||||||
|
COPY .docker/production/entrypoint.sh /usr/local/bin/entrypoint.sh
|
||||||
|
RUN chmod +x /usr/local/bin/entrypoint.sh
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Create volume mount points
|
||||||
|
# ============================================
|
||||||
|
VOLUME ["/var/lib/mysql", "/var/www/backend/storage"]
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Expose HTTP port
|
||||||
|
# ============================================
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Set entrypoint and default command
|
||||||
|
# ============================================
|
||||||
|
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
|
||||||
|
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]
|
||||||
345
.docker/production/README.md
Normal file
345
.docker/production/README.md
Normal file
|
|
@ -0,0 +1,345 @@
|
||||||
|
# Dish Planner - Self-Hosted Deployment Guide
|
||||||
|
|
||||||
|
This directory contains everything you need to deploy Dish Planner as a self-hosted all-in-one container.
|
||||||
|
|
||||||
|
> **Note**: This guide uses an embedded docker-compose configuration (no separate file to download). Simply copy the YAML configuration shown below and save it as `docker-compose.yml`.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### For End Users (Using Pre-Built Image)
|
||||||
|
|
||||||
|
> **Image Versioning**: This guide uses the `:latest` tag for simplicity. For production deployments, consider using a specific version tag (e.g., `:v0.3`) for stability and predictable updates.
|
||||||
|
|
||||||
|
1. **Create a docker-compose.yml file:**
|
||||||
|
|
||||||
|
Copy the following configuration and save it as `docker-compose.yml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# ============================================
|
||||||
|
# Dish Planner - Self-Hosted Deployment
|
||||||
|
# ============================================
|
||||||
|
#
|
||||||
|
# QUICK START:
|
||||||
|
# 1. Save this file as docker-compose.yml
|
||||||
|
# 2. (Optional) Edit the environment variables below
|
||||||
|
# 3. Run: docker compose up -d
|
||||||
|
# 4. Access your app at http://localhost:3000
|
||||||
|
#
|
||||||
|
# IMPORTANT SECURITY NOTES:
|
||||||
|
# - Database credentials are auto-generated on first run
|
||||||
|
# - Check the container logs to see generated credentials:
|
||||||
|
# docker logs dishplanner
|
||||||
|
# - For production, set a custom APP_URL environment variable
|
||||||
|
#
|
||||||
|
# CUSTOMIZATION:
|
||||||
|
# You can override any environment variable by uncommenting
|
||||||
|
# and editing the values below, or by creating a .env file.
|
||||||
|
#
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
services:
|
||||||
|
dishplanner:
|
||||||
|
# Pre-built all-in-one image from Codeberg Container Registry
|
||||||
|
image: codeberg.org/lvl0/dish-planner:latest
|
||||||
|
|
||||||
|
container_name: dishplanner
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
# ----------------------------------------
|
||||||
|
# Port Configuration
|
||||||
|
# ----------------------------------------
|
||||||
|
# The application will be accessible on port 3000
|
||||||
|
# Change the left number to use a different host port
|
||||||
|
# Example: "8080:80" to access on port 8080
|
||||||
|
ports:
|
||||||
|
- "3000:80"
|
||||||
|
|
||||||
|
# ----------------------------------------
|
||||||
|
# Environment Variables
|
||||||
|
# ----------------------------------------
|
||||||
|
# Uncomment and customize as needed
|
||||||
|
environment:
|
||||||
|
# Application URL (set this to your domain)
|
||||||
|
- APP_URL=http://localhost:3000
|
||||||
|
|
||||||
|
# Application environment (production recommended)
|
||||||
|
- APP_ENV=production
|
||||||
|
|
||||||
|
# Debug mode (set to false in production for security)
|
||||||
|
- APP_DEBUG=false
|
||||||
|
|
||||||
|
# Database configuration
|
||||||
|
# Note: Credentials are auto-generated on first run if not set
|
||||||
|
# Check logs for generated credentials: docker logs dishplanner
|
||||||
|
# - DB_DATABASE=dishplanner
|
||||||
|
# - DB_USERNAME=dishuser
|
||||||
|
# - DB_PASSWORD=change-this-secure-password
|
||||||
|
|
||||||
|
# Timezone (optional)
|
||||||
|
# - APP_TIMEZONE=UTC
|
||||||
|
|
||||||
|
# ----------------------------------------
|
||||||
|
# Persistent Data Volumes
|
||||||
|
# ----------------------------------------
|
||||||
|
# These volumes ensure data persists across container restarts
|
||||||
|
volumes:
|
||||||
|
# MySQL database data
|
||||||
|
- dishplanner_mysql_data:/var/lib/mysql
|
||||||
|
|
||||||
|
# Laravel storage (uploaded files, logs, cache)
|
||||||
|
- dishplanner_storage:/var/www/backend/storage
|
||||||
|
|
||||||
|
# Supervisor logs
|
||||||
|
- dishplanner_logs:/var/log/supervisor
|
||||||
|
|
||||||
|
# ----------------------------------------
|
||||||
|
# Health Check (optional)
|
||||||
|
# ----------------------------------------
|
||||||
|
# Uncomment to enable container health monitoring
|
||||||
|
# healthcheck:
|
||||||
|
# test: ["CMD", "curl", "-f", "http://localhost/api/health"]
|
||||||
|
# interval: 30s
|
||||||
|
# timeout: 10s
|
||||||
|
# retries: 3
|
||||||
|
# start_period: 60s
|
||||||
|
|
||||||
|
# ----------------------------------------
|
||||||
|
# Named Volumes
|
||||||
|
# ----------------------------------------
|
||||||
|
# Docker manages these volumes - data persists even if container is removed
|
||||||
|
volumes:
|
||||||
|
dishplanner_mysql_data:
|
||||||
|
driver: local
|
||||||
|
dishplanner_storage:
|
||||||
|
driver: local
|
||||||
|
dishplanner_logs:
|
||||||
|
driver: local
|
||||||
|
|
||||||
|
# ----------------------------------------
|
||||||
|
# Network Configuration
|
||||||
|
# ----------------------------------------
|
||||||
|
# Uses default bridge network (suitable for single-host deployment)
|
||||||
|
# For advanced setups, you can define custom networks here
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Start the application:**
|
||||||
|
```bash
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **View the initialization logs to get your database credentials:**
|
||||||
|
```bash
|
||||||
|
docker logs dishplanner
|
||||||
|
```
|
||||||
|
|
||||||
|
Save the auto-generated database credentials shown in the logs!
|
||||||
|
|
||||||
|
4. **Access the application:**
|
||||||
|
Open your browser to `http://localhost:3000`
|
||||||
|
|
||||||
|
That's it! The application is now running with:
|
||||||
|
- MySQL database
|
||||||
|
- Laravel backend API
|
||||||
|
- React Router frontend
|
||||||
|
- Nginx reverse proxy
|
||||||
|
- Background queue worker
|
||||||
|
|
||||||
|
### For DockGE Users
|
||||||
|
|
||||||
|
1. In the DockGE web UI, click "Add Stack"
|
||||||
|
2. Copy the entire YAML configuration from the code block above (starting from `services:` and including all volumes)
|
||||||
|
3. Paste it into the DockGE compose editor
|
||||||
|
4. Optionally edit the environment variables
|
||||||
|
5. Click "Start"
|
||||||
|
6. View logs to get auto-generated database credentials
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
You can customize the deployment by setting these environment variables in the docker-compose file:
|
||||||
|
|
||||||
|
| Variable | Default | Description |
|
||||||
|
|----------|---------|-------------|
|
||||||
|
| `APP_URL` | `http://localhost:3000` | The URL where your app is accessible |
|
||||||
|
| `APP_ENV` | `production` | Environment mode (production/local) |
|
||||||
|
| `APP_DEBUG` | `false` | Enable debug mode (never use in production!) |
|
||||||
|
| `DB_DATABASE` | `dishplanner` | Database name |
|
||||||
|
| `DB_USERNAME` | `dishuser` | Database username |
|
||||||
|
| `DB_PASSWORD` | Auto-generated | Database password (check logs for generated value) |
|
||||||
|
|
||||||
|
### Changing the Port
|
||||||
|
|
||||||
|
By default, the app runs on port 3000. To use a different port, edit the `ports` section:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
ports:
|
||||||
|
- "8080:80" # This makes the app available on port 8080
|
||||||
|
```
|
||||||
|
|
||||||
|
### Persistent Data
|
||||||
|
|
||||||
|
The following data is persisted in Docker volumes:
|
||||||
|
- **MySQL database** - All your dishes, schedules, and users
|
||||||
|
- **Laravel storage** - Uploaded files, logs, and cache
|
||||||
|
- **Supervisor logs** - Application and service logs
|
||||||
|
|
||||||
|
Even if you remove the container, this data remains intact.
|
||||||
|
|
||||||
|
## Building the Image Yourself
|
||||||
|
|
||||||
|
If you prefer to build the image yourself instead of using the pre-built one:
|
||||||
|
|
||||||
|
1. **Clone the repository:**
|
||||||
|
```bash
|
||||||
|
git clone https://codeberg.org/lvl0/dish-planner.git
|
||||||
|
cd dish-planner
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Build the image:**
|
||||||
|
```bash
|
||||||
|
docker build -f .docker/production/Dockerfile -t codeberg.org/lvl0/dish-planner:latest .
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Create a docker-compose.yml file** using the configuration shown in the Quick Start section above (or save the embedded compose config to a file)
|
||||||
|
|
||||||
|
4. **Run it:**
|
||||||
|
```bash
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
## Management
|
||||||
|
|
||||||
|
### Viewing Logs
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# All logs
|
||||||
|
docker logs dishplanner
|
||||||
|
|
||||||
|
# Follow logs in real-time
|
||||||
|
docker logs -f dishplanner
|
||||||
|
|
||||||
|
# Last 100 lines
|
||||||
|
docker logs --tail 100 dishplanner
|
||||||
|
```
|
||||||
|
|
||||||
|
### Stopping the Application
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose down
|
||||||
|
```
|
||||||
|
|
||||||
|
### Restarting the Application
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose restart
|
||||||
|
```
|
||||||
|
|
||||||
|
### Updating to a New Version
|
||||||
|
|
||||||
|
1. Pull the latest image:
|
||||||
|
```bash
|
||||||
|
docker compose pull
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Recreate the container:
|
||||||
|
```bash
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Your data persists in volumes automatically!
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### "Cannot connect to database"
|
||||||
|
|
||||||
|
Check that MySQL started successfully:
|
||||||
|
```bash
|
||||||
|
docker logs dishplanner | grep mysql
|
||||||
|
```
|
||||||
|
|
||||||
|
### "502 Bad Gateway"
|
||||||
|
|
||||||
|
One of the services may not have started. Check supervisor logs:
|
||||||
|
```bash
|
||||||
|
docker exec dishplanner supervisorctl status
|
||||||
|
```
|
||||||
|
|
||||||
|
### Reset Everything (CAUTION: Deletes all data!)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose down -v # The -v flag removes volumes
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Access the Container Shell
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker exec -it dishplanner bash
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run Laravel Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run migrations
|
||||||
|
docker exec dishplanner php /var/www/backend/artisan migrate
|
||||||
|
|
||||||
|
# Create a new user (if needed)
|
||||||
|
docker exec -it dishplanner php /var/www/backend/artisan tinker
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
This all-in-one container includes:
|
||||||
|
|
||||||
|
- **MySQL 8.0** - Database server
|
||||||
|
- **PHP 8.2-FPM** - Runs the Laravel backend
|
||||||
|
- **Node.js 20** - Runs the React Router frontend
|
||||||
|
- **Nginx** - Web server and reverse proxy
|
||||||
|
- **Supervisord** - Process manager that keeps everything running
|
||||||
|
|
||||||
|
All services start automatically and are monitored by supervisord. If any service crashes, it will be automatically restarted.
|
||||||
|
|
||||||
|
## Security Recommendations
|
||||||
|
|
||||||
|
For production deployments:
|
||||||
|
|
||||||
|
1. **Set a strong database password:**
|
||||||
|
```yaml
|
||||||
|
environment:
|
||||||
|
- DB_PASSWORD=your-very-secure-password-here
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Use HTTPS:** Put the container behind a reverse proxy (Nginx, Caddy, Traefik) with SSL/TLS
|
||||||
|
|
||||||
|
3. **Set APP_DEBUG to false:**
|
||||||
|
```yaml
|
||||||
|
environment:
|
||||||
|
- APP_DEBUG=false
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Keep the image updated:** Regularly pull and deploy new versions
|
||||||
|
|
||||||
|
5. **Backup your data:**
|
||||||
|
```bash
|
||||||
|
# Backup volumes
|
||||||
|
docker run --rm -v dishplanner_mysql_data:/data -v $(pwd):/backup ubuntu tar czf /backup/mysql-backup.tar.gz /data
|
||||||
|
```
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
For issues and questions:
|
||||||
|
- Codeberg Issues: https://codeberg.org/lvl0/dish-planner/issues
|
||||||
|
- Documentation: https://codeberg.org/lvl0/dish-planner
|
||||||
|
|
||||||
|
## What's Inside
|
||||||
|
|
||||||
|
The container runs these processes (managed by supervisord):
|
||||||
|
|
||||||
|
1. **MySQL** - Database (port 3306, internal only)
|
||||||
|
2. **PHP-FPM** - Laravel application (port 9000, internal only)
|
||||||
|
3. **Node.js** - React frontend (port 3000, internal only)
|
||||||
|
4. **Nginx** - Reverse proxy (port 80, exposed)
|
||||||
|
5. **Queue Worker** - Background job processor
|
||||||
|
|
||||||
|
Only Nginx's port 80 is exposed to the host (mapped to 3000 by default).
|
||||||
105
.docker/production/docker-compose.test.yml
Normal file
105
.docker/production/docker-compose.test.yml
Normal file
|
|
@ -0,0 +1,105 @@
|
||||||
|
# ============================================
|
||||||
|
# Dish Planner - Self-Hosted Deployment
|
||||||
|
# ============================================
|
||||||
|
#
|
||||||
|
# QUICK START:
|
||||||
|
# 1. Copy this file to your server
|
||||||
|
# 2. (Optional) Edit the environment variables below
|
||||||
|
# 3. Run: docker compose up -d
|
||||||
|
# 4. Access your app at http://localhost:3000
|
||||||
|
#
|
||||||
|
# IMPORTANT SECURITY NOTES:
|
||||||
|
# - Database credentials are auto-generated on first run
|
||||||
|
# - Check the container logs to see generated credentials:
|
||||||
|
# docker logs dishplanner
|
||||||
|
# - For production, set a custom APP_URL environment variable
|
||||||
|
#
|
||||||
|
# CUSTOMIZATION:
|
||||||
|
# You can override any environment variable by uncommenting
|
||||||
|
# and editing the values below, or by creating a .env file.
|
||||||
|
#
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
services:
|
||||||
|
dishplanner:
|
||||||
|
# Pre-built all-in-one image from Docker Hub
|
||||||
|
image: localhost/dishplanner-allinone:test
|
||||||
|
|
||||||
|
container_name: dishplanner
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
# ----------------------------------------
|
||||||
|
# Port Configuration
|
||||||
|
# ----------------------------------------
|
||||||
|
# The application will be accessible on port 3000
|
||||||
|
# Change the left number to use a different host port
|
||||||
|
# Example: "8080:80" to access on port 8080
|
||||||
|
ports:
|
||||||
|
- "3000:80"
|
||||||
|
|
||||||
|
# ----------------------------------------
|
||||||
|
# Environment Variables
|
||||||
|
# ----------------------------------------
|
||||||
|
# Uncomment and customize as needed
|
||||||
|
environment:
|
||||||
|
# Application URL (set this to your domain)
|
||||||
|
- APP_URL=http://localhost:3000
|
||||||
|
|
||||||
|
# Application environment (production recommended)
|
||||||
|
- APP_ENV=production
|
||||||
|
|
||||||
|
# Debug mode (set to false in production for security)
|
||||||
|
- APP_DEBUG=false
|
||||||
|
|
||||||
|
# Database configuration
|
||||||
|
# Note: Credentials are auto-generated on first run if not set
|
||||||
|
# Check logs for generated credentials: docker logs dishplanner
|
||||||
|
# - DB_DATABASE=dishplanner
|
||||||
|
# - DB_USERNAME=dishuser
|
||||||
|
# - DB_PASSWORD=change-this-secure-password
|
||||||
|
|
||||||
|
# Timezone (optional)
|
||||||
|
# - APP_TIMEZONE=UTC
|
||||||
|
|
||||||
|
# ----------------------------------------
|
||||||
|
# Persistent Data Volumes
|
||||||
|
# ----------------------------------------
|
||||||
|
# These volumes ensure data persists across container restarts
|
||||||
|
volumes:
|
||||||
|
# MySQL database data
|
||||||
|
- dishplanner_mysql_data:/var/lib/mysql
|
||||||
|
|
||||||
|
# Laravel storage (uploaded files, logs, cache)
|
||||||
|
- dishplanner_storage:/var/www/backend/storage
|
||||||
|
|
||||||
|
# Supervisor logs
|
||||||
|
- dishplanner_logs:/var/log/supervisor
|
||||||
|
|
||||||
|
# ----------------------------------------
|
||||||
|
# Health Check (optional)
|
||||||
|
# ----------------------------------------
|
||||||
|
# Uncomment to enable container health monitoring
|
||||||
|
# healthcheck:
|
||||||
|
# test: ["CMD", "curl", "-f", "http://localhost/api/health"]
|
||||||
|
# interval: 30s
|
||||||
|
# timeout: 10s
|
||||||
|
# retries: 3
|
||||||
|
# start_period: 60s
|
||||||
|
|
||||||
|
# ----------------------------------------
|
||||||
|
# Named Volumes
|
||||||
|
# ----------------------------------------
|
||||||
|
# Docker manages these volumes - data persists even if container is removed
|
||||||
|
volumes:
|
||||||
|
dishplanner_mysql_data:
|
||||||
|
driver: local
|
||||||
|
dishplanner_storage:
|
||||||
|
driver: local
|
||||||
|
dishplanner_logs:
|
||||||
|
driver: local
|
||||||
|
|
||||||
|
# ----------------------------------------
|
||||||
|
# Network Configuration
|
||||||
|
# ----------------------------------------
|
||||||
|
# Uses default bridge network (suitable for single-host deployment)
|
||||||
|
# For advanced setups, you can define custom networks here
|
||||||
153
.docker/production/entrypoint.sh
Normal file
153
.docker/production/entrypoint.sh
Normal file
|
|
@ -0,0 +1,153 @@
|
||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "=========================================="
|
||||||
|
echo "Dish Planner - Starting Initialization"
|
||||||
|
echo "=========================================="
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# MySQL Initialization
|
||||||
|
# ============================================
|
||||||
|
echo "[1/6] Initializing MySQL..."
|
||||||
|
|
||||||
|
# Check if MySQL data directory is empty (first run)
|
||||||
|
if [ ! -d "/var/lib/mysql/mysql" ]; then
|
||||||
|
echo " → First run detected, initializing MySQL data directory..."
|
||||||
|
mysqld --initialize-insecure --user=mysql --datadir=/var/lib/mysql
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Start MySQL temporarily in background for setup
|
||||||
|
echo " → Starting MySQL..."
|
||||||
|
mysqld --user=mysql --datadir=/var/lib/mysql &
|
||||||
|
MYSQL_PID=$!
|
||||||
|
|
||||||
|
# Wait for MySQL to be ready
|
||||||
|
echo " → Waiting for MySQL to be ready..."
|
||||||
|
for i in {1..30}; do
|
||||||
|
if mysqladmin ping -h localhost --silent; then
|
||||||
|
echo " → MySQL is ready!"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
echo " → Waiting... ($i/30)"
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Database Setup
|
||||||
|
# ============================================
|
||||||
|
echo "[2/6] Setting up database..."
|
||||||
|
|
||||||
|
# Set default values for database credentials
|
||||||
|
DB_DATABASE=${DB_DATABASE:-dishplanner}
|
||||||
|
DB_USERNAME=${DB_USERNAME:-dishuser}
|
||||||
|
|
||||||
|
# Check if this is first run by looking for credentials file
|
||||||
|
CREDS_FILE="/var/www/backend/storage/.db_credentials"
|
||||||
|
|
||||||
|
if [ -f "$CREDS_FILE" ]; then
|
||||||
|
# Not first run - load existing credentials
|
||||||
|
echo " → Loading existing database credentials..."
|
||||||
|
source "$CREDS_FILE"
|
||||||
|
else
|
||||||
|
# First run - generate new credentials and save them
|
||||||
|
echo " → First run detected, generating credentials..."
|
||||||
|
DB_PASSWORD=${DB_PASSWORD:-$(openssl rand -base64 32)}
|
||||||
|
MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD:-$(openssl rand -base64 32)}
|
||||||
|
|
||||||
|
# Save credentials for future restarts
|
||||||
|
cat > "$CREDS_FILE" <<EOF
|
||||||
|
DB_PASSWORD='${DB_PASSWORD}'
|
||||||
|
MYSQL_ROOT_PASSWORD='${MYSQL_ROOT_PASSWORD}'
|
||||||
|
EOF
|
||||||
|
chmod 600 "$CREDS_FILE"
|
||||||
|
|
||||||
|
# Set root password
|
||||||
|
mysql -u root <<-EOSQL
|
||||||
|
ALTER USER 'root'@'localhost' IDENTIFIED BY '${MYSQL_ROOT_PASSWORD}';
|
||||||
|
FLUSH PRIVILEGES;
|
||||||
|
EOSQL
|
||||||
|
|
||||||
|
# Create database and user
|
||||||
|
mysql -u root -p"${MYSQL_ROOT_PASSWORD}" <<-EOSQL
|
||||||
|
CREATE DATABASE IF NOT EXISTS ${DB_DATABASE};
|
||||||
|
CREATE USER IF NOT EXISTS '${DB_USERNAME}'@'localhost' IDENTIFIED BY '${DB_PASSWORD}';
|
||||||
|
CREATE USER IF NOT EXISTS '${DB_USERNAME}'@'%' IDENTIFIED BY '${DB_PASSWORD}';
|
||||||
|
GRANT ALL PRIVILEGES ON ${DB_DATABASE}.* TO '${DB_USERNAME}'@'localhost';
|
||||||
|
GRANT ALL PRIVILEGES ON ${DB_DATABASE}.* TO '${DB_USERNAME}'@'%';
|
||||||
|
FLUSH PRIVILEGES;
|
||||||
|
EOSQL
|
||||||
|
|
||||||
|
echo " → Database '${DB_DATABASE}' created"
|
||||||
|
echo " → User '${DB_USERNAME}' created"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Laravel Environment Setup
|
||||||
|
# ============================================
|
||||||
|
echo "[3/6] Configuring Laravel environment..."
|
||||||
|
|
||||||
|
cd /var/www/backend
|
||||||
|
|
||||||
|
# Create .env if it doesn't exist
|
||||||
|
if [ ! -f .env ]; then
|
||||||
|
echo " → Creating .env file..."
|
||||||
|
cp .env.example .env
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Update database credentials in .env using a more robust method
|
||||||
|
# Use @ as delimiter to avoid conflicts with special chars in passwords
|
||||||
|
sed -i "s@DB_DATABASE=.*@DB_DATABASE=${DB_DATABASE}@" .env
|
||||||
|
sed -i "s@DB_USERNAME=.*@DB_USERNAME=${DB_USERNAME}@" .env
|
||||||
|
sed -i "s@DB_PASSWORD=.*@DB_PASSWORD=${DB_PASSWORD}@" .env
|
||||||
|
sed -i "s@DB_HOST=.*@DB_HOST=127.0.0.1@" .env
|
||||||
|
sed -i "s@DB_PORT=.*@DB_PORT=3306@" .env
|
||||||
|
|
||||||
|
# Generate APP_KEY if not set
|
||||||
|
if ! grep -q "APP_KEY=base64:" .env; then
|
||||||
|
echo " → Generating application key..."
|
||||||
|
php artisan key:generate --force
|
||||||
|
else
|
||||||
|
echo " → Application key already set"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Set APP_URL if provided
|
||||||
|
if [ -n "${APP_URL}" ]; then
|
||||||
|
sed -i "s@APP_URL=.*@APP_URL=${APP_URL}@" .env
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Run Database Migrations
|
||||||
|
# ============================================
|
||||||
|
echo "[4/6] Running database migrations..."
|
||||||
|
php artisan migrate --force
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Laravel Optimizations
|
||||||
|
# ============================================
|
||||||
|
echo "[5/6] Optimizing Laravel..."
|
||||||
|
php artisan config:cache
|
||||||
|
php artisan route:cache
|
||||||
|
php artisan view:cache
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Stop temporary MySQL instance
|
||||||
|
# ============================================
|
||||||
|
echo "[6/6] Stopping temporary MySQL instance..."
|
||||||
|
mysqladmin -u root -p"${MYSQL_ROOT_PASSWORD}" shutdown
|
||||||
|
wait $MYSQL_PID
|
||||||
|
|
||||||
|
echo "=========================================="
|
||||||
|
echo "Initialization complete!"
|
||||||
|
echo "=========================================="
|
||||||
|
echo ""
|
||||||
|
echo "Database Credentials (save these!):"
|
||||||
|
echo " Database: ${DB_DATABASE}"
|
||||||
|
echo " Username: ${DB_USERNAME}"
|
||||||
|
echo " Password: ${DB_PASSWORD}"
|
||||||
|
echo " Root Password: ${MYSQL_ROOT_PASSWORD}"
|
||||||
|
echo ""
|
||||||
|
echo "Starting all services with supervisord..."
|
||||||
|
echo "=========================================="
|
||||||
|
|
||||||
|
# Execute the command passed to the container (supervisord)
|
||||||
|
exec "$@"
|
||||||
109
.docker/production/nginx.conf
Normal file
109
.docker/production/nginx.conf
Normal file
|
|
@ -0,0 +1,109 @@
|
||||||
|
server {
|
||||||
|
listen 80 default_server;
|
||||||
|
listen [::]:80 default_server;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
access_log /var/log/nginx/access.log;
|
||||||
|
error_log /var/log/nginx/error.log;
|
||||||
|
|
||||||
|
# =========================================
|
||||||
|
# Frontend Static Assets (served directly by nginx) - MUST BE FIRST
|
||||||
|
# =========================================
|
||||||
|
# Use ^~ to prevent regex locations from matching
|
||||||
|
location ^~ /assets/ {
|
||||||
|
alias /var/www/frontend/build/client/assets/;
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
access_log off;
|
||||||
|
}
|
||||||
|
|
||||||
|
# =========================================
|
||||||
|
# Backend API Routes (Laravel)
|
||||||
|
# =========================================
|
||||||
|
root /var/www/backend/public;
|
||||||
|
index index.php index.html;
|
||||||
|
|
||||||
|
location ~ ^/(api|sanctum)/ {
|
||||||
|
try_files $uri $uri/ /index.php?$query_string;
|
||||||
|
|
||||||
|
location ~ \.php$ {
|
||||||
|
fastcgi_pass 127.0.0.1:9000;
|
||||||
|
fastcgi_index index.php;
|
||||||
|
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||||
|
include fastcgi_params;
|
||||||
|
|
||||||
|
# Laravel-specific settings
|
||||||
|
fastcgi_param HTTP_PROXY "";
|
||||||
|
fastcgi_read_timeout 300;
|
||||||
|
fastcgi_buffer_size 128k;
|
||||||
|
fastcgi_buffers 256 16k;
|
||||||
|
fastcgi_busy_buffers_size 256k;
|
||||||
|
fastcgi_temp_file_write_size 256k;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Handle API routes that don't map to files
|
||||||
|
location ~ ^/(api|sanctum) {
|
||||||
|
try_files $uri /index.php?$query_string;
|
||||||
|
}
|
||||||
|
|
||||||
|
# PHP file handler for backend
|
||||||
|
location ~ \.php$ {
|
||||||
|
fastcgi_pass 127.0.0.1:9000;
|
||||||
|
fastcgi_index index.php;
|
||||||
|
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||||
|
include fastcgi_params;
|
||||||
|
}
|
||||||
|
|
||||||
|
# =========================================
|
||||||
|
# Frontend Routes (React Router via Node.js)
|
||||||
|
# =========================================
|
||||||
|
location / {
|
||||||
|
proxy_pass http://127.0.0.1:3000;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
|
||||||
|
# Standard proxy headers
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
|
||||||
|
# WebSocket support (for React Router HMR if needed)
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
|
||||||
|
# Timeouts
|
||||||
|
proxy_connect_timeout 60s;
|
||||||
|
proxy_send_timeout 60s;
|
||||||
|
proxy_read_timeout 60s;
|
||||||
|
}
|
||||||
|
|
||||||
|
# =========================================
|
||||||
|
# Security & Performance
|
||||||
|
# =========================================
|
||||||
|
|
||||||
|
# Deny access to hidden files
|
||||||
|
location ~ /\. {
|
||||||
|
deny all;
|
||||||
|
access_log off;
|
||||||
|
log_not_found off;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Disable logging for favicon and robots.txt
|
||||||
|
location = /favicon.ico {
|
||||||
|
access_log off;
|
||||||
|
log_not_found off;
|
||||||
|
}
|
||||||
|
|
||||||
|
location = /robots.txt {
|
||||||
|
access_log off;
|
||||||
|
log_not_found off;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Cache static assets
|
||||||
|
location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot)$ {
|
||||||
|
expires 30d;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
}
|
||||||
|
}
|
||||||
52
.docker/production/supervisord.conf
Normal file
52
.docker/production/supervisord.conf
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
[supervisord]
|
||||||
|
nodaemon=true
|
||||||
|
user=root
|
||||||
|
logfile=/var/log/supervisor/supervisord.log
|
||||||
|
pidfile=/var/run/supervisord.pid
|
||||||
|
loglevel=info
|
||||||
|
|
||||||
|
[program:mysql]
|
||||||
|
command=/usr/sbin/mysqld --user=mysql --console
|
||||||
|
autostart=true
|
||||||
|
autorestart=true
|
||||||
|
stdout_logfile=/var/log/supervisor/mysql.log
|
||||||
|
stderr_logfile=/var/log/supervisor/mysql_error.log
|
||||||
|
priority=1
|
||||||
|
|
||||||
|
[program:php-fpm]
|
||||||
|
command=/usr/sbin/php-fpm8.2 -F
|
||||||
|
autostart=true
|
||||||
|
autorestart=true
|
||||||
|
stdout_logfile=/var/log/supervisor/php-fpm.log
|
||||||
|
stderr_logfile=/var/log/supervisor/php-fpm_error.log
|
||||||
|
priority=10
|
||||||
|
|
||||||
|
[program:nginx]
|
||||||
|
command=/usr/sbin/nginx -g "daemon off;"
|
||||||
|
autostart=true
|
||||||
|
autorestart=true
|
||||||
|
stdout_logfile=/var/log/supervisor/nginx.log
|
||||||
|
stderr_logfile=/var/log/supervisor/nginx_error.log
|
||||||
|
priority=20
|
||||||
|
|
||||||
|
[program:frontend]
|
||||||
|
command=/usr/bin/node /var/www/frontend/node_modules/@react-router/serve/dist/cli.js /var/www/frontend/build/server/index.js
|
||||||
|
directory=/var/www/frontend
|
||||||
|
autostart=true
|
||||||
|
autorestart=true
|
||||||
|
stdout_logfile=/var/log/supervisor/frontend.log
|
||||||
|
stderr_logfile=/var/log/supervisor/frontend_error.log
|
||||||
|
environment=PORT=3000,NODE_ENV=production
|
||||||
|
priority=30
|
||||||
|
user=www-data
|
||||||
|
|
||||||
|
[program:queue-worker]
|
||||||
|
command=/usr/bin/php /var/www/backend/artisan queue:work --sleep=3 --tries=3 --max-time=3600
|
||||||
|
directory=/var/www/backend
|
||||||
|
autostart=true
|
||||||
|
autorestart=true
|
||||||
|
stdout_logfile=/var/log/supervisor/queue-worker.log
|
||||||
|
stderr_logfile=/var/log/supervisor/queue-worker_error.log
|
||||||
|
priority=40
|
||||||
|
user=www-data
|
||||||
|
numprocs=1
|
||||||
|
|
@ -1,62 +0,0 @@
|
||||||
# Git
|
|
||||||
.git
|
|
||||||
.gitignore
|
|
||||||
.gitattributes
|
|
||||||
|
|
||||||
# Documentation
|
|
||||||
*.md
|
|
||||||
LICENSE
|
|
||||||
docs/
|
|
||||||
|
|
||||||
# Environment files
|
|
||||||
.env
|
|
||||||
.env.*
|
|
||||||
!.env.example
|
|
||||||
|
|
||||||
# Dependencies (will be installed fresh)
|
|
||||||
vendor/
|
|
||||||
node_modules/
|
|
||||||
|
|
||||||
# Storage (will be mounted as volumes)
|
|
||||||
storage/app/*
|
|
||||||
storage/framework/cache/*
|
|
||||||
storage/framework/sessions/*
|
|
||||||
storage/framework/views/*
|
|
||||||
storage/logs/*
|
|
||||||
bootstrap/cache/*
|
|
||||||
|
|
||||||
# Testing
|
|
||||||
phpunit.xml
|
|
||||||
.phpunit.result.cache
|
|
||||||
tests/
|
|
||||||
coverage/
|
|
||||||
|
|
||||||
# IDE
|
|
||||||
.idea/
|
|
||||||
.vscode/
|
|
||||||
*.swp
|
|
||||||
*.swo
|
|
||||||
*~
|
|
||||||
|
|
||||||
# OS
|
|
||||||
.DS_Store
|
|
||||||
Thumbs.db
|
|
||||||
|
|
||||||
# Docker
|
|
||||||
Dockerfile*
|
|
||||||
docker-compose*.yml
|
|
||||||
.dockerignore
|
|
||||||
|
|
||||||
# Frontend old
|
|
||||||
frontend-old/
|
|
||||||
|
|
||||||
# Build artifacts
|
|
||||||
public/build/
|
|
||||||
public/hot
|
|
||||||
public/mix-manifest.json
|
|
||||||
|
|
||||||
# Development
|
|
||||||
.php_cs.cache
|
|
||||||
.php-cs-fixer.cache
|
|
||||||
phpstan.neon
|
|
||||||
.editorconfig
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
APP_NAME=DishPlanner
|
|
||||||
APP_ENV=testing
|
|
||||||
APP_KEY=base64:KSKZNT+cJuaBRBv4Y2HQqav6hzREKoLkNIKN8yszU1Q=
|
|
||||||
APP_DEBUG=true
|
|
||||||
APP_URL=http://dishplanner_app:8000
|
|
||||||
|
|
||||||
LOG_CHANNEL=single
|
|
||||||
|
|
||||||
# Test database
|
|
||||||
DB_CONNECTION=mysql
|
|
||||||
DB_HOST=db
|
|
||||||
DB_PORT=3306
|
|
||||||
DB_DATABASE=dishplanner_test
|
|
||||||
DB_USERNAME=dishplanner
|
|
||||||
DB_PASSWORD=dishplanner
|
|
||||||
|
|
||||||
BROADCAST_DRIVER=log
|
|
||||||
CACHE_DRIVER=array
|
|
||||||
FILESYSTEM_DISK=local
|
|
||||||
QUEUE_CONNECTION=sync
|
|
||||||
SESSION_DRIVER=file
|
|
||||||
SESSION_LIFETIME=120
|
|
||||||
|
|
||||||
MAIL_MAILER=array
|
|
||||||
66
.env.example
66
.env.example
|
|
@ -1,66 +0,0 @@
|
||||||
# Application Settings
|
|
||||||
APP_NAME=DishPlanner
|
|
||||||
APP_ENV=local
|
|
||||||
APP_KEY=
|
|
||||||
APP_DEBUG=true
|
|
||||||
APP_TIMEZONE=UTC
|
|
||||||
APP_URL=http://localhost:8000
|
|
||||||
|
|
||||||
APP_LOCALE=en
|
|
||||||
APP_FALLBACK_LOCALE=en
|
|
||||||
APP_FAKER_LOCALE=en_US
|
|
||||||
|
|
||||||
# Logging
|
|
||||||
LOG_CHANNEL=stack
|
|
||||||
LOG_STACK=single
|
|
||||||
LOG_DEPRECATIONS_CHANNEL=null
|
|
||||||
LOG_LEVEL=debug
|
|
||||||
|
|
||||||
# Database Connection (Docker)
|
|
||||||
DB_CONNECTION=mysql
|
|
||||||
DB_HOST=db # Use 'db' for Docker, 'localhost' for local
|
|
||||||
DB_PORT=3306
|
|
||||||
DB_DATABASE=dishplanner
|
|
||||||
DB_USERNAME=dishplanner
|
|
||||||
DB_PASSWORD=dishplanner
|
|
||||||
DB_ROOT_PASSWORD=root # For Docker MariaDB root user
|
|
||||||
|
|
||||||
# Session & Cache
|
|
||||||
SESSION_DRIVER=file # Use 'database' for production
|
|
||||||
SESSION_LIFETIME=120
|
|
||||||
SESSION_ENCRYPT=false
|
|
||||||
SESSION_PATH=/
|
|
||||||
SESSION_DOMAIN=null
|
|
||||||
|
|
||||||
CACHE_STORE=file
|
|
||||||
CACHE_PREFIX=dishplanner
|
|
||||||
|
|
||||||
# Queue
|
|
||||||
QUEUE_CONNECTION=sync # Use 'database' for production
|
|
||||||
|
|
||||||
BROADCAST_CONNECTION=log
|
|
||||||
FILESYSTEM_DISK=local
|
|
||||||
|
|
||||||
# Redis (Optional - uncomment if using Redis)
|
|
||||||
# REDIS_CLIENT=phpredis
|
|
||||||
# REDIS_HOST=redis # Use 'redis' for Docker
|
|
||||||
# REDIS_PASSWORD=null
|
|
||||||
# REDIS_PORT=6379
|
|
||||||
|
|
||||||
# Mail Settings
|
|
||||||
MAIL_MAILER=log # Use 'smtp' for production
|
|
||||||
MAIL_HOST=mailhog # Use 'mailhog' for Docker dev
|
|
||||||
MAIL_PORT=1025
|
|
||||||
MAIL_USERNAME=null
|
|
||||||
MAIL_PASSWORD=null
|
|
||||||
MAIL_ENCRYPTION=null
|
|
||||||
MAIL_FROM_ADDRESS="noreply@dishplanner.local"
|
|
||||||
MAIL_FROM_NAME="${APP_NAME}"
|
|
||||||
|
|
||||||
AWS_ACCESS_KEY_ID=
|
|
||||||
AWS_SECRET_ACCESS_KEY=
|
|
||||||
AWS_DEFAULT_REGION=us-east-1
|
|
||||||
AWS_BUCKET=
|
|
||||||
AWS_USE_PATH_STYLE_ENDPOINT=false
|
|
||||||
|
|
||||||
VITE_APP_NAME="${APP_NAME}"
|
|
||||||
25
.gitignore
vendored
25
.gitignore
vendored
|
|
@ -1,26 +1 @@
|
||||||
/composer.lock
|
|
||||||
/.phpunit.cache
|
|
||||||
/coverage
|
|
||||||
/node_modules
|
|
||||||
/public/build
|
|
||||||
/public/hot
|
|
||||||
/public/storage
|
|
||||||
/storage/*.key
|
|
||||||
/storage/pail
|
|
||||||
/vendor
|
|
||||||
.env
|
|
||||||
.env.backup
|
|
||||||
.env.production
|
|
||||||
.phpactor.json
|
|
||||||
.phpunit.result.cache
|
|
||||||
Homestead.json
|
|
||||||
Homestead.yaml
|
|
||||||
npm-debug.log
|
|
||||||
yarn-error.log
|
|
||||||
/auth.json
|
|
||||||
/.fleet
|
|
||||||
/.idea
|
/.idea
|
||||||
/.nova
|
|
||||||
/.vscode
|
|
||||||
/.zed
|
|
||||||
/.vite
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
{
|
|
||||||
"hash": "9e76e24f",
|
|
||||||
"configHash": "16a1459d",
|
|
||||||
"lockfileHash": "e3b0c442",
|
|
||||||
"browserHash": "8e8bb46e",
|
|
||||||
"optimized": {},
|
|
||||||
"chunks": {}
|
|
||||||
}
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
{
|
|
||||||
"type": "module"
|
|
||||||
}
|
|
||||||
123
Dockerfile
123
Dockerfile
|
|
@ -1,123 +0,0 @@
|
||||||
# Production Dockerfile with FrankenPHP
|
|
||||||
FROM dunglas/frankenphp:latest-php8.3-alpine
|
|
||||||
|
|
||||||
# Install system dependencies
|
|
||||||
RUN apk add --no-cache \
|
|
||||||
nodejs \
|
|
||||||
npm \
|
|
||||||
git \
|
|
||||||
mysql-client
|
|
||||||
|
|
||||||
# Install PHP extensions
|
|
||||||
RUN install-php-extensions \
|
|
||||||
pdo_mysql \
|
|
||||||
opcache \
|
|
||||||
zip \
|
|
||||||
gd \
|
|
||||||
intl \
|
|
||||||
bcmath
|
|
||||||
|
|
||||||
# Install Composer
|
|
||||||
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
|
|
||||||
|
|
||||||
# Set working directory
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Set fixed production environment variables
|
|
||||||
ENV APP_ENV=production \
|
|
||||||
APP_DEBUG=false \
|
|
||||||
DB_CONNECTION=mysql \
|
|
||||||
DB_HOST=db \
|
|
||||||
DB_PORT=3306 \
|
|
||||||
SESSION_DRIVER=database \
|
|
||||||
CACHE_DRIVER=file \
|
|
||||||
QUEUE_CONNECTION=database \
|
|
||||||
LOG_CHANNEL=stack \
|
|
||||||
LOG_LEVEL=error \
|
|
||||||
MAIL_MAILER=smtp \
|
|
||||||
MAIL_ENCRYPTION=tls
|
|
||||||
|
|
||||||
# Copy application code first
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
# Install PHP dependencies (production only)
|
|
||||||
RUN composer install --no-dev --no-interaction --optimize-autoloader
|
|
||||||
|
|
||||||
# Install ALL Node dependencies (including dev for building)
|
|
||||||
RUN npm ci
|
|
||||||
|
|
||||||
# Build frontend assets
|
|
||||||
RUN npm run build
|
|
||||||
|
|
||||||
# Remove node_modules after build to save space
|
|
||||||
RUN rm -rf node_modules
|
|
||||||
|
|
||||||
# Laravel optimizations
|
|
||||||
RUN php artisan config:cache \
|
|
||||||
&& php artisan route:cache \
|
|
||||||
&& composer dump-autoload --optimize
|
|
||||||
|
|
||||||
# Set permissions
|
|
||||||
RUN chown -R www-data:www-data /app/storage /app/bootstrap/cache
|
|
||||||
|
|
||||||
# Configure Caddy
|
|
||||||
RUN cat > /etc/caddy/Caddyfile <<EOF
|
|
||||||
{
|
|
||||||
frankenphp
|
|
||||||
order php_server before file_server
|
|
||||||
}
|
|
||||||
|
|
||||||
:8000 {
|
|
||||||
root * /app/public
|
|
||||||
|
|
||||||
php_server {
|
|
||||||
index index.php
|
|
||||||
}
|
|
||||||
|
|
||||||
encode gzip
|
|
||||||
|
|
||||||
file_server
|
|
||||||
|
|
||||||
header {
|
|
||||||
X-Frame-Options "SAMEORIGIN"
|
|
||||||
X-Content-Type-Options "nosniff"
|
|
||||||
X-XSS-Protection "1; mode=block"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# Expose port
|
|
||||||
EXPOSE 8000
|
|
||||||
|
|
||||||
# Health check
|
|
||||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
|
||||||
CMD curl -f http://localhost:8000/up || exit 1
|
|
||||||
|
|
||||||
# Create startup script for production
|
|
||||||
RUN cat > /start-prod.sh <<'EOF'
|
|
||||||
#!/bin/sh
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# Wait for database to be ready
|
|
||||||
echo "Waiting for database..."
|
|
||||||
for i in $(seq 1 30); do
|
|
||||||
if mysqladmin ping -h "$DB_HOST" -u "$DB_USERNAME" -p"$DB_PASSWORD" --silent 2>/dev/null; then
|
|
||||||
echo "Database is ready!"
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
echo "Waiting for database... ($i/30)"
|
|
||||||
sleep 2
|
|
||||||
done
|
|
||||||
|
|
||||||
# Run migrations
|
|
||||||
echo "Running migrations..."
|
|
||||||
php artisan migrate --force || echo "Migrations failed or already up-to-date"
|
|
||||||
|
|
||||||
# Start FrankenPHP
|
|
||||||
exec frankenphp run --config /etc/caddy/Caddyfile
|
|
||||||
EOF
|
|
||||||
|
|
||||||
RUN chmod +x /start-prod.sh
|
|
||||||
|
|
||||||
# Start with our script
|
|
||||||
CMD ["/start-prod.sh"]
|
|
||||||
128
Dockerfile.dev
128
Dockerfile.dev
|
|
@ -1,128 +0,0 @@
|
||||||
# Development Dockerfile with FrankenPHP
|
|
||||||
FROM dunglas/frankenphp:latest-php8.3-alpine
|
|
||||||
|
|
||||||
# Install system dependencies + development tools
|
|
||||||
RUN apk add --no-cache \
|
|
||||||
nodejs \
|
|
||||||
npm \
|
|
||||||
git \
|
|
||||||
mysql-client \
|
|
||||||
vim \
|
|
||||||
bash \
|
|
||||||
nano
|
|
||||||
|
|
||||||
# Install PHP extensions including xdebug for development
|
|
||||||
RUN install-php-extensions \
|
|
||||||
pdo_mysql \
|
|
||||||
opcache \
|
|
||||||
zip \
|
|
||||||
gd \
|
|
||||||
intl \
|
|
||||||
bcmath \
|
|
||||||
xdebug
|
|
||||||
|
|
||||||
# Install Composer
|
|
||||||
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
|
|
||||||
|
|
||||||
# Set working directory
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Configure PHP for development
|
|
||||||
RUN mv "$PHP_INI_DIR/php.ini-development" "$PHP_INI_DIR/php.ini"
|
|
||||||
|
|
||||||
# Configure Xdebug (disabled by default to reduce noise)
|
|
||||||
RUN echo "xdebug.mode=off" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini \
|
|
||||||
&& echo ";xdebug.mode=debug" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini \
|
|
||||||
&& echo ";xdebug.client_host=host.docker.internal" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini \
|
|
||||||
&& echo ";xdebug.start_with_request=yes" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini
|
|
||||||
|
|
||||||
# Configure Caddy for development (simpler, no worker mode)
|
|
||||||
RUN cat > /etc/caddy/Caddyfile <<EOF
|
|
||||||
{
|
|
||||||
frankenphp
|
|
||||||
order php_server before file_server
|
|
||||||
}
|
|
||||||
|
|
||||||
:8000 {
|
|
||||||
root * /app/public
|
|
||||||
|
|
||||||
php_server {
|
|
||||||
index index.php
|
|
||||||
}
|
|
||||||
|
|
||||||
encode gzip
|
|
||||||
|
|
||||||
file_server
|
|
||||||
|
|
||||||
# Less strict headers for development
|
|
||||||
header {
|
|
||||||
X-Frame-Options "SAMEORIGIN"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# Install Node development dependencies globally
|
|
||||||
RUN npm install -g nodemon
|
|
||||||
|
|
||||||
# Create startup script for development (runs as host user)
|
|
||||||
RUN cat > /start.sh <<'EOF'
|
|
||||||
#!/bin/sh
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# Create .env file if it doesn't exist
|
|
||||||
if [ ! -f ".env" ]; then
|
|
||||||
echo "Creating .env file from .env.example..."
|
|
||||||
cp .env.example .env
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Install dependencies if volumes are empty
|
|
||||||
if [ ! -f "vendor/autoload.php" ]; then
|
|
||||||
echo "Installing composer dependencies..."
|
|
||||||
composer install
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Handle node_modules with care - clean install if having issues
|
|
||||||
if [ ! -f "node_modules/.bin/vite" ]; then
|
|
||||||
echo "Installing npm dependencies..."
|
|
||||||
# Clean any remnants first
|
|
||||||
rm -rf node_modules/.* 2>/dev/null || true
|
|
||||||
rm -rf /app/.npm 2>/dev/null || true
|
|
||||||
# Fresh install with cache in tmp to avoid permission issues
|
|
||||||
npm install --cache /tmp/.npm
|
|
||||||
else
|
|
||||||
echo "Node modules already installed, skipping npm install"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Clear Laravel caches
|
|
||||||
php artisan config:clear || true
|
|
||||||
php artisan cache:clear || true
|
|
||||||
|
|
||||||
# Wait for database and run migrations
|
|
||||||
echo "Waiting for database..."
|
|
||||||
sleep 5
|
|
||||||
php artisan migrate --force || echo "Migration failed or not needed"
|
|
||||||
|
|
||||||
# Run development seeder (only in dev environment)
|
|
||||||
echo "Running development seeder..."
|
|
||||||
php artisan db:seed --class=DevelopmentSeeder --force || echo "Seeding skipped or already done"
|
|
||||||
|
|
||||||
# Generate app key if not set
|
|
||||||
if [ -z "$APP_KEY" ] || [ "$APP_KEY" = "base64:YOUR_KEY_HERE" ]; then
|
|
||||||
echo "Generating application key..."
|
|
||||||
php artisan key:generate
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Start Vite dev server in background
|
|
||||||
npm run dev &
|
|
||||||
|
|
||||||
# Start FrankenPHP
|
|
||||||
exec frankenphp run --config /etc/caddy/Caddyfile
|
|
||||||
EOF
|
|
||||||
|
|
||||||
RUN chmod +x /start.sh
|
|
||||||
|
|
||||||
# Expose ports
|
|
||||||
EXPOSE 8000 5173
|
|
||||||
|
|
||||||
# Use the startup script
|
|
||||||
CMD ["/start.sh"]
|
|
||||||
123
Makefile
123
Makefile
|
|
@ -1,123 +0,0 @@
|
||||||
# Dish Planner - Docker Commands
|
|
||||||
|
|
||||||
.PHONY: help
|
|
||||||
help: ## Show this help message
|
|
||||||
@echo "Dish Planner - Docker Management"
|
|
||||||
@echo ""
|
|
||||||
@echo "Usage: make [command]"
|
|
||||||
@echo ""
|
|
||||||
@echo "Available commands:"
|
|
||||||
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " %-20s %s\n", $$1, $$2}'
|
|
||||||
|
|
||||||
# Development Commands
|
|
||||||
.PHONY: dev
|
|
||||||
dev: ## Start development environment
|
|
||||||
docker compose up -d
|
|
||||||
@echo "Development server running at http://localhost:8000"
|
|
||||||
@echo "Mailhog available at http://localhost:8025"
|
|
||||||
|
|
||||||
.PHONY: dev-build
|
|
||||||
dev-build: ## Build and start development environment
|
|
||||||
docker compose build
|
|
||||||
docker compose up -d
|
|
||||||
|
|
||||||
.PHONY: dev-stop
|
|
||||||
dev-stop: ## Stop development environment
|
|
||||||
docker compose down
|
|
||||||
|
|
||||||
.PHONY: dev-clean
|
|
||||||
dev-clean: ## Stop and remove volumes (CAUTION: removes database)
|
|
||||||
docker compose down -v
|
|
||||||
|
|
||||||
.PHONY: logs
|
|
||||||
logs: ## Show application logs
|
|
||||||
docker compose logs -f app
|
|
||||||
|
|
||||||
.PHONY: logs-db
|
|
||||||
logs-db: ## Show database logs
|
|
||||||
docker compose logs -f db
|
|
||||||
|
|
||||||
# Production Commands
|
|
||||||
.PHONY: prod-build
|
|
||||||
prod-build: ## Build production image for Codeberg
|
|
||||||
./bin/build-push.sh
|
|
||||||
|
|
||||||
.PHONY: prod-build-tag
|
|
||||||
prod-build-tag: ## Build with specific tag (usage: make prod-build-tag TAG=v1.0.0)
|
|
||||||
./bin/build-push.sh $(TAG)
|
|
||||||
|
|
||||||
.PHONY: prod-login
|
|
||||||
prod-login: ## Login to Codeberg registry
|
|
||||||
podman login codeberg.org
|
|
||||||
|
|
||||||
# Laravel Commands
|
|
||||||
.PHONY: artisan
|
|
||||||
artisan: ## Run artisan command (usage: make artisan cmd="migrate")
|
|
||||||
docker compose exec app php artisan $(cmd)
|
|
||||||
|
|
||||||
.PHONY: composer
|
|
||||||
composer: ## Run composer command (usage: make composer cmd="require package")
|
|
||||||
docker compose exec app composer $(cmd)
|
|
||||||
|
|
||||||
.PHONY: npm
|
|
||||||
npm: ## Run npm command (usage: make npm cmd="install package")
|
|
||||||
docker compose exec app npm $(cmd)
|
|
||||||
|
|
||||||
.PHONY: migrate
|
|
||||||
migrate: ## Run database migrations
|
|
||||||
docker compose exec app php artisan migrate
|
|
||||||
|
|
||||||
.PHONY: seed
|
|
||||||
seed: ## Seed the database
|
|
||||||
docker compose exec app php artisan db:seed
|
|
||||||
|
|
||||||
.PHONY: fresh
|
|
||||||
fresh: ## Fresh migrate and seed
|
|
||||||
docker compose exec app php artisan migrate:fresh --seed
|
|
||||||
|
|
||||||
.PHONY: tinker
|
|
||||||
tinker: ## Start Laravel tinker
|
|
||||||
docker compose exec app php artisan tinker
|
|
||||||
|
|
||||||
.PHONY: test
|
|
||||||
test: ## Run tests
|
|
||||||
docker compose exec app php artisan test
|
|
||||||
|
|
||||||
# Utility Commands
|
|
||||||
.PHONY: shell
|
|
||||||
shell: ## Enter app container shell
|
|
||||||
docker compose exec app sh
|
|
||||||
|
|
||||||
.PHONY: db-shell
|
|
||||||
db-shell: ## Enter database shell
|
|
||||||
docker compose exec db mariadb -u dishplanner -pdishplanner dishplanner
|
|
||||||
|
|
||||||
.PHONY: clear
|
|
||||||
clear: ## Clear all Laravel caches
|
|
||||||
docker compose exec app php artisan cache:clear
|
|
||||||
docker compose exec app php artisan config:clear
|
|
||||||
docker compose exec app php artisan route:clear
|
|
||||||
docker compose exec app php artisan view:clear
|
|
||||||
|
|
||||||
.PHONY: optimize
|
|
||||||
optimize: ## Optimize Laravel for production
|
|
||||||
docker compose exec app php artisan config:cache
|
|
||||||
docker compose exec app php artisan route:cache
|
|
||||||
docker compose exec app php artisan view:cache
|
|
||||||
docker compose exec app php artisan livewire:discover
|
|
||||||
|
|
||||||
# Installation
|
|
||||||
.PHONY: install
|
|
||||||
install: ## First time setup
|
|
||||||
@echo "Setting up Dish Planner..."
|
|
||||||
@cp -n .env.example .env || true
|
|
||||||
@echo "Generating application key..."
|
|
||||||
@docker compose build
|
|
||||||
@docker compose up -d
|
|
||||||
@sleep 5
|
|
||||||
@docker compose exec app php artisan key:generate
|
|
||||||
@docker compose exec app php artisan migrate
|
|
||||||
@echo ""
|
|
||||||
@echo "✅ Installation complete!"
|
|
||||||
@echo "Access the app at: http://localhost:8000"
|
|
||||||
@echo "Create your first planner and user to get started."
|
|
||||||
295
README.md
295
README.md
|
|
@ -1,295 +0,0 @@
|
||||||
# 🍽️ Dish Planner
|
|
||||||
|
|
||||||
A Laravel-based meal planning application that helps households organize and schedule their dishes among multiple users. Built with Laravel, Livewire, and FrankenPHP for a modern, single-container deployment.
|
|
||||||
|
|
||||||
## ✨ Features
|
|
||||||
|
|
||||||
- **Multi-user dish management** - Assign dishes to specific household members
|
|
||||||
- **Smart scheduling** - Automatic schedule generation with recurrence patterns
|
|
||||||
- **Calendar view** - Visual 31-day schedule overview
|
|
||||||
- **Real-time updates** - Livewire-powered reactive interface
|
|
||||||
- **Dark theme UI** - Modern interface with purple/pink accents
|
|
||||||
- **Single container deployment** - Simplified hosting with FrankenPHP
|
|
||||||
|
|
||||||
## 🚀 Quick Start
|
|
||||||
|
|
||||||
### Prerequisites
|
|
||||||
- Docker and Docker Compose
|
|
||||||
- Make (optional, for convenience commands)
|
|
||||||
|
|
||||||
### First Time Setup
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Clone the repository
|
|
||||||
git clone https://github.com/yourusername/dish-planner.git
|
|
||||||
cd dish-planner
|
|
||||||
|
|
||||||
# Quick install with Make
|
|
||||||
make install
|
|
||||||
|
|
||||||
# Or manually:
|
|
||||||
cp .env.example .env
|
|
||||||
docker compose build
|
|
||||||
docker compose up -d
|
|
||||||
docker compose exec app php artisan key:generate
|
|
||||||
docker compose exec app php artisan migrate
|
|
||||||
```
|
|
||||||
|
|
||||||
The application will be available at **http://localhost:8000**
|
|
||||||
|
|
||||||
## 🔧 Development
|
|
||||||
|
|
||||||
### Starting the Development Environment
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Start all services
|
|
||||||
make dev
|
|
||||||
|
|
||||||
# Or with Docker Compose directly
|
|
||||||
docker compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
**Available services:**
|
|
||||||
- **App**: http://localhost:8000 (Laravel + FrankenPHP)
|
|
||||||
- **Vite**: http://localhost:5173 (Asset hot-reload)
|
|
||||||
- **Mailhog**: http://localhost:8025 (Email testing)
|
|
||||||
- **Database**: localhost:3306 (MariaDB)
|
|
||||||
|
|
||||||
### Common Development Commands
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# View logs
|
|
||||||
make logs # App logs
|
|
||||||
make logs-db # Database logs
|
|
||||||
|
|
||||||
# Laravel commands
|
|
||||||
make artisan cmd="migrate" # Run artisan commands
|
|
||||||
make tinker # Start Laravel tinker
|
|
||||||
make test # Run tests
|
|
||||||
|
|
||||||
# Database
|
|
||||||
make migrate # Run migrations
|
|
||||||
make seed # Seed database
|
|
||||||
make fresh # Fresh migrate with seeds
|
|
||||||
|
|
||||||
# Testing
|
|
||||||
make test # Run tests
|
|
||||||
composer test:coverage-html # Run tests with coverage report (generates coverage/index.html)
|
|
||||||
|
|
||||||
# Utilities
|
|
||||||
make shell # Enter app container
|
|
||||||
make db-shell # Enter database shell
|
|
||||||
make clear # Clear all caches
|
|
||||||
```
|
|
||||||
|
|
||||||
### Project Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
dish-planner/
|
|
||||||
├── app/
|
|
||||||
│ ├── Livewire/ # Livewire components
|
|
||||||
│ │ ├── Auth/ # Authentication
|
|
||||||
│ │ ├── Dishes/ # Dish management
|
|
||||||
│ │ ├── Schedule/ # Schedule calendar
|
|
||||||
│ │ └── Users/ # User management
|
|
||||||
│ └── Models/ # Eloquent models
|
|
||||||
├── resources/
|
|
||||||
│ └── views/
|
|
||||||
│ └── livewire/ # Livewire component views
|
|
||||||
├── docker-compose.yml # Development environment
|
|
||||||
├── docker-compose.prod.yml # Production environment
|
|
||||||
├── Dockerfile # Production image
|
|
||||||
├── Dockerfile.dev # Development image
|
|
||||||
└── Makefile # Convenience commands
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🚢 Production Deployment
|
|
||||||
|
|
||||||
### Building for Production
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Build production image
|
|
||||||
make prod-build
|
|
||||||
|
|
||||||
# Start production environment
|
|
||||||
make prod
|
|
||||||
|
|
||||||
# Or with Docker Compose
|
|
||||||
docker compose -f docker-compose.prod.yml build
|
|
||||||
docker compose -f docker-compose.prod.yml up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
### Production Environment Variables
|
|
||||||
|
|
||||||
Required environment variables for production:
|
|
||||||
|
|
||||||
```env
|
|
||||||
# Required - Generate APP_KEY (see instructions below)
|
|
||||||
APP_KEY=base64:your-generated-key-here
|
|
||||||
APP_URL=https://your-domain.com
|
|
||||||
|
|
||||||
# Database Configuration
|
|
||||||
DB_DATABASE=dishplanner
|
|
||||||
DB_USERNAME=dishplanner
|
|
||||||
DB_PASSWORD=strong-password-here
|
|
||||||
DB_ROOT_PASSWORD=strong-root-password
|
|
||||||
|
|
||||||
# Optional Email Configuration
|
|
||||||
MAIL_HOST=your-smtp-host
|
|
||||||
MAIL_PORT=587
|
|
||||||
MAIL_USERNAME=your-username
|
|
||||||
MAIL_PASSWORD=your-password
|
|
||||||
MAIL_FROM_ADDRESS=noreply@your-domain.com
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Generating APP_KEY
|
|
||||||
|
|
||||||
The APP_KEY is critical for encryption and must be kept consistent across deployments. Generate one using any of these methods:
|
|
||||||
|
|
||||||
**Option 1: Using OpenSSL (Linux/Mac/Windows with Git Bash)**
|
|
||||||
```bash
|
|
||||||
echo "base64:$(openssl rand -base64 32)"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Option 2: Using Node.js (Cross-platform)**
|
|
||||||
```bash
|
|
||||||
node -e "console.log('base64:' + require('crypto').randomBytes(32).toString('base64'))"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Option 3: Using Python (Cross-platform)**
|
|
||||||
```bash
|
|
||||||
python -c "import base64, os; print('base64:' + base64.b64encode(os.urandom(32)).decode())"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Option 4: Online Generator**
|
|
||||||
Generate a random 32-character string at https://randomkeygen.com/ and prepend with `base64:`
|
|
||||||
|
|
||||||
⚠️ **Important**: Save this key securely! If lost, you won't be able to decrypt existing data.
|
|
||||||
|
|
||||||
### Deployment with DockGE
|
|
||||||
|
|
||||||
The production setup is optimized for DockGE deployment with just 2 containers:
|
|
||||||
|
|
||||||
1. **app** - Laravel application with FrankenPHP
|
|
||||||
2. **db** - MariaDB database
|
|
||||||
|
|
||||||
Simply import the `docker-compose.prod.yml` into DockGE and configure your environment variables.
|
|
||||||
|
|
||||||
## 🛠️ Technology Stack
|
|
||||||
|
|
||||||
- **Backend**: Laravel 12 with Livewire 3
|
|
||||||
- **Web Server**: FrankenPHP (PHP 8.3 + Caddy)
|
|
||||||
- **Database**: MariaDB 11
|
|
||||||
- **Frontend**: Blade + Livewire + Alpine.js
|
|
||||||
- **Styling**: TailwindCSS with custom dark theme
|
|
||||||
- **Assets**: Vite for bundling
|
|
||||||
|
|
||||||
## 📦 Docker Architecture
|
|
||||||
|
|
||||||
### Development (`docker-compose.yml`)
|
|
||||||
- **Hot reload** - Volume mounts for live code editing
|
|
||||||
- **Debug tools** - Xdebug configured for debugging
|
|
||||||
- **Email testing** - Mailhog for capturing emails
|
|
||||||
- **Asset watching** - Vite dev server for instant updates
|
|
||||||
|
|
||||||
### Production (`docker-compose.prod.yml`)
|
|
||||||
- **Optimized** - Multi-stage builds with caching
|
|
||||||
- **Secure** - No debug tools, proper permissions
|
|
||||||
- **Health checks** - Automatic container monitoring
|
|
||||||
- **Single container** - FrankenPHP serves everything
|
|
||||||
|
|
||||||
## 🔨 Make Commands Reference
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Development
|
|
||||||
make dev # Start development environment
|
|
||||||
make dev-build # Build and start development
|
|
||||||
make dev-stop # Stop development environment
|
|
||||||
make dev-clean # Stop and remove volumes (CAUTION)
|
|
||||||
|
|
||||||
# Production
|
|
||||||
make prod # Start production environment
|
|
||||||
make prod-build # Build production image
|
|
||||||
make prod-stop # Stop production environment
|
|
||||||
make prod-logs # Show production logs
|
|
||||||
|
|
||||||
# Laravel
|
|
||||||
make artisan cmd="..." # Run artisan command
|
|
||||||
make composer cmd="..." # Run composer command
|
|
||||||
make npm cmd="..." # Run npm command
|
|
||||||
make migrate # Run migrations
|
|
||||||
make seed # Seed database
|
|
||||||
make fresh # Fresh migrate and seed
|
|
||||||
make tinker # Start tinker session
|
|
||||||
make test # Run tests
|
|
||||||
|
|
||||||
# Utilities
|
|
||||||
make shell # Enter app container
|
|
||||||
make db-shell # Enter database shell
|
|
||||||
make logs # Show app logs
|
|
||||||
make logs-db # Show database logs
|
|
||||||
make clear # Clear all caches
|
|
||||||
make optimize # Optimize for production
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🐛 Troubleshooting
|
|
||||||
|
|
||||||
### Container won't start
|
|
||||||
```bash
|
|
||||||
# Check logs
|
|
||||||
docker compose logs app
|
|
||||||
|
|
||||||
# Rebuild containers
|
|
||||||
docker compose build --no-cache
|
|
||||||
docker compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
### Database connection issues
|
|
||||||
```bash
|
|
||||||
# Verify database is running
|
|
||||||
docker compose ps
|
|
||||||
|
|
||||||
# Check database logs
|
|
||||||
docker compose logs db
|
|
||||||
|
|
||||||
# Try manual connection
|
|
||||||
make db-shell
|
|
||||||
```
|
|
||||||
|
|
||||||
### Permission issues
|
|
||||||
```bash
|
|
||||||
# Fix storage permissions
|
|
||||||
docker compose exec app chmod -R 777 storage bootstrap/cache
|
|
||||||
```
|
|
||||||
|
|
||||||
### Clear all caches
|
|
||||||
```bash
|
|
||||||
make clear
|
|
||||||
# Or manually
|
|
||||||
docker compose exec app php artisan cache:clear
|
|
||||||
docker compose exec app php artisan config:clear
|
|
||||||
docker compose exec app php artisan route:clear
|
|
||||||
docker compose exec app php artisan view:clear
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📄 License
|
|
||||||
|
|
||||||
This project is open-source software licensed under the [MIT license](LICENSE.md).
|
|
||||||
|
|
||||||
## 🤝 Contributing
|
|
||||||
|
|
||||||
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
||||||
|
|
||||||
1. Fork the repository
|
|
||||||
2. Create your feature branch (`git checkout -b feature/AmazingFeature`)
|
|
||||||
3. Commit your changes (`git commit -m 'Add some AmazingFeature'`)
|
|
||||||
4. Push to the branch (`git push origin feature/AmazingFeature`)
|
|
||||||
5. Open a Pull Request
|
|
||||||
|
|
||||||
## 📞 Support
|
|
||||||
|
|
||||||
For issues and questions, please use the [GitHub Issues](https://github.com/yourusername/dish-planner/issues) page.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Built with ❤️ using Laravel and Livewire
|
|
||||||
|
|
@ -1,84 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Actions\User;
|
|
||||||
|
|
||||||
use App\Models\User;
|
|
||||||
use Exception;
|
|
||||||
use Illuminate\Support\Facades\DB;
|
|
||||||
use Illuminate\Support\Facades\Log;
|
|
||||||
use InvalidArgumentException;
|
|
||||||
|
|
||||||
class CreateUserAction
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @throws Exception
|
|
||||||
*/
|
|
||||||
public function execute(array $data): User
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
// Validate required fields first
|
|
||||||
if (!isset($data['name']) || empty($data['name'])) {
|
|
||||||
throw new InvalidArgumentException('Name is required');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isset($data['planner_id']) || empty($data['planner_id'])) {
|
|
||||||
throw new InvalidArgumentException('Planner ID is required');
|
|
||||||
}
|
|
||||||
|
|
||||||
DB::beginTransaction();
|
|
||||||
|
|
||||||
Log::info('CreateUserAction: Starting user creation', [
|
|
||||||
'name' => $data['name'],
|
|
||||||
'planner_id' => $data['planner_id'],
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Create the user
|
|
||||||
$user = User::create([
|
|
||||||
'name' => $data['name'],
|
|
||||||
'planner_id' => $data['planner_id'],
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (!$user) {
|
|
||||||
throw new Exception('User creation returned null');
|
|
||||||
}
|
|
||||||
|
|
||||||
Log::info('CreateUserAction: User creation result', [
|
|
||||||
'user_id' => $user->id,
|
|
||||||
'name' => $user->name,
|
|
||||||
'planner_id' => $user->planner_id,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Verify the user was actually created
|
|
||||||
$createdUser = User::find($user->id);
|
|
||||||
if (!$createdUser) {
|
|
||||||
throw new Exception('User creation did not persist to database');
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($createdUser->name !== $data['name']) {
|
|
||||||
throw new Exception('User creation data mismatch');
|
|
||||||
}
|
|
||||||
|
|
||||||
DB::commit();
|
|
||||||
|
|
||||||
Log::info('CreateUserAction: User successfully created', [
|
|
||||||
'user_id' => $user->id,
|
|
||||||
'name' => $user->name,
|
|
||||||
'planner_id' => $user->planner_id,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return $user;
|
|
||||||
|
|
||||||
} catch (Exception $e) {
|
|
||||||
DB::rollBack();
|
|
||||||
|
|
||||||
Log::error('CreateUserAction: User creation failed', [
|
|
||||||
'name' => $data['name'] ?? 'N/A',
|
|
||||||
'planner_id' => $data['planner_id'] ?? 'N/A',
|
|
||||||
'error' => $e->getMessage(),
|
|
||||||
'trace' => $e->getTraceAsString(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
throw $e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,79 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Actions\User;
|
|
||||||
|
|
||||||
use App\Models\User;
|
|
||||||
use Exception;
|
|
||||||
use Illuminate\Support\Facades\DB;
|
|
||||||
use Illuminate\Support\Facades\Log;
|
|
||||||
|
|
||||||
class DeleteUserAction
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @throws Exception
|
|
||||||
*/
|
|
||||||
public function execute(User $user): bool
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
DB::beginTransaction();
|
|
||||||
|
|
||||||
Log::info('DeleteUserAction: Starting user deletion', [
|
|
||||||
'user_id' => $user->id,
|
|
||||||
'user_name' => $user->name,
|
|
||||||
'planner_id' => $user->planner_id,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Check for related data
|
|
||||||
$userDishCount = $user->userDishes()->count();
|
|
||||||
$dishCount = $user->dishes()->count();
|
|
||||||
|
|
||||||
Log::info('DeleteUserAction: User relationship counts', [
|
|
||||||
'user_id' => $user->id,
|
|
||||||
'user_dishes_count' => $userDishCount,
|
|
||||||
'dishes_count' => $dishCount,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Store user info before deletion for verification
|
|
||||||
$userId = $user->id;
|
|
||||||
$userName = $user->name;
|
|
||||||
|
|
||||||
// Delete the user (cascading deletes should handle related records)
|
|
||||||
$result = $user->delete();
|
|
||||||
|
|
||||||
Log::info('DeleteUserAction: Delete result', [
|
|
||||||
'result' => $result,
|
|
||||||
'user_id' => $userId,
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (! $result) {
|
|
||||||
throw new Exception('User deletion returned false');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify the deletion actually happened
|
|
||||||
$stillExists = User::find($userId);
|
|
||||||
if ($stillExists) {
|
|
||||||
throw new Exception('User deletion did not persist to database');
|
|
||||||
}
|
|
||||||
|
|
||||||
DB::commit();
|
|
||||||
|
|
||||||
Log::info('DeleteUserAction: User successfully deleted', [
|
|
||||||
'user_id' => $userId,
|
|
||||||
'user_name' => $userName,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
|
|
||||||
} catch (Exception $e) {
|
|
||||||
DB::rollBack();
|
|
||||||
|
|
||||||
Log::error('DeleteUserAction: User deletion failed', [
|
|
||||||
'user_id' => $user->id,
|
|
||||||
'error' => $e->getMessage(),
|
|
||||||
'trace' => $e->getTraceAsString(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
throw $e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,63 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Actions\User;
|
|
||||||
|
|
||||||
use App\Models\User;
|
|
||||||
use Illuminate\Support\Facades\DB;
|
|
||||||
use Illuminate\Support\Facades\Log;
|
|
||||||
|
|
||||||
class EditUserAction
|
|
||||||
{
|
|
||||||
public function execute(User $user, array $data): bool
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
DB::beginTransaction();
|
|
||||||
|
|
||||||
Log::info('EditUserAction: Starting user update', [
|
|
||||||
'user_id' => $user->id,
|
|
||||||
'old_name' => $user->name,
|
|
||||||
'new_name' => $data['name'],
|
|
||||||
'planner_id' => $user->planner_id,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$result = $user->update([
|
|
||||||
'name' => $data['name'],
|
|
||||||
]);
|
|
||||||
|
|
||||||
Log::info('EditUserAction: Update result', [
|
|
||||||
'result' => $result,
|
|
||||||
'user_id' => $user->id,
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (!$result) {
|
|
||||||
throw new \Exception('User update returned false');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify the update actually happened
|
|
||||||
$user->refresh();
|
|
||||||
if ($user->name !== $data['name']) {
|
|
||||||
throw new \Exception('User update did not persist to database');
|
|
||||||
}
|
|
||||||
|
|
||||||
DB::commit();
|
|
||||||
|
|
||||||
Log::info('EditUserAction: User successfully updated', [
|
|
||||||
'user_id' => $user->id,
|
|
||||||
'updated_name' => $user->name,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
DB::rollBack();
|
|
||||||
|
|
||||||
Log::error('EditUserAction: User update failed', [
|
|
||||||
'user_id' => $user->id,
|
|
||||||
'error' => $e->getMessage(),
|
|
||||||
'trace' => $e->getTraceAsString(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
throw $e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Enums;
|
|
||||||
|
|
||||||
enum AppModeEnum: string
|
|
||||||
{
|
|
||||||
case APP = 'app';
|
|
||||||
case SAAS = 'saas';
|
|
||||||
|
|
||||||
public static function current(): self
|
|
||||||
{
|
|
||||||
return self::from(config('app.mode', 'app'));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function isApp(): bool
|
|
||||||
{
|
|
||||||
return $this === self::APP;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function isSaas(): bool
|
|
||||||
{
|
|
||||||
return $this === self::SAAS;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,44 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Controllers\Auth;
|
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Illuminate\Support\Facades\Auth;
|
|
||||||
use Illuminate\Validation\ValidationException;
|
|
||||||
|
|
||||||
class LoginController extends Controller
|
|
||||||
{
|
|
||||||
public function showLoginForm()
|
|
||||||
{
|
|
||||||
return view('auth.login');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function login(Request $request)
|
|
||||||
{
|
|
||||||
$credentials = $request->validate([
|
|
||||||
'email' => ['required', 'email'],
|
|
||||||
'password' => ['required'],
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (Auth::attempt($credentials, $request->boolean('remember'))) {
|
|
||||||
$request->session()->regenerate();
|
|
||||||
|
|
||||||
return redirect()->intended(route('dashboard'));
|
|
||||||
}
|
|
||||||
|
|
||||||
throw ValidationException::withMessages([
|
|
||||||
'email' => 'These credentials do not match our records.',
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function logout(Request $request)
|
|
||||||
{
|
|
||||||
Auth::logout();
|
|
||||||
|
|
||||||
$request->session()->invalidate();
|
|
||||||
$request->session()->regenerateToken();
|
|
||||||
|
|
||||||
return redirect('/');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Controllers\Auth;
|
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
|
||||||
use App\Models\Planner;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Illuminate\Support\Facades\Auth;
|
|
||||||
use Illuminate\Support\Facades\Hash;
|
|
||||||
use Illuminate\Validation\Rules;
|
|
||||||
|
|
||||||
class RegisterController extends Controller
|
|
||||||
{
|
|
||||||
public function showRegistrationForm()
|
|
||||||
{
|
|
||||||
return view('auth.register');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function register(Request $request)
|
|
||||||
{
|
|
||||||
$request->validate([
|
|
||||||
'name' => ['required', 'string', 'max:255'],
|
|
||||||
'email' => ['required', 'string', 'email', 'max:255', 'unique:planners'],
|
|
||||||
'password' => ['required', 'confirmed', Rules\Password::defaults()],
|
|
||||||
]);
|
|
||||||
|
|
||||||
$user = Planner::create([
|
|
||||||
'name' => $request->name,
|
|
||||||
'email' => $request->email,
|
|
||||||
'password' => Hash::make($request->password),
|
|
||||||
]);
|
|
||||||
|
|
||||||
Auth::login($user);
|
|
||||||
|
|
||||||
return redirect(route('dashboard'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,112 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Controllers;
|
|
||||||
|
|
||||||
use Illuminate\Http\RedirectResponse;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Laravel\Cashier\Cashier;
|
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
|
||||||
|
|
||||||
class SubscriptionController extends Controller
|
|
||||||
{
|
|
||||||
public function checkout(Request $request)
|
|
||||||
{
|
|
||||||
$planner = $request->user();
|
|
||||||
|
|
||||||
if ($planner->subscribed()) {
|
|
||||||
return redirect()->route('dashboard');
|
|
||||||
}
|
|
||||||
|
|
||||||
$plan = $request->input('plan', 'monthly');
|
|
||||||
$priceId = $plan === 'yearly'
|
|
||||||
? config('services.stripe.price_yearly')
|
|
||||||
: config('services.stripe.price_monthly');
|
|
||||||
|
|
||||||
return $planner->newSubscription('default', $priceId)
|
|
||||||
->checkout([
|
|
||||||
'success_url' => route('subscription.success') . '?session_id={CHECKOUT_SESSION_ID}',
|
|
||||||
'cancel_url' => route('subscription.index'),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function success(Request $request): RedirectResponse
|
|
||||||
{
|
|
||||||
$sessionId = $request->query('session_id');
|
|
||||||
|
|
||||||
if ($sessionId) {
|
|
||||||
$planner = $request->user();
|
|
||||||
$session = Cashier::stripe()->checkout->sessions->retrieve($sessionId, [
|
|
||||||
'expand' => ['subscription'],
|
|
||||||
]);
|
|
||||||
|
|
||||||
if ($session->subscription && ! $planner->subscribed()) {
|
|
||||||
$subscription = $session->subscription;
|
|
||||||
|
|
||||||
$planner->subscriptions()->create([
|
|
||||||
'type' => 'default',
|
|
||||||
'stripe_id' => $subscription->id,
|
|
||||||
'stripe_status' => $subscription->status,
|
|
||||||
'stripe_price' => $subscription->items->data[0]->price->id ?? null,
|
|
||||||
'quantity' => $subscription->items->data[0]->quantity ?? 1,
|
|
||||||
'trial_ends_at' => $subscription->trial_end ? now()->setTimestamp($subscription->trial_end) : null,
|
|
||||||
'ends_at' => null,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return redirect()->route('dashboard')->with('success', 'Subscription activated!');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function billing(Request $request)
|
|
||||||
{
|
|
||||||
$planner = $request->user();
|
|
||||||
$subscription = $planner->subscription();
|
|
||||||
|
|
||||||
if (! $subscription) {
|
|
||||||
return redirect()->route('subscription.index');
|
|
||||||
}
|
|
||||||
|
|
||||||
$planType = match ($subscription->stripe_price) {
|
|
||||||
config('services.stripe.price_yearly') => 'Yearly',
|
|
||||||
config('services.stripe.price_monthly') => 'Monthly',
|
|
||||||
default => 'Unknown',
|
|
||||||
};
|
|
||||||
|
|
||||||
$nextBillingDate = null;
|
|
||||||
if ($subscription->stripe_status === 'active') {
|
|
||||||
try {
|
|
||||||
$stripeSubscription = Cashier::stripe()->subscriptions->retrieve($subscription->stripe_id);
|
|
||||||
$nextBillingDate = $stripeSubscription->current_period_end
|
|
||||||
? now()->setTimestamp($stripeSubscription->current_period_end)
|
|
||||||
: null;
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
// Stripe API error - continue without next billing date
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return view('billing.index', [
|
|
||||||
'subscription' => $subscription,
|
|
||||||
'planner' => $planner,
|
|
||||||
'planType' => $planType,
|
|
||||||
'nextBillingDate' => $nextBillingDate,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function cancel(Request $request): RedirectResponse
|
|
||||||
{
|
|
||||||
$planner = $request->user();
|
|
||||||
|
|
||||||
if (! $planner->subscribed()) {
|
|
||||||
return back()->with('error', 'No active subscription found.');
|
|
||||||
}
|
|
||||||
|
|
||||||
$planner->subscription()->cancel();
|
|
||||||
|
|
||||||
return back()->with('success', 'Subscription canceled. Access will continue until the end of your billing period.');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function billingPortal(Request $request)
|
|
||||||
{
|
|
||||||
return $request->user()->redirectToBillingPortal(route('billing'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Middleware;
|
|
||||||
|
|
||||||
use Closure;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
|
||||||
|
|
||||||
class ForceJsonResponse
|
|
||||||
{
|
|
||||||
public function handle(Request $request, Closure $next): Response
|
|
||||||
{
|
|
||||||
// Only force JSON for API routes
|
|
||||||
if ($request->is('api/*')) {
|
|
||||||
$request->headers->set('Accept', 'application/json');
|
|
||||||
}
|
|
||||||
|
|
||||||
return $next($request);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Middleware;
|
|
||||||
|
|
||||||
use Closure;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
|
||||||
|
|
||||||
class RequireSubscription
|
|
||||||
{
|
|
||||||
public function handle(Request $request, Closure $next): Response
|
|
||||||
{
|
|
||||||
if (is_mode_app()) {
|
|
||||||
return $next($request);
|
|
||||||
}
|
|
||||||
|
|
||||||
$planner = $request->user();
|
|
||||||
|
|
||||||
if (! $planner?->subscribed()) {
|
|
||||||
return redirect()->route('subscription.index');
|
|
||||||
}
|
|
||||||
|
|
||||||
return $next($request);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,132 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Livewire\Dishes;
|
|
||||||
|
|
||||||
use App\Models\Dish;
|
|
||||||
use App\Models\User;
|
|
||||||
use Livewire\Component;
|
|
||||||
use Livewire\WithPagination;
|
|
||||||
|
|
||||||
class DishesList extends Component
|
|
||||||
{
|
|
||||||
use WithPagination;
|
|
||||||
|
|
||||||
public $showCreateModal = false;
|
|
||||||
public $showEditModal = false;
|
|
||||||
public $showDeleteModal = false;
|
|
||||||
|
|
||||||
public $editingDish = null;
|
|
||||||
public $deletingDish = null;
|
|
||||||
|
|
||||||
// Form fields
|
|
||||||
public $name = '';
|
|
||||||
public $selectedUsers = [];
|
|
||||||
|
|
||||||
protected $rules = [
|
|
||||||
'name' => 'required|string|max:255',
|
|
||||||
'selectedUsers' => 'array',
|
|
||||||
];
|
|
||||||
|
|
||||||
public function render()
|
|
||||||
{
|
|
||||||
$dishes = Dish::with('users')
|
|
||||||
->orderBy('name')
|
|
||||||
->paginate(10);
|
|
||||||
|
|
||||||
$users = User::where('planner_id', auth()->id())
|
|
||||||
->orderBy('name')
|
|
||||||
->get();
|
|
||||||
|
|
||||||
return view('livewire.dishes.dishes-list', [
|
|
||||||
'dishes' => $dishes,
|
|
||||||
'users' => $users
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function create()
|
|
||||||
{
|
|
||||||
$this->reset(['name', 'selectedUsers']);
|
|
||||||
$this->resetValidation();
|
|
||||||
$this->showCreateModal = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function store()
|
|
||||||
{
|
|
||||||
$this->validate();
|
|
||||||
|
|
||||||
$dish = Dish::create([
|
|
||||||
'name' => $this->name,
|
|
||||||
'planner_id' => auth()->id(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Attach selected users
|
|
||||||
if (!empty($this->selectedUsers)) {
|
|
||||||
$dish->users()->attach($this->selectedUsers);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->showCreateModal = false;
|
|
||||||
$this->reset(['name', 'selectedUsers']);
|
|
||||||
|
|
||||||
session()->flash('success', 'Dish created successfully.');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function edit(Dish $dish)
|
|
||||||
{
|
|
||||||
$this->editingDish = $dish;
|
|
||||||
$this->name = $dish->name;
|
|
||||||
$this->selectedUsers = $dish->users->pluck('id')->toArray();
|
|
||||||
$this->resetValidation();
|
|
||||||
$this->showEditModal = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function update()
|
|
||||||
{
|
|
||||||
$this->validate();
|
|
||||||
|
|
||||||
$this->editingDish->update([
|
|
||||||
'name' => $this->name,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Sync users
|
|
||||||
$this->editingDish->users()->sync($this->selectedUsers);
|
|
||||||
|
|
||||||
$this->showEditModal = false;
|
|
||||||
$this->reset(['name', 'selectedUsers', 'editingDish']);
|
|
||||||
|
|
||||||
session()->flash('success', 'Dish updated successfully.');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function confirmDelete(Dish $dish)
|
|
||||||
{
|
|
||||||
$this->deletingDish = $dish;
|
|
||||||
$this->showDeleteModal = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function delete()
|
|
||||||
{
|
|
||||||
$this->deletingDish->users()->detach();
|
|
||||||
$this->deletingDish->delete();
|
|
||||||
$this->showDeleteModal = false;
|
|
||||||
$this->deletingDish = null;
|
|
||||||
|
|
||||||
session()->flash('success', 'Dish deleted successfully.');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function cancel()
|
|
||||||
{
|
|
||||||
$this->showCreateModal = false;
|
|
||||||
$this->showEditModal = false;
|
|
||||||
$this->showDeleteModal = false;
|
|
||||||
$this->reset(['name', 'selectedUsers', 'editingDish', 'deletingDish']);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function toggleAllUsers(): void
|
|
||||||
{
|
|
||||||
$users = User::where('planner_id', auth()->id())->get();
|
|
||||||
if (count($this->selectedUsers) === $users->count()) {
|
|
||||||
$this->selectedUsers = [];
|
|
||||||
} else {
|
|
||||||
$this->selectedUsers = $users->pluck('id')->map(fn($id) => (string) $id)->toArray();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,433 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Livewire\Schedule;
|
|
||||||
|
|
||||||
use App\Models\Dish;
|
|
||||||
use App\Models\Schedule;
|
|
||||||
use App\Models\ScheduledUserDish;
|
|
||||||
use App\Models\User;
|
|
||||||
use App\Models\UserDish;
|
|
||||||
use Carbon\Carbon;
|
|
||||||
use DishPlanner\Schedule\Services\ScheduleCalendarService;
|
|
||||||
use DishPlanner\ScheduledUserDish\Actions\DeleteScheduledUserDishForDateAction;
|
|
||||||
use DishPlanner\ScheduledUserDish\Actions\SkipScheduledUserDishForDateAction;
|
|
||||||
use Exception;
|
|
||||||
use Illuminate\Contracts\View\View;
|
|
||||||
use Illuminate\Support\Facades\Log;
|
|
||||||
use Livewire\Component;
|
|
||||||
|
|
||||||
class ScheduleCalendar extends Component
|
|
||||||
{
|
|
||||||
public $currentMonth;
|
|
||||||
public $currentYear;
|
|
||||||
public $calendarDays = [];
|
|
||||||
public $showRegenerateModal = false;
|
|
||||||
public $regenerateDate = null;
|
|
||||||
public $regenerateUserId = null;
|
|
||||||
|
|
||||||
// Edit dish modal
|
|
||||||
public $showEditDishModal = false;
|
|
||||||
public $editDate = null;
|
|
||||||
public $editUserId = null;
|
|
||||||
public $selectedDishId = null;
|
|
||||||
public $availableDishes = [];
|
|
||||||
|
|
||||||
// Add dish modal
|
|
||||||
public $showAddDishModal = false;
|
|
||||||
public $addDate = null;
|
|
||||||
public $addUserIds = [];
|
|
||||||
public $addSelectedDishId = null;
|
|
||||||
public $addAvailableUsers = [];
|
|
||||||
public $addAvailableDishes = [];
|
|
||||||
|
|
||||||
public function mount(): void
|
|
||||||
{
|
|
||||||
$this->currentMonth = now()->month;
|
|
||||||
$this->currentYear = now()->year;
|
|
||||||
$this->loadCalendar();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected $listeners = ['schedule-generated' => 'refreshCalendar'];
|
|
||||||
|
|
||||||
public function render(): View
|
|
||||||
{
|
|
||||||
return view('livewire.schedule.schedule-calendar');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function refreshCalendar(): void
|
|
||||||
{
|
|
||||||
$this->loadCalendar();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function loadCalendar(): void
|
|
||||||
{
|
|
||||||
$service = new ScheduleCalendarService();
|
|
||||||
$this->calendarDays = $service->getCalendarDays(
|
|
||||||
auth()->user(),
|
|
||||||
$this->currentMonth,
|
|
||||||
$this->currentYear
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function previousMonth(): void
|
|
||||||
{
|
|
||||||
if ($this->currentMonth === 1) {
|
|
||||||
$this->currentMonth = 12;
|
|
||||||
$this->currentYear--;
|
|
||||||
} else {
|
|
||||||
$this->currentMonth--;
|
|
||||||
}
|
|
||||||
$this->loadCalendar();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function nextMonth(): void
|
|
||||||
{
|
|
||||||
if ($this->currentMonth === 12) {
|
|
||||||
$this->currentMonth = 1;
|
|
||||||
$this->currentYear++;
|
|
||||||
} else {
|
|
||||||
$this->currentMonth++;
|
|
||||||
}
|
|
||||||
$this->loadCalendar();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function regenerateForUserDate($date, $userId): void
|
|
||||||
{
|
|
||||||
if (!$this->authorizeUser($userId)) {
|
|
||||||
session()->flash('error', 'Unauthorized action.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->regenerateDate = $date;
|
|
||||||
$this->regenerateUserId = $userId;
|
|
||||||
$this->showRegenerateModal = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function confirmRegenerate(): void
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
if (!$this->authorizeUser($this->regenerateUserId)) {
|
|
||||||
session()->flash('error', 'Unauthorized action.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$action = new DeleteScheduledUserDishForDateAction();
|
|
||||||
$action->execute(
|
|
||||||
auth()->user(),
|
|
||||||
Carbon::parse($this->regenerateDate),
|
|
||||||
$this->regenerateUserId
|
|
||||||
);
|
|
||||||
|
|
||||||
$this->showRegenerateModal = false;
|
|
||||||
$this->loadCalendar();
|
|
||||||
|
|
||||||
session()->flash('success', 'Schedule regenerated for the selected date!');
|
|
||||||
} catch (Exception $e) {
|
|
||||||
Log::error('Schedule regeneration failed', ['exception' => $e, 'date' => $this->regenerateDate]);
|
|
||||||
session()->flash('error', 'Unable to regenerate schedule. Please try again.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function skipDay($date, $userId): void
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
if (!$this->authorizeUser($userId)) {
|
|
||||||
session()->flash('error', 'Unauthorized action.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$action = new SkipScheduledUserDishForDateAction();
|
|
||||||
$action->execute(
|
|
||||||
auth()->user(),
|
|
||||||
Carbon::parse($date),
|
|
||||||
$userId
|
|
||||||
);
|
|
||||||
|
|
||||||
$this->loadCalendar();
|
|
||||||
|
|
||||||
session()->flash('success', 'Day skipped successfully!');
|
|
||||||
} catch (Exception $e) {
|
|
||||||
Log::error('Skip day failed', ['exception' => $e, 'date' => $date, 'userId' => $userId]);
|
|
||||||
session()->flash('error', 'Unable to skip day. Please try again.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private function authorizeUser(int $userId): bool
|
|
||||||
{
|
|
||||||
$user = User::find($userId);
|
|
||||||
return $user && $user->planner_id === auth()->id();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function cancel(): void
|
|
||||||
{
|
|
||||||
$this->showRegenerateModal = false;
|
|
||||||
$this->regenerateDate = null;
|
|
||||||
$this->regenerateUserId = null;
|
|
||||||
$this->showEditDishModal = false;
|
|
||||||
$this->editDate = null;
|
|
||||||
$this->editUserId = null;
|
|
||||||
$this->selectedDishId = null;
|
|
||||||
$this->availableDishes = [];
|
|
||||||
$this->showAddDishModal = false;
|
|
||||||
$this->addDate = null;
|
|
||||||
$this->addUserIds = [];
|
|
||||||
$this->addSelectedDishId = null;
|
|
||||||
$this->addAvailableUsers = [];
|
|
||||||
$this->addAvailableDishes = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function removeDish($date, $userId): void
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
if (!$this->authorizeUser($userId)) {
|
|
||||||
session()->flash('error', 'Unauthorized action.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$schedule = Schedule::where('planner_id', auth()->id())
|
|
||||||
->where('date', $date)
|
|
||||||
->first();
|
|
||||||
|
|
||||||
if ($schedule) {
|
|
||||||
ScheduledUserDish::where('schedule_id', $schedule->id)
|
|
||||||
->where('user_id', $userId)
|
|
||||||
->delete();
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->loadCalendar();
|
|
||||||
session()->flash('success', 'Dish removed successfully!');
|
|
||||||
} catch (Exception $e) {
|
|
||||||
Log::error('Remove dish failed', ['exception' => $e, 'date' => $date, 'userId' => $userId]);
|
|
||||||
session()->flash('error', 'Unable to remove dish. Please try again.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function openAddDishModal($date): void
|
|
||||||
{
|
|
||||||
$this->addDate = $date;
|
|
||||||
|
|
||||||
// Load all users for this planner
|
|
||||||
$this->addAvailableUsers = User::where('planner_id', auth()->id())
|
|
||||||
->orderBy('name')
|
|
||||||
->get();
|
|
||||||
|
|
||||||
$this->addAvailableDishes = [];
|
|
||||||
$this->addUserIds = [];
|
|
||||||
$this->addSelectedDishId = null;
|
|
||||||
|
|
||||||
$this->showAddDishModal = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function toggleAllUsers(): void
|
|
||||||
{
|
|
||||||
if (count($this->addUserIds) === count($this->addAvailableUsers)) {
|
|
||||||
$this->addUserIds = [];
|
|
||||||
} else {
|
|
||||||
$this->addUserIds = $this->addAvailableUsers->pluck('id')->map(fn($id) => (string) $id)->toArray();
|
|
||||||
}
|
|
||||||
$this->updateAvailableDishes();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function updatedAddUserIds(): void
|
|
||||||
{
|
|
||||||
$this->updateAvailableDishes();
|
|
||||||
}
|
|
||||||
|
|
||||||
private function updateAvailableDishes(): void
|
|
||||||
{
|
|
||||||
if (empty($this->addUserIds)) {
|
|
||||||
$this->addAvailableDishes = [];
|
|
||||||
} else {
|
|
||||||
// Load dishes that ALL selected users have in common
|
|
||||||
$selectedCount = count($this->addUserIds);
|
|
||||||
$this->addAvailableDishes = Dish::whereHas('users', function ($query) {
|
|
||||||
$query->whereIn('users.id', $this->addUserIds);
|
|
||||||
}, '=', $selectedCount)->orderBy('name')->get();
|
|
||||||
}
|
|
||||||
$this->addSelectedDishId = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function saveAddDish(): void
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
if (empty($this->addUserIds)) {
|
|
||||||
session()->flash('error', 'Please select at least one user.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$this->addSelectedDishId) {
|
|
||||||
session()->flash('error', 'Please select a dish.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find or create the schedule for this date
|
|
||||||
$schedule = Schedule::firstOrCreate(
|
|
||||||
[
|
|
||||||
'planner_id' => auth()->id(),
|
|
||||||
'date' => $this->addDate,
|
|
||||||
],
|
|
||||||
['is_skipped' => false]
|
|
||||||
);
|
|
||||||
|
|
||||||
$addedCount = 0;
|
|
||||||
$skippedCount = 0;
|
|
||||||
|
|
||||||
foreach ($this->addUserIds as $userId) {
|
|
||||||
if (!$this->authorizeUser((int) $userId)) {
|
|
||||||
$skippedCount++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if user already has a dish scheduled for this date
|
|
||||||
$existing = ScheduledUserDish::where('schedule_id', $schedule->id)
|
|
||||||
->where('user_id', $userId)
|
|
||||||
->first();
|
|
||||||
|
|
||||||
if ($existing) {
|
|
||||||
$skippedCount++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the UserDish for this user and dish
|
|
||||||
$userDish = UserDish::where('user_id', $userId)
|
|
||||||
->where('dish_id', $this->addSelectedDishId)
|
|
||||||
->first();
|
|
||||||
|
|
||||||
if (!$userDish) {
|
|
||||||
$skippedCount++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the scheduled user dish
|
|
||||||
ScheduledUserDish::create([
|
|
||||||
'schedule_id' => $schedule->id,
|
|
||||||
'user_id' => $userId,
|
|
||||||
'user_dish_id' => $userDish->id,
|
|
||||||
'is_skipped' => false,
|
|
||||||
]);
|
|
||||||
$addedCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->closeAddDishModal();
|
|
||||||
$this->loadCalendar();
|
|
||||||
|
|
||||||
if ($addedCount > 0 && $skippedCount > 0) {
|
|
||||||
session()->flash('success', "Dish added for {$addedCount} user(s). {$skippedCount} user(s) skipped (already scheduled).");
|
|
||||||
} elseif ($addedCount > 0) {
|
|
||||||
session()->flash('success', "Dish added for {$addedCount} user(s)!");
|
|
||||||
} else {
|
|
||||||
session()->flash('error', 'No users could be scheduled. They may already have dishes for this date.');
|
|
||||||
}
|
|
||||||
} catch (Exception $e) {
|
|
||||||
Log::error('Add dish failed', ['exception' => $e]);
|
|
||||||
session()->flash('error', 'Unable to add dish. Please try again.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private function closeAddDishModal(): void
|
|
||||||
{
|
|
||||||
$this->showAddDishModal = false;
|
|
||||||
$this->addDate = null;
|
|
||||||
$this->addUserIds = [];
|
|
||||||
$this->addSelectedDishId = null;
|
|
||||||
$this->addAvailableUsers = [];
|
|
||||||
$this->addAvailableDishes = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function editDish($date, $userId): void
|
|
||||||
{
|
|
||||||
if (!$this->authorizeUser($userId)) {
|
|
||||||
session()->flash('error', 'Unauthorized action.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->editDate = $date;
|
|
||||||
$this->editUserId = $userId;
|
|
||||||
|
|
||||||
// Load dishes available for this user (via UserDish pivot)
|
|
||||||
$this->availableDishes = Dish::whereHas('users', function ($query) use ($userId) {
|
|
||||||
$query->where('users.id', $userId);
|
|
||||||
})->orderBy('name')->get();
|
|
||||||
|
|
||||||
// Get currently selected dish for this date/user if exists
|
|
||||||
$schedule = Schedule::where('planner_id', auth()->id())
|
|
||||||
->where('date', $date)
|
|
||||||
->first();
|
|
||||||
|
|
||||||
if ($schedule) {
|
|
||||||
$scheduledUserDish = ScheduledUserDish::where('schedule_id', $schedule->id)
|
|
||||||
->where('user_id', $userId)
|
|
||||||
->first();
|
|
||||||
|
|
||||||
if ($scheduledUserDish && $scheduledUserDish->userDish) {
|
|
||||||
$this->selectedDishId = $scheduledUserDish->userDish->dish_id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->showEditDishModal = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function saveDish(): void
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
if (!$this->authorizeUser($this->editUserId)) {
|
|
||||||
session()->flash('error', 'Unauthorized action.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$this->selectedDishId) {
|
|
||||||
session()->flash('error', 'Please select a dish.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find or create the schedule for this date
|
|
||||||
$schedule = Schedule::firstOrCreate(
|
|
||||||
[
|
|
||||||
'planner_id' => auth()->id(),
|
|
||||||
'date' => $this->editDate,
|
|
||||||
],
|
|
||||||
['is_skipped' => false]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Find the UserDish for this user and dish
|
|
||||||
$userDish = UserDish::where('user_id', $this->editUserId)
|
|
||||||
->where('dish_id', $this->selectedDishId)
|
|
||||||
->first();
|
|
||||||
|
|
||||||
if (!$userDish) {
|
|
||||||
session()->flash('error', 'This dish is not assigned to this user.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update or create the scheduled user dish
|
|
||||||
ScheduledUserDish::updateOrCreate(
|
|
||||||
[
|
|
||||||
'schedule_id' => $schedule->id,
|
|
||||||
'user_id' => $this->editUserId,
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'user_dish_id' => $userDish->id,
|
|
||||||
'is_skipped' => false,
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
$this->showEditDishModal = false;
|
|
||||||
$this->editDate = null;
|
|
||||||
$this->editUserId = null;
|
|
||||||
$this->selectedDishId = null;
|
|
||||||
$this->availableDishes = [];
|
|
||||||
|
|
||||||
$this->loadCalendar();
|
|
||||||
session()->flash('success', 'Dish updated successfully!');
|
|
||||||
} catch (Exception $e) {
|
|
||||||
Log::error('Save dish failed', ['exception' => $e]);
|
|
||||||
session()->flash('error', 'Unable to save dish. Please try again.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getMonthNameProperty(): string
|
|
||||||
{
|
|
||||||
$service = new ScheduleCalendarService();
|
|
||||||
return $service->getMonthName($this->currentMonth, $this->currentYear);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,141 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Livewire\Schedule;
|
|
||||||
|
|
||||||
use App\Models\User;
|
|
||||||
use Carbon\Carbon;
|
|
||||||
use DishPlanner\Schedule\Actions\ClearScheduleForMonthAction;
|
|
||||||
use DishPlanner\Schedule\Actions\GenerateScheduleForMonthAction;
|
|
||||||
use DishPlanner\Schedule\Actions\RegenerateScheduleForDateForUsersAction;
|
|
||||||
use Illuminate\Support\Facades\Log;
|
|
||||||
use Livewire\Component;
|
|
||||||
|
|
||||||
class ScheduleGenerator extends Component
|
|
||||||
{
|
|
||||||
private const YEARS_IN_PAST = 1;
|
|
||||||
private const YEARS_IN_FUTURE = 5;
|
|
||||||
|
|
||||||
public $selectedMonth;
|
|
||||||
public $selectedYear;
|
|
||||||
public $selectedUsers = [];
|
|
||||||
public $clearExisting = true;
|
|
||||||
public $showAdvancedOptions = false;
|
|
||||||
public $isGenerating = false;
|
|
||||||
|
|
||||||
public function mount(): void
|
|
||||||
{
|
|
||||||
$this->selectedMonth = now()->month;
|
|
||||||
$this->selectedYear = now()->year;
|
|
||||||
|
|
||||||
$this->selectedUsers = User::where('planner_id', auth()->id())
|
|
||||||
->pluck('id')
|
|
||||||
->toArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function render(): \Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View
|
|
||||||
{
|
|
||||||
$users = User::where('planner_id', auth()->id())
|
|
||||||
->orderBy('name')
|
|
||||||
->get();
|
|
||||||
|
|
||||||
$years = range(now()->year - self::YEARS_IN_PAST, now()->year + self::YEARS_IN_FUTURE);
|
|
||||||
|
|
||||||
return view('livewire.schedule.schedule-generator', [
|
|
||||||
'users' => $users,
|
|
||||||
'months' => $this->getMonthNames(),
|
|
||||||
'years' => $years
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function generate(): void
|
|
||||||
{
|
|
||||||
$this->validate([
|
|
||||||
'selectedUsers' => 'required|array|min:1',
|
|
||||||
'selectedMonth' => 'required|integer|min:1|max:12',
|
|
||||||
'selectedYear' => 'required|integer|min:' . (now()->year - self::YEARS_IN_PAST) . '|max:' . (now()->year + self::YEARS_IN_FUTURE),
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->isGenerating = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
$action = new GenerateScheduleForMonthAction();
|
|
||||||
$action->execute(
|
|
||||||
auth()->user(),
|
|
||||||
$this->selectedMonth,
|
|
||||||
$this->selectedYear,
|
|
||||||
$this->selectedUsers,
|
|
||||||
$this->clearExisting
|
|
||||||
);
|
|
||||||
|
|
||||||
$this->isGenerating = false;
|
|
||||||
$this->dispatch('schedule-generated');
|
|
||||||
|
|
||||||
session()->flash('success', 'Schedule generated successfully for ' .
|
|
||||||
$this->getSelectedMonthName() . ' ' . $this->selectedYear);
|
|
||||||
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
$this->isGenerating = false;
|
|
||||||
Log::error('Schedule generation failed', ['exception' => $e]);
|
|
||||||
session()->flash('error', 'Unable to generate schedule. Please try again.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function regenerateForDate($date): void
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
$action = new RegenerateScheduleForDateForUsersAction();
|
|
||||||
$action->execute(
|
|
||||||
auth()->user(),
|
|
||||||
Carbon::parse($date),
|
|
||||||
$this->selectedUsers
|
|
||||||
);
|
|
||||||
|
|
||||||
$this->dispatch('schedule-generated');
|
|
||||||
session()->flash('success', 'Schedule regenerated for ' . Carbon::parse($date)->format('M d, Y'));
|
|
||||||
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
Log::error('Schedule regeneration failed', ['exception' => $e, 'date' => $date]);
|
|
||||||
session()->flash('error', 'Unable to regenerate schedule. Please try again.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function clearMonth(): void
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
$action = new ClearScheduleForMonthAction();
|
|
||||||
$action->execute(
|
|
||||||
auth()->user(),
|
|
||||||
$this->selectedMonth,
|
|
||||||
$this->selectedYear,
|
|
||||||
$this->selectedUsers
|
|
||||||
);
|
|
||||||
|
|
||||||
$this->dispatch('schedule-generated');
|
|
||||||
session()->flash('success', 'Schedule cleared for ' .
|
|
||||||
$this->getSelectedMonthName() . ' ' . $this->selectedYear);
|
|
||||||
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
Log::error('Clear month failed', ['exception' => $e]);
|
|
||||||
session()->flash('error', 'Unable to clear schedule. Please try again.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function toggleAdvancedOptions()
|
|
||||||
{
|
|
||||||
$this->showAdvancedOptions = !$this->showAdvancedOptions;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function getMonthNames(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
1 => 'January', 2 => 'February', 3 => 'March', 4 => 'April',
|
|
||||||
5 => 'May', 6 => 'June', 7 => 'July', 8 => 'August',
|
|
||||||
9 => 'September', 10 => 'October', 11 => 'November', 12 => 'December'
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
private function getSelectedMonthName(): string
|
|
||||||
{
|
|
||||||
return $this->getMonthNames()[$this->selectedMonth];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,126 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Livewire\Users;
|
|
||||||
|
|
||||||
use App\Models\User;
|
|
||||||
use App\Actions\User\CreateUserAction;
|
|
||||||
use App\Actions\User\DeleteUserAction;
|
|
||||||
use App\Actions\User\EditUserAction;
|
|
||||||
use Exception;
|
|
||||||
use Illuminate\Contracts\View\View;
|
|
||||||
use Livewire\Component;
|
|
||||||
use Livewire\WithPagination;
|
|
||||||
|
|
||||||
class UsersList extends Component
|
|
||||||
{
|
|
||||||
use WithPagination;
|
|
||||||
|
|
||||||
public bool $showCreateModal = false;
|
|
||||||
public bool $showEditModal = false;
|
|
||||||
public bool $showDeleteModal = false;
|
|
||||||
|
|
||||||
public ?User $editingUser = null;
|
|
||||||
public ?User $deletingUser = null;
|
|
||||||
|
|
||||||
// Form fields
|
|
||||||
public string $name = '';
|
|
||||||
|
|
||||||
protected array $rules = [
|
|
||||||
'name' => 'required|string|max:255',
|
|
||||||
];
|
|
||||||
|
|
||||||
public function render(): View
|
|
||||||
{
|
|
||||||
$users = User::where('planner_id', auth()->id())
|
|
||||||
->orderBy('name')
|
|
||||||
->paginate(10);
|
|
||||||
|
|
||||||
return view('livewire.users.users-list', [
|
|
||||||
'users' => $users
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function create(): void
|
|
||||||
{
|
|
||||||
$this->reset(['name']);
|
|
||||||
$this->resetValidation();
|
|
||||||
$this->showCreateModal = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function store(): void
|
|
||||||
{
|
|
||||||
$this->validate();
|
|
||||||
|
|
||||||
try {
|
|
||||||
(new CreateUserAction())->execute([
|
|
||||||
'name' => $this->name,
|
|
||||||
'planner_id' => auth()->id(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->showCreateModal = false;
|
|
||||||
$this->reset(['name']);
|
|
||||||
|
|
||||||
session()->flash('success', 'User created successfully.');
|
|
||||||
} catch (Exception $e) {
|
|
||||||
session()->flash('error', 'Failed to create user: ' . $e->getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function edit(User $user): void
|
|
||||||
{
|
|
||||||
$this->editingUser = $user;
|
|
||||||
$this->name = $user->name;
|
|
||||||
$this->resetValidation();
|
|
||||||
$this->showEditModal = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function update(): void
|
|
||||||
{
|
|
||||||
$this->validate();
|
|
||||||
|
|
||||||
try {
|
|
||||||
(new EditUserAction())->execute($this->editingUser, ['name' => $this->name]);
|
|
||||||
|
|
||||||
$this->showEditModal = false;
|
|
||||||
$this->reset(['name', 'editingUser']);
|
|
||||||
|
|
||||||
session()->flash('success', 'User updated successfully.');
|
|
||||||
|
|
||||||
// Force component to re-render with fresh data
|
|
||||||
$this->resetPage();
|
|
||||||
} catch (Exception $e) {
|
|
||||||
session()->flash('error', 'Failed to update user: ' . $e->getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function confirmDelete(User $user): void
|
|
||||||
{
|
|
||||||
$this->deletingUser = $user;
|
|
||||||
$this->showDeleteModal = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function delete(): void
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
(new DeleteUserAction())->execute($this->deletingUser);
|
|
||||||
|
|
||||||
$this->showDeleteModal = false;
|
|
||||||
$this->deletingUser = null;
|
|
||||||
|
|
||||||
session()->flash('success', 'User deleted successfully.');
|
|
||||||
|
|
||||||
// Force component to re-render with fresh data
|
|
||||||
$this->resetPage();
|
|
||||||
} catch (Exception $e) {
|
|
||||||
session()->flash('error', 'Failed to delete user: ' . $e->getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function cancel(): void
|
|
||||||
{
|
|
||||||
$this->showCreateModal = false;
|
|
||||||
$this->showEditModal = false;
|
|
||||||
$this->showDeleteModal = false;
|
|
||||||
$this->reset(['name', 'editingUser', 'deletingUser']);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
use App\Enums\AppModeEnum;
|
|
||||||
|
|
||||||
if (! function_exists('is_mode_app')) {
|
|
||||||
function is_mode_app(): bool
|
|
||||||
{
|
|
||||||
return AppModeEnum::current()->isApp();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! function_exists('is_mode_saas')) {
|
|
||||||
function is_mode_saas(): bool
|
|
||||||
{
|
|
||||||
return AppModeEnum::current()->isSaas();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
71
backend/.env.example
Normal file
71
backend/.env.example
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
APP_NAME=DishPlanner
|
||||||
|
APP_ENV=local
|
||||||
|
APP_KEY=base64:Z3WnYIG9I6xxft15P1EO31WHinj1R36eM/iN3ouyFBM=
|
||||||
|
APP_DEBUG=true
|
||||||
|
APP_TIMEZONE=UTC
|
||||||
|
APP_URL=http://localhost:8000
|
||||||
|
|
||||||
|
SANCTUM_STATEFUL_DOMAINS=localhost:3000
|
||||||
|
SESSION_DOMAIN=localhost
|
||||||
|
|
||||||
|
APP_LOCALE=en
|
||||||
|
APP_FALLBACK_LOCALE=en
|
||||||
|
APP_FAKER_LOCALE=en_US
|
||||||
|
|
||||||
|
APP_MAINTENANCE_DRIVER=file
|
||||||
|
# APP_MAINTENANCE_STORE=database
|
||||||
|
|
||||||
|
PHP_CLI_SERVER_WORKERS=4
|
||||||
|
|
||||||
|
BCRYPT_ROUNDS=12
|
||||||
|
|
||||||
|
LOG_CHANNEL=stack
|
||||||
|
LOG_STACK=single
|
||||||
|
LOG_DEPRECATIONS_CHANNEL=null
|
||||||
|
LOG_LEVEL=debug
|
||||||
|
|
||||||
|
WWWGROUP=1000
|
||||||
|
|
||||||
|
DB_CONNECTION=mysql
|
||||||
|
DB_HOST=mysql
|
||||||
|
DB_PORT=3306
|
||||||
|
DB_DATABASE=dishplanner
|
||||||
|
DB_USERNAME=dpuser
|
||||||
|
DB_PASSWORD=dppass
|
||||||
|
|
||||||
|
SESSION_DRIVER=database
|
||||||
|
SESSION_LIFETIME=120
|
||||||
|
SESSION_ENCRYPT=false
|
||||||
|
SESSION_PATH=/
|
||||||
|
SESSION_DOMAIN=null
|
||||||
|
|
||||||
|
BROADCAST_CONNECTION=log
|
||||||
|
FILESYSTEM_DISK=local
|
||||||
|
QUEUE_CONNECTION=database
|
||||||
|
|
||||||
|
CACHE_STORE=file
|
||||||
|
CACHE_PREFIX=dishplanner
|
||||||
|
|
||||||
|
MEMCACHED_HOST=127.0.0.1
|
||||||
|
|
||||||
|
REDIS_CLIENT=phpredis
|
||||||
|
REDIS_HOST=127.0.0.1
|
||||||
|
REDIS_PASSWORD=null
|
||||||
|
REDIS_PORT=6379
|
||||||
|
|
||||||
|
MAIL_MAILER=log
|
||||||
|
MAIL_SCHEME=null
|
||||||
|
MAIL_HOST=127.0.0.1
|
||||||
|
MAIL_PORT=2525
|
||||||
|
MAIL_USERNAME=null
|
||||||
|
MAIL_PASSWORD=null
|
||||||
|
MAIL_FROM_ADDRESS="hello@example.com"
|
||||||
|
MAIL_FROM_NAME="${APP_NAME}"
|
||||||
|
|
||||||
|
AWS_ACCESS_KEY_ID=
|
||||||
|
AWS_SECRET_ACCESS_KEY=
|
||||||
|
AWS_DEFAULT_REGION=us-east-1
|
||||||
|
AWS_BUCKET=
|
||||||
|
AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||||
|
|
||||||
|
VITE_APP_NAME="${APP_NAME}"
|
||||||
0
.gitattributes → backend/.gitattributes
vendored
0
.gitattributes → backend/.gitattributes
vendored
25
backend/.gitignore
vendored
Normal file
25
backend/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
/composer.lock
|
||||||
|
/.composer
|
||||||
|
/.phpunit.cache
|
||||||
|
/node_modules
|
||||||
|
/public/build
|
||||||
|
/public/hot
|
||||||
|
/public/storage
|
||||||
|
/storage/*.key
|
||||||
|
/storage/pail
|
||||||
|
/vendor
|
||||||
|
.env
|
||||||
|
.env.backup
|
||||||
|
.env.production
|
||||||
|
.phpactor.json
|
||||||
|
.phpunit.result.cache
|
||||||
|
Homestead.json
|
||||||
|
Homestead.yaml
|
||||||
|
npm-debug.log
|
||||||
|
yarn-error.log
|
||||||
|
/auth.json
|
||||||
|
/.fleet
|
||||||
|
/.idea
|
||||||
|
/.nova
|
||||||
|
/.vscode
|
||||||
|
/.zed
|
||||||
35
backend/Dockerfile
Normal file
35
backend/Dockerfile
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
# Use official PHP base with required extensions
|
||||||
|
FROM php:8.2-fpm
|
||||||
|
|
||||||
|
# Install system dependencies & PHP extensions
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
git unzip curl libzip-dev libpng-dev libonig-dev libxml2-dev zip \
|
||||||
|
&& docker-php-ext-install pdo pdo_mysql zip mbstring exif pcntl bcmath
|
||||||
|
|
||||||
|
# Install Composer globally
|
||||||
|
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
|
||||||
|
|
||||||
|
# Set working dir
|
||||||
|
WORKDIR /var/www
|
||||||
|
|
||||||
|
# Copy app files
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Install PHP dependencies
|
||||||
|
RUN composer install --no-dev --optimize-autoloader
|
||||||
|
|
||||||
|
# Laravel optimizations
|
||||||
|
RUN php artisan config:cache \
|
||||||
|
&& php artisan route:cache \
|
||||||
|
&& php artisan view:cache
|
||||||
|
|
||||||
|
# Set correct permissions
|
||||||
|
RUN chown -R www-data:www-data /var/www \
|
||||||
|
&& chmod -R 755 /var/www/storage
|
||||||
|
|
||||||
|
USER www-data
|
||||||
|
|
||||||
|
# Expose port 9000 (default for php-fpm)
|
||||||
|
EXPOSE 9000
|
||||||
|
|
||||||
|
CMD ["php-fpm"]
|
||||||
14
backend/README.md
Normal file
14
backend/README.md
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
# Dish Planner
|
||||||
|
|
||||||
|
Plan your future dishes
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
This project uses Laravel Sail, so install all composer packages, get your docker up, and run
|
||||||
|
|
||||||
|
```shell
|
||||||
|
./vendor/bin/sail up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
|
@ -6,14 +6,12 @@
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
class RequireSaasMode
|
class ForceJsonResponse
|
||||||
{
|
{
|
||||||
public function handle(Request $request, Closure $next): Response
|
public function handle(Request $request, Closure $next): Response
|
||||||
{
|
{
|
||||||
if (! is_mode_saas()) {
|
$request->headers->set('Accept', 'application/json');
|
||||||
abort(404);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $next($request);
|
return $next($request);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -6,17 +6,15 @@
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||||
use Illuminate\Notifications\Notifiable;
|
use Illuminate\Notifications\Notifiable;
|
||||||
use Laravel\Cashier\Billable;
|
|
||||||
use Laravel\Sanctum\HasApiTokens;
|
use Laravel\Sanctum\HasApiTokens;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @property int $id
|
* @property int $id
|
||||||
* @property static PlannerFactory factory($count = null, $state = [])
|
* @property static PlannerFactory factory($count = null, $state = [])
|
||||||
* @method static first()
|
|
||||||
*/
|
*/
|
||||||
class Planner extends Authenticatable
|
class Planner extends Authenticatable
|
||||||
{
|
{
|
||||||
use Billable, HasApiTokens, HasFactory, Notifiable;
|
use HasApiTokens, HasFactory, Notifiable;
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'name', 'email', 'password',
|
'name', 'email', 'password',
|
||||||
|
|
@ -26,10 +24,6 @@ class Planner extends Authenticatable
|
||||||
'password', 'remember_token',
|
'password', 'remember_token',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $casts = [
|
|
||||||
'password' => 'hashed',
|
|
||||||
];
|
|
||||||
|
|
||||||
public function schedules(): HasMany
|
public function schedules(): HasMany
|
||||||
{
|
{
|
||||||
return $this->hasMany(Schedule::class);
|
return $this->hasMany(Schedule::class);
|
||||||
|
|
@ -27,7 +27,6 @@
|
||||||
* @method static create(array $array)
|
* @method static create(array $array)
|
||||||
* @method static Builder where(array|Closure|Expression|string $column, mixed $operator = null, mixed $value = null, string $boolean = 'and')
|
* @method static Builder where(array|Closure|Expression|string $column, mixed $operator = null, mixed $value = null, string $boolean = 'and')
|
||||||
* @method static ScheduleFactory factory($count = null, $state = [])
|
* @method static ScheduleFactory factory($count = null, $state = [])
|
||||||
* @method static firstOrCreate(array $array, false[] $array1)
|
|
||||||
*/
|
*/
|
||||||
class Schedule extends Model
|
class Schedule extends Model
|
||||||
{
|
{
|
||||||
|
|
@ -57,6 +56,6 @@ public function scheduledUserDishes(): HasMany
|
||||||
|
|
||||||
public function hasAllUsersScheduled(): bool
|
public function hasAllUsersScheduled(): bool
|
||||||
{
|
{
|
||||||
return $this->scheduledUserDishes->count() === User::where('planner_id', $this->planner_id)->count();
|
return $this->scheduledUserDishes->count() === User::all()->count();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -12,25 +12,17 @@
|
||||||
* @property int $id
|
* @property int $id
|
||||||
* @property int $schedule_id
|
* @property int $schedule_id
|
||||||
* @property Schedule $schedule
|
* @property Schedule $schedule
|
||||||
* @property int $user_id
|
|
||||||
* @property User $user
|
|
||||||
* @property int $user_dish_id
|
* @property int $user_dish_id
|
||||||
* @property UserDish $userDish
|
* @property UserDish $userDish
|
||||||
* @property bool $is_skipped
|
* @property bool $is_skipped
|
||||||
* @method static create(array $array)
|
* @method static create(array $array)
|
||||||
* @method static ScheduledUserDishFactory factory($count = null, $state = [])
|
* @method static ScheduledUserDishFactory factory($count = null, $state = [])
|
||||||
* @method static firstOrCreate(array $array, array $array1)
|
|
||||||
*/
|
*/
|
||||||
class ScheduledUserDish extends Model
|
class ScheduledUserDish extends Model
|
||||||
{
|
{
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = ['schedule_id', 'user_id', 'user_dish_id', 'is_skipped'];
|
||||||
'schedule_id',
|
|
||||||
'user_id',
|
|
||||||
'user_dish_id',
|
|
||||||
'is_skipped'
|
|
||||||
];
|
|
||||||
|
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
'is_skipped' => 'boolean',
|
'is_skipped' => 'boolean',
|
||||||
|
|
@ -9,7 +9,8 @@
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
|
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||||
|
use Illuminate\Notifications\Notifiable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @property int $id
|
* @property int $id
|
||||||
|
|
@ -19,17 +20,22 @@
|
||||||
* @property Collection<UserDish> $userDishes
|
* @property Collection<UserDish> $userDishes
|
||||||
* @method static User findOrFail(int $user_id)
|
* @method static User findOrFail(int $user_id)
|
||||||
* @method static UserFactory factory($count = null, $state = [])
|
* @method static UserFactory factory($count = null, $state = [])
|
||||||
* @method static create(array $array)
|
|
||||||
* @method static where(string $string, int|string|null $id)
|
|
||||||
*/
|
*/
|
||||||
class User extends Model
|
class User extends Authenticatable
|
||||||
{
|
{
|
||||||
/** @use HasFactory<UserFactory> */
|
/** @use HasFactory<UserFactory> */
|
||||||
use HasFactory;
|
use HasFactory, Notifiable;
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'planner_id',
|
'planner_id',
|
||||||
'name',
|
'name',
|
||||||
|
'email',
|
||||||
|
'password',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $hidden = [
|
||||||
|
'password',
|
||||||
|
'remember_token',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected static function booted(): void
|
protected static function booted(): void
|
||||||
|
|
@ -37,6 +43,19 @@ protected static function booted(): void
|
||||||
static::addGlobalScope(new BelongsToPlanner);
|
static::addGlobalScope(new BelongsToPlanner);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the attributes that should be cast.
|
||||||
|
*
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'email_verified_at' => 'datetime',
|
||||||
|
'password' => 'hashed',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
public function dishes(): BelongsToMany
|
public function dishes(): BelongsToMany
|
||||||
{
|
{
|
||||||
return $this->belongsToMany(Dish::class, 'user_dishes', 'user_id', 'dish_id');
|
return $this->belongsToMany(Dish::class, 'user_dishes', 'user_id', 'dish_id');
|
||||||
|
|
@ -4,12 +4,10 @@
|
||||||
|
|
||||||
use App\Exceptions\CustomException;
|
use App\Exceptions\CustomException;
|
||||||
use App\Models\Dish;
|
use App\Models\Dish;
|
||||||
use App\Models\Planner;
|
|
||||||
use App\Models\Schedule;
|
use App\Models\Schedule;
|
||||||
use App\Models\ScheduledUserDish;
|
use App\Models\ScheduledUserDish;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Models\UserDish;
|
use App\Models\UserDish;
|
||||||
use Laravel\Cashier\Cashier;
|
|
||||||
use DishPlanner\Dish\Policies\DishPolicy;
|
use DishPlanner\Dish\Policies\DishPolicy;
|
||||||
use DishPlanner\Schedule\Policies\SchedulePolicy;
|
use DishPlanner\Schedule\Policies\SchedulePolicy;
|
||||||
use DishPlanner\ScheduledUserDish\Policies\ScheduledUserDishPolicy;
|
use DishPlanner\ScheduledUserDish\Policies\ScheduledUserDishPolicy;
|
||||||
|
|
@ -47,8 +45,6 @@ public function render($request, Throwable $e)
|
||||||
|
|
||||||
public function boot(): void
|
public function boot(): void
|
||||||
{
|
{
|
||||||
Cashier::useCustomerModel(Planner::class);
|
|
||||||
|
|
||||||
Gate::policy(Dish::class, DishPolicy::class);
|
Gate::policy(Dish::class, DishPolicy::class);
|
||||||
Gate::policy(Schedule::class, SchedulePolicy::class);
|
Gate::policy(Schedule::class, SchedulePolicy::class);
|
||||||
Gate::policy(ScheduledUserDish::class, ScheduledUserDishPolicy::class);
|
Gate::policy(ScheduledUserDish::class, ScheduledUserDishPolicy::class);
|
||||||
61
backend/bootstrap/app.php
Normal file
61
backend/bootstrap/app.php
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Http\Middleware\ForceJsonResponse;
|
||||||
|
use App\Http\Middleware\HandleResourceNotFound;
|
||||||
|
use App\Services\OutputService;
|
||||||
|
use DishPlanner\Dish\Controllers\DishController;
|
||||||
|
use DishPlanner\Schedule\Console\Commands\GenerateScheduleCommand;
|
||||||
|
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||||
|
use Illuminate\Foundation\Application;
|
||||||
|
use Illuminate\Foundation\Configuration\Exceptions;
|
||||||
|
use Illuminate\Foundation\Configuration\Middleware;
|
||||||
|
use Illuminate\Http\Middleware\HandleCors;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Session\Middleware\StartSession;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
use Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||||
|
|
||||||
|
return Application::configure(basePath: dirname(__DIR__))
|
||||||
|
->withRouting(
|
||||||
|
web: __DIR__.'/../routes/web.php',
|
||||||
|
api: __DIR__.'/../routes/api.php',
|
||||||
|
commands: __DIR__.'/../routes/console.php',
|
||||||
|
health: '/up',
|
||||||
|
)
|
||||||
|
->withMiddleware(function (Middleware $middleware) {
|
||||||
|
$middleware->append(ForceJsonResponse::class);
|
||||||
|
$middleware->append(StartSession::class);
|
||||||
|
$middleware->append(HandleCors::class);
|
||||||
|
})
|
||||||
|
->withExceptions(function (Exceptions $exceptions) {
|
||||||
|
$exceptions->shouldRenderJsonWhen(function (Request $request, Throwable $e) {
|
||||||
|
if ($request->is('api/*')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $request->expectsJson();
|
||||||
|
});
|
||||||
|
|
||||||
|
/** @var OutputService $outputService */
|
||||||
|
$outputService = resolve(OutputService::class);
|
||||||
|
|
||||||
|
$exceptions->render(fn (ValidationException $e, Request $request) => $outputService
|
||||||
|
->response(false, null, [$e->getMessage()], 404)
|
||||||
|
);
|
||||||
|
|
||||||
|
$exceptions->render(fn (NotFoundHttpException $e, Request $request) => response()->json(
|
||||||
|
$outputService->response(false, null, ['MODEL_NOT_FOUND']),
|
||||||
|
404
|
||||||
|
));
|
||||||
|
|
||||||
|
$exceptions->render(fn (AccessDeniedHttpException $e, Request $request) => response()->json(
|
||||||
|
$outputService->response(false, null, [$e->getMessage()]),
|
||||||
|
403
|
||||||
|
));
|
||||||
|
})
|
||||||
|
->withCommands([
|
||||||
|
GenerateScheduleCommand::class,
|
||||||
|
])
|
||||||
|
->create();
|
||||||
0
bootstrap/cache/.gitignore → backend/bootstrap/cache/.gitignore
vendored
Executable file → Normal file
0
bootstrap/cache/.gitignore → backend/bootstrap/cache/.gitignore
vendored
Executable file → Normal file
|
|
@ -10,15 +10,12 @@
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"require": {
|
"require": {
|
||||||
"php": "^8.2",
|
"php": "^8.2",
|
||||||
"laravel/cashier": "^16.1",
|
|
||||||
"laravel/framework": "^12.9.2",
|
"laravel/framework": "^12.9.2",
|
||||||
"laravel/sanctum": "^4.0",
|
"laravel/sanctum": "^4.0",
|
||||||
"laravel/tinker": "^2.9",
|
"laravel/tinker": "^2.9"
|
||||||
"livewire/livewire": "^3.7"
|
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"fakerphp/faker": "^1.23",
|
"fakerphp/faker": "^1.23",
|
||||||
"laravel/dusk": "^8.3",
|
|
||||||
"laravel/pail": "^1.1",
|
"laravel/pail": "^1.1",
|
||||||
"laravel/pint": "^1.13",
|
"laravel/pint": "^1.13",
|
||||||
"laravel/sail": "^1.26",
|
"laravel/sail": "^1.26",
|
||||||
|
|
@ -27,9 +24,6 @@
|
||||||
"phpunit/phpunit": "^11.0.1"
|
"phpunit/phpunit": "^11.0.1"
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"files": [
|
|
||||||
"app/helpers.php"
|
|
||||||
],
|
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
"App\\": "app/",
|
"App\\": "app/",
|
||||||
"DishPlanner\\": "src/DishPlanner/",
|
"DishPlanner\\": "src/DishPlanner/",
|
||||||
|
|
@ -61,17 +55,6 @@
|
||||||
"dev": [
|
"dev": [
|
||||||
"Composer\\Config::disableProcessTimeout",
|
"Composer\\Config::disableProcessTimeout",
|
||||||
"npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1\" \"php artisan pail --timeout=0\" \"npm run dev\" --names=server,queue,logs,vite"
|
"npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1\" \"php artisan pail --timeout=0\" \"npm run dev\" --names=server,queue,logs,vite"
|
||||||
],
|
|
||||||
"test": [
|
|
||||||
"@php artisan test"
|
|
||||||
],
|
|
||||||
"test:coverage": [
|
|
||||||
"Composer\\Config::disableProcessTimeout",
|
|
||||||
"@php -d xdebug.mode=coverage artisan test --coverage"
|
|
||||||
],
|
|
||||||
"test:coverage-html": [
|
|
||||||
"Composer\\Config::disableProcessTimeout",
|
|
||||||
"@php -d xdebug.mode=coverage vendor/bin/phpunit --coverage-html coverage --coverage-text"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"extra": {
|
"extra": {
|
||||||
|
|
@ -13,7 +13,7 @@
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'name' => env('APP_NAME', 'Dish Planner'),
|
'name' => env('APP_NAME', 'Laravel'),
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|
@ -28,18 +28,6 @@
|
||||||
|
|
||||||
'env' => env('APP_ENV', 'production'),
|
'env' => env('APP_ENV', 'production'),
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Application Mode
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| Determines the application deployment mode: 'app' for self-hosted,
|
|
||||||
| 'saas' for multi-tenant SaaS, 'demo' for demonstration instances.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'mode' => env('APP_MODE', 'app'),
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
| Application Debug Mode
|
| Application Debug Mode
|
||||||
|
|
@ -35,14 +35,4 @@
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
'stripe' => [
|
|
||||||
'key' => env('STRIPE_KEY'),
|
|
||||||
'secret' => env('STRIPE_SECRET'),
|
|
||||||
'webhook' => [
|
|
||||||
'secret' => env('STRIPE_WEBHOOK_SECRET'),
|
|
||||||
],
|
|
||||||
'price_monthly' => env('STRIPE_PRICE_MONTHLY'),
|
|
||||||
'price_yearly' => env('STRIPE_PRICE_YEARLY'),
|
|
||||||
],
|
|
||||||
|
|
||||||
];
|
];
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue