diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 00000000..292688d7 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,790 @@ +# DNA Admin - GitHub Copilot Instructions + +## Coding Principles + +1. Always aim for the ideal. Strive for clean, maintainable, and efficient code that achieves the most advanced and cutting-edge solutions possible within the project's constraints. +1. Prioritize readability and maintainability. Write code that is easy to understand and modify by other developers. +1. Follow established patterns. Adhere to the existing architecture, design patterns, and coding conventions used in Spree and Rails. +1. Optimize for performance. Ensure the code is efficient and performs well, especially for critical paths. +1. Avoid upgrading locked dependencies. Do not suggest or implement upgrades for dependencies that are explicitly version-locked in the project. + +## Project Overview + +This is a **custom fork of Spree v4.2.5 e-commerce application** built on **Ruby on Rails 6.1.3** using **Ruby 2.7.2**. It serves as the backend admin and API system for a DNA e-commerce platform, integrating with a custom **Next.js v13 frontend** that accesses data via the Spree Storefront API. + +The application is containerized with Docker and deployed to both Heroku and Kubernetes environments. + +## Critical Version Constraints ⚠️ + +**DO NOT upgrade or suggest upgrading these dependencies:** +- **Ruby**: 2.7.2 (locked) +- **Rails**: ~> 6.1.3 (locked) +- **Spree**: Uses custom fork at `github: '1instinct/spree', branch: 'instinct-dna'` +- **PostgreSQL**: Primary database +- **Puma**: ~> 4.3 (web server) + +## Tech Stack + +### Core Framework +- **Ruby on Rails 6.1.3**: Web application framework +- **Ruby 2.7.2**: Programming language +- **PostgreSQL**: Primary database +- **Puma 4.3**: Application server + +### Spree Commerce Platform +- **Spree Core**: Custom fork from `1instinct/spree` (instinct-dna branch) +- **Spree Auth Devise**: ~> 4.3 (Authentication) +- **Spree Gateway**: ~> 3.9 (Payment processing) + +### Spree Extensions +- **spree_static_content**: Static page management (from spree-contrib) +- **spree_digital**: Digital product downloads (from spree-contrib) +- **spree_reffiliate**: Affiliate/referral system (from 1instinct) +- **spree_loyalty_points**: Loyalty points system (from 1instinct) +- **spree_editor**: Rich text editor for admin (from spree-contrib) + +### Key Libraries +- **Devise**: User authentication and authorization +- **CanCan**: Authorization +- **ActiveStorage**: File uploads (with AWS S3 support) +- **Rack CORS**: Cross-origin resource sharing +- **RestClient**: HTTP client for external API calls +- **Swagger Blocks**: API documentation +- **Factory Bot**: Test data generation +- **Faker**: Fake data generation +- **dotenv-rails**: Environment variable management + +### Frontend Assets +- **Sass Rails**: SCSS compilation +- **Sassc Rails**: Sass engine for Bootstrap +- **Uglifier**: JavaScript compression +- **CoffeeScript**: Asset pipeline support +- **Turbolinks**: Page navigation optimization +- **Jbuilder**: JSON API builder + +### Infrastructure +- **Docker**: Containerization +- **Heroku**: Production deployment +- **Kubernetes**: Alternative deployment option +- **AWS S3**: File storage in production +- **Mailgun**: Email delivery + +## Project Structure + +``` +app/ +├── controllers/ +│ ├── application_controller.rb +│ ├── apidocs_controller.rb # Swagger UI +│ ├── pusher_controller.rb +│ ├── concerns/ +│ │ ├── response.rb # Standardized JSON responses +│ │ └── swagger_global_model.rb # Swagger documentation models +│ └── spree/ +│ ├── admin/ # Admin interface controllers +│ │ ├── contacts_controller.rb +│ │ ├── live_stream_controller.rb +│ │ ├── menu_items_controller.rb +│ │ ├── menu_locations_controller.rb +│ │ ├── messages_controller.rb +│ │ └── threads_controller.rb +│ ├── api/ +│ │ ├── base_controller.rb # Base API controller with auth +│ │ └── v1/ # API v1 endpoints +│ │ ├── contacts_controller.rb +│ │ ├── live_stream_controller.rb +│ │ ├── menu_items_controller.rb +│ │ ├── menu_locations_controller.rb +│ │ ├── messages_controller.rb +│ │ ├── pages_controller.rb +│ │ ├── threads_controller.rb +│ │ └── users_controller.rb +│ └── store_controller_decorator.rb +├── models/ +│ ├── contact.rb +│ ├── live_stream.rb # Live streaming functionality +│ ├── live_stream_contact.rb +│ ├── live_stream_like.rb +│ ├── live_stream_product.rb +│ ├── menu_item.rb # Hierarchical menu system +│ ├── menu_location.rb +│ ├── message.rb # Messaging/chat system +│ ├── thread_table.rb # Message thread management +│ └── spree/ +│ ├── app_configuration_decorator.rb +│ ├── image_decorator.rb +│ └── user.rb # Extended Spree user +├── services/ +│ ├── ops_web.rb # External API integration +│ └── system_connect.rb # SMS/messaging dispatch +├── helpers/ +│ ├── application_helper.rb +│ └── conversation_helper.rb +└── views/ # Rails views for admin interface + +config/ +├── application.rb # Main app configuration +├── routes.rb # Route definitions +├── database.yml # Database configuration +├── storage.yml # ActiveStorage configuration (S3) +├── initializers/ +│ ├── spree.rb # Spree configuration +│ ├── cors.rb # CORS settings +│ ├── dotenv.rb # Environment variables +│ ├── constants.rb # App constants +│ ├── global.rb # Global helpers +│ ├── pusher.rb # Pusher configuration (commented) +│ └── devise.rb # Devise authentication +└── environments/ + ├── development.rb # Local storage, Mailgun SMTP + └── production.rb # S3 storage, optimized assets + +db/ +├── schema.rb # Database schema +├── seeds.rb # Database seeding +└── seeds/ # Seed data YAML files + +lib/ +├── spree_navigator.rb # Custom navigation +└── tasks/ # Rake tasks + +public/ +├── swagger-ui/ # API documentation UI +└── assets/ # Static assets +``` + +## Code Patterns & Conventions + +### Controller Patterns + +**Admin Controllers** inherit from `Spree::Admin::BaseController`: +```ruby +class Spree::Admin::MenuItemsController < Spree::Admin::BaseController + # Admin interface logic +end +``` + +**API Controllers** inherit from `Spree::Api::BaseController`: +```ruby +class Spree::Api::V1::UsersController < Spree::Api::BaseController + include Swagger::Blocks + include Response + before_action :authenticate_user, except: [:sign_up, :sign_in] +end +``` + +### Authentication & Authorization + +- **API Authentication**: Uses `spree_api_key` token-based authentication +- **Before Actions**: + - `before_action :authenticate_user` - Validates API key + - `before_action :load_user` - Loads current user + - `before_action :load_user_roles` - Loads user roles +- **Public Endpoints**: Use `except:` clause to bypass authentication +- **Devise**: Handles admin user authentication + +### Response Standardization + +All API controllers use the `Response` concern for consistent JSON responses: + +```ruby +# Success responses +success_model(200, "Success message") +singular_success_model(200, "Message", data_object) +render_object_success(objects, "Message", :object_name, count, offset) + +# Error responses +error_model(400, "Error message") +unauthorized_401_error(401, "Unauthorized") +un_expected_error("Error", 302) +``` + +### Model Patterns + +**Spree Model Extension** (Decorator Pattern): +```ruby +module Spree + class User < Spree::Base + include UserAddress + include UserMethods + include UserPaymentSource + + has_many :sent_messages, class_name: 'Message', as: :sender + has_many :received_messages, class_name: 'Message', as: :receiver + end +end +``` + +**Custom Models** inherit from `Spree::Base`: +```ruby +class LiveStream < Spree::Base + has_many :live_stream_products, dependent: :destroy + belongs_to :thread_table, optional: true + + self.whitelisted_ransackable_attributes = %w[title] + self.whitelisted_ransackable_scopes = %w[search_livestream] +end +``` + +### Hierarchical Data + +**Menu System** with parent-child relationships: +```ruby +class MenuItem < Spree::Base + belongs_to :parent, class_name: 'MenuItem', optional: true + has_many :childrens, class_name: 'MenuItem', foreign_key: :parent_id + belongs_to :menu_location + + scope :top_level, -> { where(parent_id: nil) } + default_scope { order(position: :asc) } +end +``` + +### Messaging System + +**Polymorphic Messages**: +```ruby +class Message < Spree::Base + belongs_to :sender, polymorphic: true + belongs_to :receiver, polymorphic: true + belongs_to :thread_table + + after_create :assign_thread_id +end +``` + +**Thread Management**: +- Automatically creates threads for message conversations +- Archives threads after 7 days of inactivity +- Groups messages between two parties + +### API Documentation + +All API endpoints documented with **Swagger Blocks**: +```ruby +swagger_path "/users/sign_up" do + operation :post do + key :summary, "SIGNUP" + key :description, "Signing up with email and password" + key :tags, ['Authentication'] + + parameter do + key :name, 'user[email]' + key :in, :formData + key :required, true + key :type, :string + end + + response 200 do + key :description, "Successful" + schema do + key :'$ref', :user_response + end + end + end +end +``` + +### Service Objects + +**External API Integration**: +```ruby +class OpsWeb + def self.send_message_to_dna_api(msg) + payload = { + object_id: msg.id, + object_type: msg.class.to_s, + behavior: 'SEND_SMS' + } + http_post(payload) + end + + def self.http_post(payload_hsh) + RestClient.post( + "#{SITE_URL}/#{SITE_SLUG}/internals/", + payload_hsh.to_json, + { content_type: :json, accept: :json, 'Api-Key': DNA_API_KEY } + ) + end +end +``` + +## Environment Variables + +### Required Environment Variables (Development/Test) + +```bash +# AWS S3 Storage +AWS_ACCESS_KEY_ID +AWS_BUCKET +AWS_BUCKET_NAME +AWS_REGION_NAME +AWS_SECRET_ACCESS_KEY + +# Branding +COMPANY_LOGO +COMPANY_BACKGROUND + +# Database +DATABASE_URL + +# Security +DEVISE_SECRET_KEY +SECRET_KEY_BASE +TOKEN_KEY_BASE +TOKEN_EXPIRATION + +# External Services +DNA_API_URL +DNA_API_KEY +SITE_URL +SITE_SLUG +SITE_TITLE +SITE_ADMIN_NAME + +# Email (Mailgun) +MAILGUN_HOST +MAILGUN_PORT +MAILGUN_USER +MAILGUN_PASS +MAILGUN_DOMAIN # In config + +# Pusher (Optional - currently disabled) +PUSHER_APP_ID +PUSHER_KEY +PUSHER_SECRET +PUSHER_CLUSTER + +# Twilio (Optional) +TWILIO_ACCOUNT_SID +TWILIO_PHONE_NUMBER +TWILIO_TOKEN_AUTH + +# Rails +RAILS_ENV +RACK_ENV +RAILS_LOG_TO_STDOUT +RAILS_SERVE_STATIC_FILES +``` + +### Storage Configuration + +**Development**: Uses local disk storage +**Production**: Uses AWS S3 (configured in `config/storage.yml`) + +## API Routes & Endpoints + +### Admin Routes (Spree Admin Panel) +- `/admin/messages` - Message management with conversation views +- `/admin/live_stream` - Live stream management +- `/admin/contacts` - Contact management +- `/admin/threads` - Thread/conversation management +- `/admin/menu_locations` - Menu location management +- `/admin/menu_items` - Menu item CRUD with hierarchy support + +### API v1 Routes (JSON) + +**Authentication** (`/api/v1`): +- `POST /users/sign_up` - User registration +- `POST /users/sign_in` - User login + +**Content** (`/api/v1`): +- `GET /pages` - List static pages +- `GET /pages/:slug` - Get page by slug +- `GET /menu_locations` - List menu locations +- `GET /menu_locations/:id` - Get menu location +- `GET /menu_locations/:id/menu_items` - Get menu items for location +- `GET /menu_items` - List menu items +- `GET /menu_items/:id` - Get menu item +- `GET /menu_items/:id/children` - Get child menu items + +**Live Streaming** (`/api/v1`): +- `GET/POST /live_stream` - Live stream CRUD +- `GET /live_stream/:id` - Live stream details + +**Messaging** (`/api/v1`): +- `GET/POST /messages` - Message CRUD +- `GET/POST /threads` - Thread management +- `GET/POST /contacts` - Contact management + +### Swagger Documentation +- `/apidocs/swagger_ui` - Interactive API documentation + +## Database Schema Highlights + +### Custom Tables +- `contacts` - Contact form submissions +- `live_streams` - Live streaming events with Mux integration +- `live_stream_products` - Products featured in streams +- `live_stream_likes` - User likes on streams +- `menu_items` - Hierarchical navigation menu items +- `menu_locations` - Menu placement locations +- `messages` - Polymorphic messaging system +- `thread_tables` - Message thread containers + +### Spree Tables (Key Ones) +- `spree_users` - User accounts with Devise +- `spree_products` - Product catalog +- `spree_variants` - Product variants +- `spree_orders` - E-commerce orders +- `spree_addresses` - Customer addresses +- `spree_payments` - Payment transactions +- `spree_shipments` - Order shipments +- `spree_affiliates` - Affiliate system +- `spree_loyalty_points_transactions` - Loyalty program +- `spree_pages` - Static content pages +- `spree_digitals` - Digital product files +- `spree_referrals` - Referral codes + +## Common Tasks + +### Creating a New API Endpoint + +1. **Add route** in `config/routes.rb`: +```ruby +Spree::Core::Engine.add_routes do + namespace :api do + namespace :v1 do + resources :your_resource + end + end +end +``` + +2. **Create controller** in `app/controllers/spree/api/v1/`: +```ruby +class Spree::Api::V1::YourResourceController < Spree::Api::BaseController + include Swagger::Blocks + include Response + before_action :authenticate_user, except: [:index, :show] + + def index + # Implementation + render_object_success(@resources, "Success", :resources, count, offset) + end +end +``` + +3. **Add Swagger documentation** in the controller +4. **Update SWAGGERED_CLASSES** in `app/controllers/apidocs_controller.rb` + +### Adding a New Model + +1. **Generate migration**: +```bash +docker-compose exec web rails g migration CreateYourModel +``` + +2. **Create model** in `app/models/`: +```ruby +class YourModel < Spree::Base + # Searchable attributes for Ransack + self.whitelisted_ransackable_attributes = %w[name] + self.whitelisted_ransackable_scopes = %w[search_by_name] + + # Associations + # Validations + # Scopes +end +``` + +3. **Run migrations**: +```bash +docker-compose exec web rails db:migrate +``` + +### Extending Spree Models + +Use the **decorator pattern**: + +```ruby +# app/models/spree/product_decorator.rb +module Spree + Product.class_eval do + has_many :custom_associations + + def custom_method + # Your logic + end + end +end +``` + +### Working with ActiveStorage (S3) + +**Development**: Files stored in `storage/` directory +**Production**: Files stored in AWS S3 + +Configured in `config/storage.yml` and `config/environments/production.rb`: +```ruby +config.active_storage.service = :amazon +``` + +## Docker Development + +### Common Commands + +**Build containers**: +```bash +./docker-build.sh +# or +docker-compose build +``` + +**Start application**: +```bash +docker-compose up +``` + +**Setup database** (first time): +```bash +docker-compose exec web rails db:create db:schema:load db:migrate +docker-compose exec -e ADMIN_EMAIL=spree@example.com -e ADMIN_PASSWORD=spree123 web rails db:seed +docker-compose exec web rails spree_sample:load +docker-compose restart +``` + +**Rails console**: +```bash +docker exec -it dna-admin_web_1 bin/rails console +``` + +**Reset database**: +```bash +docker-compose exec web rails db:reset railties:install:migrations db:migrate spree_sample:load db:seed +``` + +**Run rake tasks**: +```bash +docker-compose exec web rails reffiliate:generate # Generate affiliate codes +docker-compose exec web rails spree_auth:admin:create # Create admin user +``` + +## Deployment + +### Heroku Deployment + +The app uses **heroku ruby buildpack** on **heroku-20 stack**. + +Key Heroku commands: +```bash +heroku run rake db:schema:load db:migrate -a dna-admin-staging +heroku run rake db:seed -a dna-admin-staging +heroku run rake spree_sample:load -a dna-admin-staging +heroku run rake assets:precompile -a dna-admin-staging +``` + +### Kubernetes Deployment + +Configuration files: +- `k8-deployment.yaml` - Deployment configuration +- `k8-service.yml` - Service definition +- `k8-ingress.yml` - Ingress rules +- `secret.yaml.example` - Secret template +- `k8-deploy.sh` - Deployment script + +## Important Notes + +### Spree Customization +- Uses custom fork of Spree at `1instinct/spree` (instinct-dna branch) +- Never upgrade Spree without testing thoroughly +- Extensions are specifically versioned for Spree 4.2.x compatibility + +### Active Storage +- Development uses local disk storage +- Production uses AWS S3 +- Configure S3 credentials in environment variables + +### CORS Configuration +- Wide-open CORS policy (`origins '*'`) in `config/initializers/cors.rb` +- Allows GET, POST, PATCH, PUT methods +- May need tightening for production security + +### Authentication +- Admin: Devise-based username/password +- API: Token-based using `spree_api_key` +- API key passed in request headers or parameters + +### Messaging System +- Polymorphic sender/receiver (can be any model) +- Auto-creates conversation threads +- Threads auto-archive after 7 days +- Integrates with external DNA API for SMS + +### Live Streaming +- Integrates with Mux or similar service +- Supports product tagging in streams +- User likes and engagement tracking + +## Code Style & Best Practices + +### Ruby Style +- Use 2-space indentation +- Follow Ruby style guide conventions +- Use meaningful variable names +- Comment complex business logic + +### Rails Conventions +- Fat models, skinny controllers +- Use concerns for shared behavior +- Service objects for complex business logic +- Keep controllers focused on HTTP concerns + +### Database +- Always use migrations for schema changes +- Index foreign keys and frequently queried columns +- Use `acts_as_paranoid` for soft deletes where appropriate +- Leverage Ransack for search functionality + +### API Design +- Use RESTful conventions +- Version APIs (currently v1) +- Return consistent JSON structures using Response concern +- Document all endpoints with Swagger +- Handle errors gracefully with appropriate HTTP status codes + +### Security +- Never commit `.env` files +- Use environment variables for secrets +- Validate and sanitize user inputs +- Use strong parameters in controllers +- Implement proper authorization with CanCan + +## Testing & Development + +### Test Data +- **Factory Bot**: For creating test records +- **Faker**: For generating fake data +- **Spree Sample**: Sample products/data (`rails spree_sample:load`) + +### Debugging +- Use `byebug` for debugging in development +- Check logs in `log/development.log` +- Rails console for interactive debugging + +## Known Issues & Planned Improvements + +### Menu Management System Issues +**Location**: `/admin/menu_items` + +**Problems**: +- Edit template breaks when editing items with children +- Delete confirmation appears as a new element at the bottom of the page instead of a modal (easy to miss) +- Successfully editing a single item duplicates it at the root level + +**Impact**: Menu hierarchy management is unreliable and can corrupt menu structure + +### Homepage WYSIWYG Editor (Planned Feature) +**Similar to**: Live stream management pattern + +**Requirements**: +- Create homepage content widgets/sections system +- Add "Homepage" sidebar item in admin with section editors +- Implement authenticated API endpoint for homepage content +- Allow editing of multiple content sections/widgets +- Follow the pattern used for live streams (custom admin controller + API v1 endpoint) + +**Implementation Pattern**: +```ruby +# Admin: app/controllers/spree/admin/homepage_sections_controller.rb +# API: app/controllers/spree/api/v1/homepage_sections_controller.rb +# Model: app/models/homepage_section.rb +# Routes: Add to config/routes.rb under Spree::Core::Engine.add_routes +``` + +### Messages/Threads/Conversations System +**Location**: `/admin/messages`, `/admin/threads` + +**Problems**: +- Three different views for the same data that don't work together cohesively +- No seed data for testing/development +- UI should be redesigned to iChat-style interface + +**Requirements**: +- Create seed data for messages, threads, and conversations +- Consolidate three separate views into unified iChat-style UI +- Ensure message threading logic works consistently +- Improve conversation view to show real-time chat interface + +**Current System**: +- Messages: Polymorphic sender/receiver +- ThreadTable: Groups messages between parties +- Auto-archives threads after 7 days of inactivity +- Integration with external DNA API for SMS + +### Reporting Dashboard (Planned Feature) +**Location**: Admin dashboard + +**Requirements**: +- Add graphs and charts for recent activity +- Track and visualize: + - Orders (volume, revenue over time) + - Contacts/Users (registrations, growth) + - Messages (volume, response times) +- Consider using charting libraries (Chart.js, ApexCharts, or Chartkick gem) + +**Implementation Considerations**: +- May need to add dashboard controller +- Create database queries/scopes for metrics +- Add date range filters +- Consider caching for performance + +### WYSIWYG Editor Issues +**Location**: Rich text editor throughout admin + +**Problems**: +- No icons displaying on editor buttons +- "Rich Editor" menu option exists but unclear purpose +- CKEditor displays security warning: "This CKEditor 4.11.3 version is not secure. Consider upgrading to the latest one, 4.25.1-lts" + +**Affected by**: `spree_editor` gem from spree-contrib + +**Options**: +1. Upgrade CKEditor to 4.25.1-lts (security fix) +2. Migrate to Trix editor (Rails default) +3. Migrate to ActionText (Rails 6 built-in) +4. Investigate spree_editor gem updates + +**⚠️ Caution**: Editor change may affect existing content formatting + +### Admin UI Layout Issue +**Location**: Top right corner of admin interface + +**Problem**: +- Admin/user icon link displays strangely on desktop +- Erroneous link displays: `?class=logo+navbar-brand` +- Pushes account menu down inappropriately + +**Likely Cause**: Incorrect link_to helper or partial rendering issue in admin layout + +**Files to Check**: +- Admin layout partials (likely in Spree admin gem) +- May need decorator for admin layout + +## When Suggesting Code + +✅ **Do:** +- Use existing patterns from the codebase (decorators, services, concerns) +- Follow Spree conventions and override patterns +- Inherit from appropriate base classes (`Spree::Base`, `Spree::Admin::BaseController`) +- Include `Response` concern for API controllers +- Add Swagger documentation for API endpoints +- Use environment variables for configuration +- Implement proper authentication checks +- Use Ransack for search functionality +- Follow Rails conventions and best practices +- Use Docker commands for development tasks +- Reference known issues section when working on affected features +- Consider security implications (especially for CKEditor upgrade) + +❌ **Don't:** +- Upgrade Ruby, Rails, or Spree versions +- Modify Spree core tables directly (use decorators) +- Hardcode configuration values +- Skip authentication on sensitive endpoints +- Forget to update Swagger documentation +- Bypass the Response concern for API responses +- Create migrations without proper indexing +- Ignore the existing hierarchical menu system patterns +- Modify the messaging/threading system without understanding its flow +- Break existing menu hierarchies when fixing menu management bugs \ No newline at end of file diff --git a/.gitignore b/.gitignore index e907f490..133637c5 100644 --- a/.gitignore +++ b/.gitignore @@ -24,10 +24,6 @@ envvars.txt .env.staging .env.production .prettierrc -secret.yml -secret.yaml -secrets.yml -secrets.yaml .byebug_history # Ignore precompiled javascript packs @@ -90,5 +86,5 @@ secret.yaml *.tar *.yml.prod docker-compose.yml.local - -.anima/ \ No newline at end of file +.amazonq/ +yarn.lock diff --git a/Dockerfile b/Dockerfile index 7cb2658a..2695a6a2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,17 @@ # https://docs.docker.com/compose/rails/#define-the-project FROM ruby:2.7.2 +# Fix Debian repository URLs for old Buster image +RUN sed -i 's|http://deb.debian.org|http://archive.debian.org|g' /etc/apt/sources.list && \ + sed -i 's|http://security.debian.org|http://archive.debian.org|g' /etc/apt/sources.list && \ + sed -i '/security.debian.org/d' /etc/apt/sources.list # The qq is for silent output in the console -RUN apt-get update -qq && apt-get install -y postgresql-client nodejs +RUN apt-get update -qq && apt-get install -y \ + build-essential \ + libpq-dev \ + nodejs \ + npm \ + curl \ + && rm -rf /var/lib/apt/lists/* # This is given by the Ruby Image. # This will be the de-facto directory that @@ -20,7 +30,7 @@ COPY Gemfile Gemfile.lock ./ COPY .env.development .env.development # Install the Gems -RUN gem install bundler:2.2.11 +RUN gem install bundler:2.4.13 RUN bundle config set force_ruby_platform true @@ -29,16 +39,7 @@ RUN bundle install COPY . ./ # Precompile assets - -# Set environment variables for asset precompilation -# ARG DATABASE_URL=postgresql://postgres:password@db:5432/dna_admin_development -# ENV RAILS_ENV=production - # RUN RAILS_ENV=production bundle exec rake assets:precompile -# RUN RAILS_ENV=development DATABASE_URL=postgresql://postgres:password@db:5432/dna_admin_development bundle exec rake assets:precompile -# COPY wait-for-it.sh /usr/bin/ -# RUN chmod +x /usr/bin/wait-for-it.sh -# RUN /usr/bin/wait-for-it.sh db:5432 --timeout=30 --strict -- bundle exec rake assets:precompile # Add a script to be executed every time the container starts. COPY entrypoint.sh /usr/bin/ diff --git a/Gemfile b/Gemfile index d0fbaf17..c28ceb18 100644 --- a/Gemfile +++ b/Gemfile @@ -11,6 +11,8 @@ gem 'pg' gem 'puma', '~> 4.3' # Use SCSS for stylesheets gem 'sass-rails', '~> 5.0' +# Sass engine required by bootstrap +gem 'sassc-rails' # Use Uglifier as compressor for JavaScript assets gem 'uglifier', '>= 1.3.0' # See https://github.com/rails/execjs#readme for more supported runtimes @@ -49,6 +51,8 @@ gem 'spree_static_content', github: 'spree-contrib/spree_static_content' # gem 'spree_digital', github: 'spree-contrib/spree_digital' gem 'spree_reffiliate', github: '1instinct/spree_reffiliate' gem 'spree_loyalty_points', github: '1instinct/spree-loyalty-points' +# gem 'spree_avatax_official', github: 'spree-contrib/spree_avatax_official', branch: 'spree-4-1-plus' +# gem 'spree_avatax_official', '~> 1.9.0' gem 'spree_avatar', github: '1instinct/spree_avatar' gem 'spree_editor', github: 'spree-contrib/spree_editor' diff --git a/Gemfile.lock b/Gemfile.lock deleted file mode 100644 index c106169d..00000000 --- a/Gemfile.lock +++ /dev/null @@ -1,626 +0,0 @@ -GIT - remote: https://github.com/1instinct/spree-loyalty-points.git - revision: e99d2ed4913fd52bed865a5c814fefec7d82a28f - specs: - spree_loyalty_points (1.2.1) - spree_core (~> 4.2.5) - -GIT - remote: https://github.com/1instinct/spree.git - revision: 6bda546328a300aff04591d89d40c7b3f4b02473 - branch: instinct-dna - specs: - spree (4.2.5) - spree_api (= 4.2.5) - spree_backend (= 4.2.5) - spree_cmd (= 4.2.5) - spree_core (= 4.2.5) - spree_frontend (= 4.2.5) - spree_sample (= 4.2.5) - spree_api (4.2.5) - doorkeeper (~> 5.2, >= 5.2.1) - jsonapi-serializer (~> 2.1) - rabl (~> 0.14, >= 0.14.2) - spree_core (= 4.2.5) - spree_backend (4.2.5) - bootstrap (~> 4.0) - flatpickr (~> 4.6) - glyphicons (~> 1.0) - inline_svg (~> 1.5) - jquery-rails (~> 4.3) - jquery-ui-rails (~> 6.0) - select2-rails (~> 4.0) - spree_api (= 4.2.5) - spree_core (= 4.2.5) - spree_cmd (4.2.5) - thor (~> 1.0) - spree_core (4.2.5) - active_storage_validations (~> 0.9) - activemerchant (~> 1.67) - acts_as_list (>= 0.8) - awesome_nested_set (~> 3.3, >= 3.3.1) - cancancan (~> 3.2) - carmen (>= 1.0) - friendly_id (~> 5.2, >= 5.2.1) - highline (~> 2.0) - image_processing (~> 1.2) - kaminari (~> 1.2) - mini_magick (~> 4.9, >= 4.9.4) - monetize (~> 1.9) - money (~> 6.13) - paranoia (~> 2.4) - premailer-rails - rails (>= 5.2, < 6.2) - ransack (~> 2.3) - responders - rexml - sprockets (~> 3.7) - sprockets-rails - state_machines-activemodel (~> 0.7) - state_machines-activerecord (~> 0.6) - stringex - twitter_cldr (>= 4.3) - spree_frontend (4.2.5) - bootstrap (~> 4.0) - canonical-rails (~> 0.2, >= 0.2.10) - glyphicons (~> 1.0) - inline_svg (~> 1.5) - jquery-rails (~> 4.3) - spree_api (= 4.2.5) - spree_core (= 4.2.5) - turbolinks (~> 5.2) - spree_sample (4.2.5) - ffaker (~> 2.9) - spree_core (= 4.2.5) - -GIT - remote: https://github.com/1instinct/spree_avatar.git - revision: 2831457dd9d7bd3e36981f3fae57f978ba579a4f - specs: - spree_avatar (1.1.0) - active_storage_validations - inline_svg - rails (~> 6.0) - spree (~> 4.0, < 6.0) - -GIT - remote: https://github.com/1instinct/spree_reffiliate.git - revision: 16a092086888b5d9f60aa8510f5bdddf4eb96953 - specs: - spree_reffiliate (3.2.0.alpha) - spree_core (>= 3.2.0, <= 4.3.0.alpha) - -GIT - remote: https://github.com/spree-contrib/spree_editor.git - revision: 1e79e8c1150e784f76ef2f4e008a8b0e9133bab3 - specs: - spree_editor (3.3.2) - ckeditor (~> 5.0.0) - coffee-rails - deface (~> 1.0) - spree_backend (>= 3.1.0, < 5.0) - spree_core (>= 3.1.0, < 5.0) - spree_extension - tinymce-rails (~> 4.2.5) - -GIT - remote: https://github.com/spree-contrib/spree_static_content.git - revision: d2205a3a48f85d0f4d65fcc38dccb887f920bd74 - specs: - spree_static_content (4.2.0) - deface (~> 1.5) - spree_core (>= 3.7.0, < 5.0) - spree_extension - -GEM - remote: https://rubygems.org/ - specs: - actioncable (6.1.7.8) - actionpack (= 6.1.7.8) - activesupport (= 6.1.7.8) - nio4r (~> 2.0) - websocket-driver (>= 0.6.1) - actionmailbox (6.1.7.8) - actionpack (= 6.1.7.8) - activejob (= 6.1.7.8) - activerecord (= 6.1.7.8) - activestorage (= 6.1.7.8) - activesupport (= 6.1.7.8) - mail (>= 2.7.1) - actionmailer (6.1.7.8) - actionpack (= 6.1.7.8) - actionview (= 6.1.7.8) - activejob (= 6.1.7.8) - activesupport (= 6.1.7.8) - mail (~> 2.5, >= 2.5.4) - rails-dom-testing (~> 2.0) - actionpack (6.1.7.8) - actionview (= 6.1.7.8) - activesupport (= 6.1.7.8) - rack (~> 2.0, >= 2.0.9) - rack-test (>= 0.6.3) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (6.1.7.8) - actionpack (= 6.1.7.8) - activerecord (= 6.1.7.8) - activestorage (= 6.1.7.8) - activesupport (= 6.1.7.8) - nokogiri (>= 1.8.5) - actionview (6.1.7.8) - activesupport (= 6.1.7.8) - builder (~> 3.1) - erubi (~> 1.4) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.1, >= 1.2.0) - active_storage_validations (0.9.8) - activejob (>= 5.2.0) - activemodel (>= 5.2.0) - activestorage (>= 5.2.0) - activesupport (>= 5.2.0) - activejob (6.1.7.8) - activesupport (= 6.1.7.8) - globalid (>= 0.3.6) - activemerchant (1.133.0) - activesupport (>= 4.2) - builder (>= 2.1.2, < 4.0.0) - i18n (>= 0.6.9) - nokogiri (~> 1.4) - rexml (~> 3.2.5) - activemodel (6.1.7.8) - activesupport (= 6.1.7.8) - activerecord (6.1.7.8) - activemodel (= 6.1.7.8) - activesupport (= 6.1.7.8) - activestorage (6.1.7.8) - actionpack (= 6.1.7.8) - activejob (= 6.1.7.8) - activerecord (= 6.1.7.8) - activesupport (= 6.1.7.8) - marcel (~> 1.0) - mini_mime (>= 1.1.0) - activesupport (6.1.7.8) - concurrent-ruby (~> 1.0, >= 1.0.2) - i18n (>= 1.6, < 2) - minitest (>= 5.1) - tzinfo (~> 2.0) - zeitwerk (~> 2.3) - acts_as_list (1.2.2) - activerecord (>= 6.1) - activesupport (>= 6.1) - addressable (2.8.7) - public_suffix (>= 2.0.2, < 7.0) - archive-zip (0.12.0) - io-like (~> 0.3.0) - autoprefixer-rails (10.4.19.0) - execjs (~> 2) - awesome_nested_set (3.6.0) - activerecord (>= 4.0.0, < 7.2) - aws-eventstream (1.3.0) - aws-partitions (1.961.0) - aws-sdk-core (3.201.3) - aws-eventstream (~> 1, >= 1.3.0) - aws-partitions (~> 1, >= 1.651.0) - aws-sigv4 (~> 1.8) - jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.88.0) - aws-sdk-core (~> 3, >= 3.201.0) - aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.157.0) - aws-sdk-core (~> 3, >= 3.201.0) - aws-sdk-kms (~> 1) - aws-sigv4 (~> 1.5) - aws-sigv4 (1.9.1) - aws-eventstream (~> 1, >= 1.0.2) - base64 (0.2.0) - bcrypt (3.1.20) - bindex (0.8.1) - bootsnap (1.8.1) - msgpack (~> 1.0) - bootstrap (4.6.2) - autoprefixer-rails (>= 9.1.0) - popper_js (>= 1.16.1, < 2) - sassc-rails (>= 2.0.0) - builder (3.3.0) - byebug (11.1.3) - camertron-eprun (1.1.1) - cancancan (3.6.1) - canonical-rails (0.2.15) - actionview (>= 4.1, <= 7.2) - capybara (3.39.2) - addressable - matrix - mini_mime (>= 0.1.3) - nokogiri (~> 1.8) - rack (>= 1.6.0) - rack-test (>= 0.6.3) - regexp_parser (>= 1.5, < 3.0) - xpath (~> 3.2) - carmen (1.1.3) - activesupport (>= 3.0.0) - chromedriver-helper (2.1.1) - archive-zip (~> 0.10) - nokogiri (~> 1.8) - ckeditor (5.0.0) - orm_adapter (~> 0.5.0) - cldr-plurals-runtime-rb (1.1.0) - climate_control (0.2.0) - coffee-rails (4.2.2) - coffee-script (>= 2.2.0) - railties (>= 4.0.0) - coffee-script (2.4.1) - coffee-script-source - execjs - coffee-script-source (1.12.2) - concurrent-ruby (1.3.3) - crass (1.0.6) - css_parser (1.17.1) - addressable - date (3.3.4) - deface (1.9.0) - actionview (>= 5.2) - nokogiri (>= 1.6) - polyglot - railties (>= 5.2) - rainbow (>= 2.1.0) - devise (4.9.4) - bcrypt (~> 3.0) - orm_adapter (~> 0.1) - railties (>= 4.1.0) - responders - warden (~> 1.2.3) - devise-encryptable (0.2.0) - devise (>= 2.1.0) - domain_name (0.6.20240107) - doorkeeper (5.7.1) - railties (>= 5) - dotenv (2.8.1) - dotenv-rails (2.8.1) - dotenv (= 2.8.1) - railties (>= 3.2) - erubi (1.13.0) - execjs (2.9.1) - factory_bot (5.2.0) - activesupport (>= 4.2.0) - factory_bot_rails (5.2.0) - factory_bot (~> 5.2.0) - railties (>= 4.2.0) - faker (3.4.2) - i18n (>= 1.8.11, < 2) - ffaker (2.21.0) - ffi (1.17.0) - flatpickr (4.6.13.1) - friendly_id (5.5.1) - activerecord (>= 4.0.0) - globalid (1.2.1) - activesupport (>= 6.1) - glyphicons (1.0.2) - haml (6.3.0) - temple (>= 0.8.2) - thor - tilt - highline (2.1.0) - htmlentities (4.3.4) - http-accept (1.7.0) - http-cookie (1.0.6) - domain_name (~> 0.5) - i18n (1.14.5) - concurrent-ruby (~> 1.0) - image_processing (1.13.0) - mini_magick (>= 4.9.5, < 5) - ruby-vips (>= 2.0.17, < 3) - inline_svg (1.9.0) - activesupport (>= 3.0) - nokogiri (>= 1.6) - io-like (0.3.1) - jbuilder (2.12.0) - actionview (>= 5.0.0) - activesupport (>= 5.0.0) - jmespath (1.6.2) - jquery-rails (4.6.0) - rails-dom-testing (>= 1, < 3) - railties (>= 4.2.0) - thor (>= 0.14, < 2.0) - jquery-ui-rails (6.0.1) - railties (>= 3.2.16) - jsonapi-serializer (2.2.0) - activesupport (>= 4.2) - kaminari (1.2.2) - activesupport (>= 4.1.0) - kaminari-actionview (= 1.2.2) - kaminari-activerecord (= 1.2.2) - kaminari-core (= 1.2.2) - kaminari-actionview (1.2.2) - actionview - kaminari-core (= 1.2.2) - kaminari-activerecord (1.2.2) - activerecord - kaminari-core (= 1.2.2) - kaminari-core (1.2.2) - listen (3.1.5) - rb-fsevent (~> 0.9, >= 0.9.4) - rb-inotify (~> 0.9, >= 0.9.7) - ruby_dep (~> 1.2) - logger (1.6.0) - loofah (2.22.0) - crass (~> 1.0.2) - nokogiri (>= 1.12.0) - mail (2.8.1) - mini_mime (>= 0.1.1) - net-imap - net-pop - net-smtp - marcel (1.0.4) - matrix (0.4.2) - method_source (1.1.0) - mime-types (3.5.2) - mime-types-data (~> 3.2015) - mime-types-data (3.2024.0702) - mimemagic (0.3.10) - nokogiri (~> 1) - rake - mini_magick (4.13.2) - mini_mime (1.1.5) - mini_portile2 (2.8.7) - minitest (5.24.1) - monetize (1.13.0) - money (~> 6.12) - money (6.19.0) - i18n (>= 0.6.4, <= 2) - msgpack (1.7.2) - net-http (0.4.1) - uri - net-imap (0.3.7) - date - net-protocol - net-pop (0.1.2) - net-protocol - net-protocol (0.2.2) - timeout - net-smtp (0.5.0) - net-protocol - netrc (0.11.0) - nio4r (2.7.3) - nokogiri (1.15.6) - mini_portile2 (~> 2.8.2) - racc (~> 1.4) - orm_adapter (0.5.0) - paperclip (6.1.0) - activemodel (>= 4.2.0) - activesupport (>= 4.2.0) - mime-types - mimemagic (~> 0.3.0) - terrapin (~> 0.6.0) - paranoia (2.6.4) - activerecord (>= 5.1, < 7.2) - pg (1.5.7) - polyglot (0.3.5) - popper_js (1.16.1) - premailer (1.22.0) - addressable - css_parser (>= 1.12.0) - htmlentities (>= 4.0.0) - premailer-rails (1.12.0) - actionmailer (>= 3) - net-smtp - premailer (~> 1.7, >= 1.7.9) - prettier (4.0.4) - syntax_tree (>= 4.0.1) - syntax_tree-haml (>= 2.0.0) - syntax_tree-rbs (>= 0.2.0) - prettier_print (1.2.1) - public_suffix (5.1.1) - puma (4.3.12) - nio4r (~> 2.0) - rabl (0.16.1) - activesupport (>= 2.3.14) - racc (1.8.1) - rack (2.2.9) - rack-cors (2.0.2) - rack (>= 2.0.0) - rack-test (2.1.0) - rack (>= 1.3) - rails (6.1.7.8) - actioncable (= 6.1.7.8) - actionmailbox (= 6.1.7.8) - actionmailer (= 6.1.7.8) - actionpack (= 6.1.7.8) - actiontext (= 6.1.7.8) - actionview (= 6.1.7.8) - activejob (= 6.1.7.8) - activemodel (= 6.1.7.8) - activerecord (= 6.1.7.8) - activestorage (= 6.1.7.8) - activesupport (= 6.1.7.8) - bundler (>= 1.15.0) - railties (= 6.1.7.8) - sprockets-rails (>= 2.0.0) - rails-dom-testing (2.2.0) - activesupport (>= 5.0.0) - minitest - nokogiri (>= 1.6) - rails-html-sanitizer (1.6.0) - loofah (~> 2.21) - nokogiri (~> 1.14) - railties (6.1.7.8) - actionpack (= 6.1.7.8) - activesupport (= 6.1.7.8) - method_source - rake (>= 12.2) - thor (~> 1.0) - rainbow (3.1.1) - rake (13.2.1) - ransack (2.6.0) - activerecord (>= 6.0.4) - activesupport (>= 6.0.4) - i18n - rb-fsevent (0.11.2) - rb-inotify (0.11.1) - ffi (~> 1.0) - rbs (3.1.3) - regexp_parser (2.9.2) - responders (3.1.1) - actionpack (>= 5.2) - railties (>= 5.2) - rest-client (2.1.0) - http-accept (>= 1.7.0, < 2.0) - http-cookie (>= 1.0.2, < 2.0) - mime-types (>= 1.16, < 4.0) - netrc (~> 0.8) - rexml (3.2.9) - strscan - ruby-vips (2.2.2) - ffi (~> 1.12) - logger - ruby_dep (1.5.0) - rubyzip (2.3.2) - sass (3.7.4) - sass-listen (~> 4.0.0) - sass-listen (4.0.0) - rb-fsevent (~> 0.9, >= 0.9.4) - rb-inotify (~> 0.9, >= 0.9.7) - sass-rails (5.1.0) - railties (>= 5.2.0) - sass (~> 3.1) - sprockets (>= 2.8, < 4.0) - sprockets-rails (>= 2.0, < 4.0) - tilt (>= 1.1, < 3) - sassc (2.4.0) - ffi (~> 1.9) - sassc-rails (2.1.2) - railties (>= 4.0.0) - sassc (>= 2.0) - sprockets (> 3.0) - sprockets-rails - tilt - select2-rails (4.0.13) - selenium-webdriver (4.9.0) - rexml (~> 3.2, >= 3.2.5) - rubyzip (>= 1.2.2, < 3.0) - websocket (~> 1.0) - spree_auth_devise (4.3.4) - devise (~> 4.7) - devise-encryptable (= 0.2.0) - spree_core (>= 4.1, < 5.0) - spree_extension - spree_extension (0.1.0) - activerecord (>= 4.2) - spree_core - spree_gateway (3.11.1) - spree_core (>= 3.7) - spree_extension - spring (2.1.1) - spring-watcher-listen (2.0.1) - listen (>= 2.7, < 4.0) - spring (>= 1.2, < 3.0) - sprockets (3.7.3) - base64 - concurrent-ruby (~> 1.0) - rack (> 1, < 3) - sprockets-helpers (1.2.3) - sprockets (>= 2.2) - sprockets-rails (3.5.2) - actionpack (>= 6.1) - activesupport (>= 6.1) - sprockets (>= 3.0.0) - state_machines (0.5.0) - state_machines-activemodel (0.8.0) - activemodel (>= 5.1) - state_machines (>= 0.5.0) - state_machines-activerecord (0.8.0) - activerecord (>= 5.1) - state_machines-activemodel (>= 0.8.0) - stringex (2.8.6) - strscan (3.1.0) - swagger-blocks (3.0.0) - syntax_tree (6.2.0) - prettier_print (>= 1.2.0) - syntax_tree-haml (4.0.3) - haml (>= 5.2) - prettier_print (>= 1.2.1) - syntax_tree (>= 6.0.0) - syntax_tree-rbs (1.0.0) - prettier_print - rbs - syntax_tree (>= 2.0.1) - temple (0.10.3) - terrapin (0.6.0) - climate_control (>= 0.0.3, < 1.0) - thor (1.3.1) - tilt (2.4.0) - timeout (0.4.1) - tinymce-rails (4.2.8) - railties (>= 3.1.1) - turbolinks (5.2.1) - turbolinks-source (~> 5.2) - turbolinks-source (5.2.0) - twitter_cldr (6.12.1) - camertron-eprun - cldr-plurals-runtime-rb (~> 1.1) - tzinfo - tzinfo (2.0.6) - concurrent-ruby (~> 1.0) - uglifier (4.2.0) - execjs (>= 0.3.0, < 3) - uri (0.13.0) - warden (1.2.9) - rack (>= 2.0.9) - web-console (4.2.1) - actionview (>= 6.0.0) - activemodel (>= 6.0.0) - bindex (>= 0.4.0) - railties (>= 6.0.0) - websocket (1.2.11) - websocket-driver (0.7.6) - websocket-extensions (>= 0.1.0) - websocket-extensions (0.1.5) - xpath (3.2.0) - nokogiri (~> 1.8) - zeitwerk (2.6.17) - -PLATFORMS - ruby - -DEPENDENCIES - aws-sdk-s3 (~> 1.96) - bootsnap (~> 1.8.1) - byebug - capybara (>= 2.15) - chromedriver-helper - coffee-rails (~> 4.2) - dotenv-rails - factory_bot_rails (~> 5.2) - faker - jbuilder (~> 2.5) - listen (>= 3.0.5, < 3.2) - net-http - paperclip (~> 6.1.0) - pg - prettier - puma (~> 4.3) - rack-cors - rails (~> 6.1.3) - rest-client - sass-rails (~> 5.0) - selenium-webdriver - spree! - spree_auth_devise (~> 4.3) - spree_avatar! - spree_editor! - spree_gateway (~> 3.9) - spree_loyalty_points! - spree_reffiliate! - spree_static_content! - spring - spring-watcher-listen (~> 2.0.0) - sprockets-helpers (~> 1.2.1) - swagger-blocks - turbolinks (~> 5) - uglifier (>= 1.3.0) - web-console (>= 3.3.0) - -RUBY VERSION - ruby 2.7.2p137 - -BUNDLED WITH - 2.1.4 diff --git a/IMPLEMENTATION_SUMMARY.md b/IMPLEMENTATION_SUMMARY.md new file mode 100644 index 00000000..3d967d68 --- /dev/null +++ b/IMPLEMENTATION_SUMMARY.md @@ -0,0 +1,221 @@ +# DNA Admin - Implementation Summary + +## Features Implemented + +### 1. ✅ Messages/Threads Seed Data +**File**: `db/seeds/008_messages.rb` + +**What was done**: +- Created realistic conversation threads between customers and support agents +- 10 conversation threads with 3-8 messages each +- Realistic back-and-forth customer service scenarios +- 5 standalone messages that will auto-assign threads +- Proper timestamps and read/unread status + +**To use**: +```bash +docker-compose exec web rails db:seed +``` + +--- + +### 2. ✅ Homepage Sections Feature +**Pattern**: Follows live stream management structure + +**Files Created**: +- **Migration**: `db/migrate/20260102113537_create_homepage_sections.rb` +- **Model**: `app/models/homepage_section.rb` +- **Admin Controller**: `app/controllers/spree/admin/homepage_sections_controller.rb` +- **API Controller**: `app/controllers/spree/api/v1/homepage_sections_controller.rb` +- **Seed Data**: `db/seeds/010_homepage_sections.rb` +- **Routes**: Updated in `config/routes.rb` +- **Swagger**: Added to `app/controllers/apidocs_controller.rb` + +**Features**: +- 10 section types: hero, features, products, content, testimonials, gallery, call_to_action, newsletter, video, custom +- Position-based ordering with move_up/move_down actions +- Visibility toggle +- Flexible JSON settings field for section-specific configurations +- Full REST API with Swagger documentation +- Admin interface at `/admin/homepage_sections` +- Public API at `/api/v1/homepage_sections` + +**Seed data includes**: +- Hero banner with background image settings +- Featured products section +- Features grid (why choose us) +- About/content section +- Customer testimonials +- Newsletter signup +- Image gallery (hidden by default) +- Call to action banner + +**To run migration**: +```bash +docker-compose exec web rails db:migrate +docker-compose exec web rails db:seed +``` + +--- + +### 3. ✅ Reporting Dashboard with Charts +**File**: `app/controllers/spree/admin/reports_controller_decorator.rb` +**View**: `app/views/spree/admin/reports/index.html.erb` + +**What was done**: +- Overrode Spree's existing `/admin/reports` route +- Added comprehensive analytics dashboard with Chart.js +- Date range filter (7, 30, 90, 365 days) + +**Metrics Tracked**: +- **Orders**: Total count, revenue, average order value +- **Users**: New signups, total users, new contacts +- **Messages**: Total messages, unread count, active threads + +**Charts Included**: +1. **Orders & Revenue Over Time** (dual-axis line chart) +2. **Order Status Distribution** (doughnut chart) +3. **User & Contact Growth** (bar chart) +4. **Message Activity** (line chart with total/unread) +5. **Top Selling Products** (table) + +**Summary Cards**: +- Total Orders +- Total Revenue & Average Order Value +- New Users & Total Users +- Messages & Unread Count + +**Access**: `/admin/reports` + +--- + +### 4. ✅ Menu Management Bug Fixes +**File**: `app/controllers/spree/admin/menu_items_controller_decorator.rb` + +**Issues Fixed**: +1. **Duplication Bug**: Menu items were duplicating at root level after edit + - Fixed by tracking original parent_id and only reorganizing when parent actually changes + - Improved organize_items to exclude current item from sibling list + +2. **Delete Confirmation**: Added proper server-side validation + - Prevents deletion of items with children + - Shows clear error message + +3. **Edit Template**: Fixed update action to prevent unnecessary reorganization + +**Note**: The JavaScript tree component may still have UI issues with modal positioning. The server-side logic is now solid. + +--- + +### 5. ✅ CKEditor Security Update +**File**: `config/initializers/spree_editor.rb` + +**What was done**: +- Created initializer to upgrade CKEditor from 4.11.3 to 4.25.1-lts +- Configured CDN URL to use latest LTS version +- Set up proper toolbar configuration +- Fixed security vulnerability warning + +**Configuration includes**: +- Latest LTS version (4.25.1-lts) +- CDN-based loading +- Customized toolbar with common editing features +- Proper height and language settings + +--- + +## Files Modified + +### New Files Created: +1. `db/migrate/20260102113537_create_homepage_sections.rb` +2. `app/models/homepage_section.rb` +3. `app/controllers/spree/admin/homepage_sections_controller.rb` +4. `app/controllers/spree/api/v1/homepage_sections_controller.rb` +5. `app/controllers/spree/admin/reports_controller_decorator.rb` +6. `app/views/spree/admin/reports/index.html.erb` +7. `app/controllers/spree/admin/menu_items_controller_decorator.rb` +8. `config/initializers/spree_editor.rb` +9. `db/seeds/010_homepage_sections.rb` + +### Files Modified: +1. `db/seeds/008_messages.rb` - Enhanced with realistic conversations +2. `config/routes.rb` - Added homepage_sections routes +3. `app/controllers/apidocs_controller.rb` - Added HomepageSectionsController to Swagger + +--- + +## Next Steps + +### To Apply Changes: + +1. **Run migrations**: +```bash +docker-compose exec web rails db:migrate +``` + +2. **Run seeds** (optional, for test data): +```bash +docker-compose exec web rails db:seed +``` + +3. **Restart containers** (to load decorators and initializers): +```bash +docker-compose restart web +``` + +4. **Test the features**: +- Visit `/admin/homepage_sections` to manage homepage content +- Visit `/admin/reports` to see the analytics dashboard +- Visit `/admin/menu_items` to test menu management fixes +- Check `/apidocs/swagger_ui` for new API documentation +- Test the editor in any content area (e.g., static pages, products) + +### API Endpoints Added: +- `GET /api/v1/homepage_sections` - List all homepage sections +- `GET /api/v1/homepage_sections/:id` - Get specific section +- `POST /api/v1/homepage_sections` - Create new section (authenticated) +- `PUT /api/v1/homepage_sections/:id` - Update section (authenticated) +- `DELETE /api/v1/homepage_sections/:id` - Delete section (authenticated) + +--- + +## Known Limitations + +1. **Menu Management**: JavaScript tree UI may still have visual issues with delete confirmation placement. Server-side logic is fixed. + +2. **CKEditor Icons**: If icons still don't display after the upgrade, may need to check asset pipeline or consider migrating to ActionText (Rails 6 built-in). + +3. **Reporting Dashboard**: Charts use CDN-loaded Chart.js. For offline environments, bundle locally. + +4. **Homepage Sections**: Admin views for homepage sections need to be created (currently using standard Spree admin scaffolding). + +--- + +## Testing Recommendations + +1. **Messages System**: + - Load seed data and check `/admin/messages` and `/admin/threads` + - Test the threading logic with new messages + - Verify 7-day archive functionality + +2. **Homepage Sections**: + - Create/edit/delete sections via admin + - Test position reordering with move_up/move_down + - Verify API endpoints return correct data + - Test visibility toggle + +3. **Reports Dashboard**: + - Change date ranges and verify data updates + - Check that charts render properly + - Verify top products table shows correct data + +4. **Menu Management**: + - Edit menu items with children + - Verify no duplication occurs + - Try to delete item with children (should fail with message) + - Edit items without children (should work) + +5. **CKEditor**: + - Open any rich text editor field + - Verify version shows 4.25.1-lts (no security warning) + - Check that toolbar icons display properly diff --git a/PRODUCT_SETUP.md b/PRODUCT_SETUP.md new file mode 100644 index 00000000..c8a1cffa --- /dev/null +++ b/PRODUCT_SETUP.md @@ -0,0 +1,30 @@ +# Product Import Setup + +## For Aaron (or any developer): + +### 1. AWS Setup - Use Shared Bucket +- Update `.env.development` with shared bucket: + ``` + AWS_ACCESS_KEY_ID=your-real-key + AWS_SECRET_ACCESS_KEY=your-real-secret + AWS_BUCKET_NAME=dna-admin-dev-jon-12345 + ``` + +### 2. Import Products +```shell +# Clear existing products (optional) +docker-compose exec web rails products:cleanup + +# Import products with images +docker-compose exec web bash -c "export AWS_BUCKET_NAME=dna-admin-dev-jon-12345 && rails products:import CSV_FILE=/dna/db/seed_data/products.csv IMAGES_PATH=/dna/db/seed_data/images" +``` + +### 3. Files Included +- `db/seed_data/products.csv` - Product data +- `lib/tasks/product_import.rake` - Import script +- `lib/tasks/product_cleanup.rake` - Cleanup script + +### 4. Notes +- Images are already uploaded to `dna-admin-dev-jon-12345` +- No need to upload images again +- Everyone shares the same S3 bucket and images \ No newline at end of file diff --git a/README.md b/README.md index 2a067838..b0fb0a7d 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,26 @@ -# DOCKER SETUP +# DOCKER SETUP ✻ + This repo is using Spree 4.2.4 + ## Build + This should only have to be done once, or whenever the Gemfile is updated. + +First try this: + +```shell +./docker-build.sh ``` + +The manual way: + +```shell docker-compose build ``` ## Create Containers -``` +```shell docker-compose up ``` @@ -19,7 +31,7 @@ but it probably needs to be set up first. In a new terminal run: -``` +```shell docker-compose exec web rails db:create db:schema:load db:migrate && docker-compose exec -e ADMIN_EMAIL=spree@example.com -e ADMIN_PASSWORD=spree123 web rails db:seed && docker-compose exec web rails spree_sample:load && @@ -29,7 +41,7 @@ docker-compose restart OPTIONAL: Create a new admin user. This can be used to reset the admin user also: -``` +```shell docker-compose exec web rails spree_auth:admin:create ``` @@ -109,6 +121,13 @@ on github is hooked in to the deployment. 11. Load Sample Data: `heroku run -a dna-admin-staging rake spree_sample:load` 12. Asset Precompile: `heroku run -a dna-admin-staging rake assets:precompile` +Alternate (container) deploy: + +1. `./docker-build.sh` +1. `heroku stack:set container -a dna-admin-prod` +1. `heroku container:push dna-admin-web-1 -a dna-admin-prod` +1. `heroku container:release web -a dna-admin-prod` +1. `heroku config:set SECRET_KEY_BASE=$(docker-compose exec dna-admin-web-1 rails secret) -a kry-admin-prod` Git: diff --git a/app/controllers/apidocs_controller.rb b/app/controllers/apidocs_controller.rb index 013f3491..73806e74 100644 --- a/app/controllers/apidocs_controller.rb +++ b/app/controllers/apidocs_controller.rb @@ -10,7 +10,7 @@ class ApidocsController < ActionController::Base key :name, 'Mayank Gandhi' end end - key :host, 'https://dna-admin-dev.instinct.is/' if Rails.env.development? + key :host, 'localhost:3001' if Rails.env.development? key :host, 'https://dna-admin-staging.instinct.is/' if Rails.env.staging? key :host, 'https://admin.instinct.is/' if Rails.env.production? @@ -24,6 +24,8 @@ class ApidocsController < ActionController::Base SWAGGERED_CLASSES_V1 = [ #SwaggerController, #controller details goes from here where from you are creating api Spree::Api::V1::LiveStreamController, + Spree::Api::V1::HomepageSectionsController, + Spree::Api::V1::FavoritesController, Spree::Api::V1::UsersController, Spree::Api::V1::PagesController, Spree::Api::V1::ContactsController, diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index a839a36d..12b67563 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,4 +1,5 @@ class ApplicationController < ActionController::Base + protect_from_forgery with: :null_session, if: -> { request.format.json? } before_action :require_login # skip_before_action :require_login, only: [:spree_login] diff --git a/app/controllers/concerns/spree/api/v1/global_helper.rb b/app/controllers/concerns/spree/api/v1/global_helper.rb index e0b9dd6e..64a007a9 100644 --- a/app/controllers/concerns/spree/api/v1/global_helper.rb +++ b/app/controllers/concerns/spree/api/v1/global_helper.rb @@ -11,4 +11,15 @@ def to_utc_time(timestamp) utc_date_time = convert_to_datetime.utc return utc_date_time end + + def user_detail(user) + return nil unless user + { + id: user.id, + email: user.email, + first_name: user.bill_address&.firstname || "", + last_name: user.bill_address&.lastname || "", + phone: user.bill_address&.phone || "" + } + end end diff --git a/app/controllers/concerns/spree/api/v1/live_stream_helper.rb b/app/controllers/concerns/spree/api/v1/live_stream_helper.rb index c1041fb9..f3ff3a14 100644 --- a/app/controllers/concerns/spree/api/v1/live_stream_helper.rb +++ b/app/controllers/concerns/spree/api/v1/live_stream_helper.rb @@ -13,7 +13,9 @@ def live_stream_detail(id) status: live_stream&.status || "", start_date: to_timestamp(live_stream&.start_date) || "", is_active: live_stream&.is_active || true, - product_ids: live_stream&.product_ids || [] + product_ids: live_stream&.product_ids || [], + contact_ids: live_stream&.contact_ids || [], + actor: user_detail(live_stream&.actor) } return live_stream end diff --git a/app/controllers/concerns/swagger_global_model.rb b/app/controllers/concerns/swagger_global_model.rb index de55761b..14c33575 100644 --- a/app/controllers/concerns/swagger_global_model.rb +++ b/app/controllers/concerns/swagger_global_model.rb @@ -12,4 +12,101 @@ class SwaggerGlobalModel # Notice, this is just a plain ruby object. end end + swagger_schema :favorites_list_response do + key :required, [:status, :message, :data] + property :status do + key :type, :integer + end + property :message do + key :type, :string + end + property :data do + key :type, :array + items do + key :'$ref', :favorite_object + end + end + property :meta do + key :'$ref', :pagination_meta + end + end + + swagger_schema :favorite_toggle_response do + key :required, [:status, :message] + property :status do + key :type, :integer + end + property :message do + key :type, :string + end + end + + swagger_schema :favorite_object do + property :id do + key :type, :integer + end + property :created_at do + key :type, :string + key :format, 'date-time' + end + property :variant do + key :type, :object + property :id do + key :type, :integer + end + property :sku do + key :type, :string + end + property :name do + key :type, :string + end + property :price do + key :type, :string + end + property :in_stock do + key :type, :boolean + end + property :product do + key :type, :object + property :id do + key :type, :integer + end + property :name do + key :type, :string + end + property :slug do + key :type, :string + end + property :description do + key :type, :string + end + property :images do + key :type, :array + items do + key :type, :object + property :url do + key :type, :string + end + end + end + end + end + end + + swagger_schema :pagination_meta do + property :total_count do + key :type, :integer + end + property :current_page do + key :type, :integer + end + property :total_pages do + key :type, :integer + end + property :per_page do + key :type, :integer + end + end + end + diff --git a/app/controllers/spree/admin/homepage_sections_controller.rb b/app/controllers/spree/admin/homepage_sections_controller.rb new file mode 100644 index 00000000..16941699 --- /dev/null +++ b/app/controllers/spree/admin/homepage_sections_controller.rb @@ -0,0 +1,119 @@ +module Spree + module Admin + class HomepageSectionsController < Spree::Admin::BaseController + before_action :set_homepage_section, only: [:edit, :update, :destroy, :show] + + def index + @q = HomepageSection.ransack(params[:q]) + @collection = @q.result + .order(position: :asc) + .page(params[:page]) + .per(params[:per_page] || 25) + end + + def new + @homepage_section = HomepageSection.new + end + + def create + @homepage_section = HomepageSection.new(homepage_section_params) + + respond_to do |format| + if @homepage_section.save + flash[:success] = Spree.t('homepage_section.added_success') + format.html { redirect_to admin_homepage_sections_path } + format.json { render json: @homepage_section, status: :created } + else + flash[:error] = @homepage_section.errors.full_messages.join(', ') + format.html { render :new } + format.json { render json: @homepage_section.errors, status: :unprocessable_entity } + end + end + end + + def edit + end + + def show + end + + def update + respond_to do |format| + if @homepage_section.update(homepage_section_params) + flash[:success] = Spree.t('homepage_section.update_success') + format.html { redirect_to admin_homepage_sections_path } + format.json { render json: @homepage_section } + else + flash[:error] = @homepage_section.errors.full_messages.join(', ') + format.html { render :edit } + format.json { render json: @homepage_section.errors, status: :unprocessable_entity } + end + end + end + + def destroy + if @homepage_section.destroy + flash[:success] = Spree.t('homepage_section.deleted_success') + else + flash[:error] = Spree.t('homepage_section.delete_failed') + end + redirect_to admin_homepage_sections_path + end + + # Move section up in order + def move_up + @homepage_section = HomepageSection.find(params[:id]) + previous_section = HomepageSection.where('position < ?', @homepage_section.position) + .order(position: :desc) + .first + + if previous_section + HomepageSection.transaction do + temp_position = @homepage_section.position + @homepage_section.update_column(:position, previous_section.position) + previous_section.update_column(:position, temp_position) + end + flash[:success] = Spree.t('homepage_section.moved_up') + end + + redirect_to admin_homepage_sections_path + end + + # Move section down in order + def move_down + @homepage_section = HomepageSection.find(params[:id]) + next_section = HomepageSection.where('position > ?', @homepage_section.position) + .order(position: :asc) + .first + + if next_section + HomepageSection.transaction do + temp_position = @homepage_section.position + @homepage_section.update_column(:position, next_section.position) + next_section.update_column(:position, temp_position) + end + flash[:success] = Spree.t('homepage_section.moved_down') + end + + redirect_to admin_homepage_sections_path + end + + private + + def set_homepage_section + @homepage_section = HomepageSection.find(params[:id]) + end + + def homepage_section_params + params.require(:homepage_section).permit( + :title, + :section_type, + :content, + :position, + :is_visible, + :settings + ) + end + end + end +end diff --git a/app/controllers/spree/admin/menu_items_controller_decorator.rb b/app/controllers/spree/admin/menu_items_controller_decorator.rb new file mode 100644 index 00000000..0c4ae4fa --- /dev/null +++ b/app/controllers/spree/admin/menu_items_controller_decorator.rb @@ -0,0 +1,78 @@ +# Menu Management System - Known Issues & Fixes +# +# This file documents issues with the menu management system and provides fixes +# +# ISSUES: +# 1. Edit template breaks when editing items with children +# 2. Delete confirmation appears at bottom instead of modal +# 3. Successfully editing a single item duplicates it at root level +# +# ROOT CAUSE: +# The menu system uses a JavaScript tree (jstree) with AJAX operations that have bugs in: +# - app/views/spree/admin/menu_items/_tree.html.erb +# - app/assets/javascripts/spree/backend/menu_items.js (if exists) +# +# FIXES IMPLEMENTED: + +module Spree + module Admin + MenuItemsController.class_eval do + # Fix: Prevent duplication when updating menu items + def update + respond_to do |format| + # Store original parent_id before update + original_parent_id = @menu_item.parent_id + + if @menu_item.update(menu_item_params) + # Only reorganize if parent changed or if it's a top-level item + if original_parent_id != @menu_item.parent_id + organize_items + end + + format.html { submit_success_redirect(:update) } + format.json { render :show, status: :ok } + else + format.html { render :edit } + format.json { render_json_error } + end + end + end + + # Fix: Improved delete with proper confirmation + def destroy + if @menu_item.childrens.any? + flash[:error] = "Cannot delete menu item with children. Please delete child items first." + redirect_to admin_menu_items_path + else + if @menu_item.destroy + flash[:success] = Spree.t('menu_navigator.admin.flash.success.destroy', name: @menu_item.name) + else + flash[:error] = "Failed to delete menu item" + end + redirect_to admin_menu_items_path + end + end + + private + + # Improved organize_items to prevent duplication + def organize_items + parent_id = menu_item_params[:parent_id] + + # Skip if no parent change + return if parent_id == @menu_item.parent_id_was.to_s + + siblings = MenuItem.where(parent_id: parent_id) + .where.not(id: @menu_item.id) # Exclude current item + .order(position: :asc) + + siblings.each_with_index do |item, index| + item.update_column(:position, index) if item.position != index + end + + # Set current item's position + @menu_item.update_column(:position, siblings.count) + end + end + end +end diff --git a/app/controllers/spree/admin/reports_controller_decorator.rb b/app/controllers/spree/admin/reports_controller_decorator.rb new file mode 100644 index 00000000..bbbca9d7 --- /dev/null +++ b/app/controllers/spree/admin/reports_controller_decorator.rb @@ -0,0 +1,115 @@ +module Spree + module Admin + ReportsController.class_eval do + # Override the index action to add analytics dashboard + def index + @reports = reports_list + + # Add analytics data + @date_range = params[:date_range] || '30' + @start_date = @date_range.to_i.days.ago.beginning_of_day + @end_date = Time.current.end_of_day + + # Order metrics + @total_orders = Spree::Order.complete.where(completed_at: @start_date..@end_date).count + @total_revenue = Spree::Order.complete.where(completed_at: @start_date..@end_date).sum(:total) + @average_order_value = @total_orders > 0 ? (@total_revenue / @total_orders) : 0 + + # User/Contact metrics + @new_users = Spree::User.where(created_at: @start_date..@end_date).count + @new_contacts = Contact.where(created_at: @start_date..@end_date).count + @total_users = Spree::User.count + + # Message metrics + @total_messages = Message.where(created_at: @start_date..@end_date).count + @unread_messages = Message.where(is_read: false, created_at: @start_date..@end_date).count + @active_threads = ThreadTable.where(archived: false).count + + # Chart data + prepare_charts_data + end + + private + + def prepare_charts_data + # Orders over time (daily) + days = @date_range.to_i + @orders_chart_data = (0...days).map do |i| + date = i.days.ago.to_date + count = Spree::Order.complete.where( + completed_at: date.beginning_of_day..date.end_of_day + ).count + revenue = Spree::Order.complete.where( + completed_at: date.beginning_of_day..date.end_of_day + ).sum(:total).to_f + + { + date: date.strftime('%m/%d'), + orders: count, + revenue: revenue.round(2) + } + end.reverse + + # Users growth + @users_chart_data = (0...days).map do |i| + date = i.days.ago.to_date + users = Spree::User.where( + created_at: date.beginning_of_day..date.end_of_day + ).count + contacts = Contact.where( + created_at: date.beginning_of_day..date.end_of_day + ).count + + { + date: date.strftime('%m/%d'), + users: users, + contacts: contacts + } + end.reverse + + # Messages activity + @messages_chart_data = (0...days).map do |i| + date = i.days.ago.to_date + total = Message.where( + created_at: date.beginning_of_day..date.end_of_day + ).count + unread = Message.where( + is_read: false, + created_at: date.beginning_of_day..date.end_of_day + ).count + + { + date: date.strftime('%m/%d'), + total: total, + unread: unread + } + end.reverse + + # Order status breakdown + @order_status_data = [ + { status: 'Complete', count: Spree::Order.where(state: 'complete', completed_at: @start_date..@end_date).count }, + { status: 'Cart', count: Spree::Order.where(state: 'cart').count }, + { status: 'Address', count: Spree::Order.where(state: 'address').count }, + { status: 'Delivery', count: Spree::Order.where(state: 'delivery').count }, + { status: 'Payment', count: Spree::Order.where(state: 'payment').count }, + { status: 'Canceled', count: Spree::Order.where(state: 'canceled').count } + ].reject { |item| item[:count].zero? } + + # Top products + @top_products = Spree::LineItem + .joins(:order, :variant) + .where('spree_orders.completed_at >= ?', @start_date) + .where('spree_orders.state = ?', 'complete') + .group('spree_variants.id') + .select('spree_variants.id, spree_variants.sku, SUM(spree_line_items.quantity) as total_quantity, SUM(spree_line_items.quantity * spree_line_items.price) as total_sales') + .order('total_quantity DESC') + .limit(10) + end + + def reports_list + # Return the default Spree reports if available + Spree::Admin::ReportsController::AVAILABLE_REPORTS rescue {} + end + end + end +end diff --git a/app/controllers/spree/api/v1/favorites_controller.rb b/app/controllers/spree/api/v1/favorites_controller.rb new file mode 100644 index 00000000..4d5a25f1 --- /dev/null +++ b/app/controllers/spree/api/v1/favorites_controller.rb @@ -0,0 +1,301 @@ +module Spree + module Api + module V1 + class FavoritesController < Spree::Api::BaseController + include Swagger::Blocks + include Response + before_action :authenticate_user + before_action :load_variant, only: [:toggle] + before_action :load_favorite, only: [:destroy] + + swagger_path '/favorites' do + operation :get do + key :summary, 'LIST USER FAVORITES' + key :description, 'Get all favorites for the authenticated user' + key :tags, ['Favorites'] + + parameter do + key :name, 'token' + key :in, :query + key :description, 'Authentication token (spree_api_key)' + key :required, true + key :type, :string + end + + parameter do + key :name, 'page' + key :in, :query + key :description, 'Page number for pagination' + key :required, false + key :type, :integer + end + + parameter do + key :name, 'per_page' + key :in, :query + key :description, 'Items per page (default: 25)' + key :required, false + key :type, :integer + end + + response 200 do + key :description, 'Successful' + schema do + key :'$ref', :favorites_list_response + end + end + + response 401 do + key :description, 'Unauthorized' + end + end + end + + swagger_path '/favorites/toggle' do + operation :post do + key :summary, 'TOGGLE FAVORITE' + key :description, 'Add or remove a variant from favorites' + key :tags, ['Favorites'] + + parameter do + key :name, 'token' + key :in, :query + key :description, 'Authentication token (spree_api_key)' + key :required, true + key :type, :string + end + + parameter do + key :name, 'variant_id' + key :in, :formData + key :description, 'Variant ID to favorite/unfavorite' + key :required, true + key :type, :integer + end + + response 200 do + key :description, 'Successful' + schema do + key :'$ref', :favorite_toggle_response + end + end + + response 401 do + key :description, 'Unauthorized' + end + + response 404 do + key :description, 'Variant not found' + end + end + end + + swagger_path '/favorites/{id}' do + operation :delete do + key :summary, 'REMOVE FAVORITE' + key :description, 'Remove a specific favorite by ID' + key :tags, ['Favorites'] + + parameter do + key :name, 'token' + key :in, :query + key :description, 'Authentication token (spree_api_key)' + key :required, true + key :type, :string + end + + parameter do + key :name, 'id' + key :in, :path + key :description, 'Favorite ID to remove' + key :required, true + key :type, :integer + end + + response 200 do + key :description, 'Successful' + end + + response 401 do + key :description, 'Unauthorized' + end + + response 404 do + key :description, 'Favorite not found' + end + end + end + + # GET /api/v1/favorites + def index + @favorites = current_api_user.favorites + .includes(variant: [:product, :images, :prices]) + .recent + .page(params[:page]) + .per(params[:per_page] || 25) + + render_favorites_success(@favorites) + end + + # POST /api/v1/favorites/toggle + def toggle + is_favorited = Favorite.toggle(current_api_user, @variant) + + if is_favorited + success_model(200, "Added to favorites") + else + success_model(200, "Removed from favorites") + end + end + + # DELETE /api/v1/favorites/:id + def destroy + if @favorite.destroy + success_model(200, "Favorite removed successfully") + else + error_model(422, "Could not remove favorite") + end + end + + # GET /api/v1/favorites/check + def check + variant = Spree::Variant.find_by(id: params[:variant_id]) + return error_model(404, "Variant not found") unless variant + + is_favorited = Favorite.favorited?(current_api_user, variant) + + render json: { + status: 200, + message: "Success", + is_favorited: is_favorited + } + end + + private + + def load_variant + @variant = Spree::Variant.find_by(id: params[:variant_id]) + return error_model(404, "Variant not found") unless @variant + end + + def load_favorite + @favorite = current_api_user.favorites.find_by(id: params[:id]) + return error_model(404, "Favorite not found") unless @favorite + end + + def render_favorites_success(favorites) + favorites_data = favorites.map do |favorite| + variant = favorite.variant + product = variant.product + + { + id: favorite.id, + created_at: favorite.created_at, + is_public: favorite.is_public || false, + variant: { + id: variant.id, + sku: variant.sku, + name: variant.name, + price: variant.price.to_s, + in_stock: variant.in_stock?, + product: { + id: product.id, + name: product.name, + slug: product.slug, + description: product.description, + images: product.images.map { |img| { url: img.attachment.url } } + } + } + } + end + + render json: { + status: 200, + message: "Favorites retrieved successfully", + data: favorites_data, + meta: { + total_count: favorites.total_count, + current_page: favorites.current_page, + total_pages: favorites.total_pages, + per_page: favorites.limit_value + } + } + end + + swagger_path '/favorites/{id}/toggle_public' do + operation :post do + key :summary, 'TOGGLE FAVORITE PUBLIC/PRIVATE' + key :description, 'Toggle whether a favorite is public or private' + key :tags, ['Favorites'] + + parameter do + key :name, 'token' + key :in, :query + key :description, 'Authentication token (spree_api_key)' + key :required, true + key :type, :string + end + + parameter do + key :name, :id + key :in, :path + key :description, 'Favorite ID' + key :required, true + key :type, :integer + end + + response 200 do + key :description, 'Successfully toggled' + schema do + key :'$ref', :favorite_toggle_public_response + end + end + + response 401 do + key :description, 'Unauthorized' + end + + response 404 do + key :description, 'Favorite not found' + end + end + end + + swagger_schema :favorite_toggle_public_response do + property :status do + key :type, :integer + end + property :message do + key :type, :string + end + property :data do + property :id do + key :type, :integer + end + property :is_public do + key :type, :boolean + end + end + end + + def toggle_public + favorite = current_api_user.favorites.find_by(id: params[:id]) + unless favorite + return error_model(404, "Favorite not found") + end + + favorite.update(is_public: !favorite.is_public) + + render json: { + status: 200, + message: favorite.is_public ? "Favorite is now public" : "Favorite is now private", + data: { + id: favorite.id, + is_public: favorite.is_public + } + } + end + end + end + end +end diff --git a/app/controllers/spree/api/v1/homepage_sections_controller.rb b/app/controllers/spree/api/v1/homepage_sections_controller.rb new file mode 100644 index 00000000..fb6265ac --- /dev/null +++ b/app/controllers/spree/api/v1/homepage_sections_controller.rb @@ -0,0 +1,209 @@ +class Spree::Api::V1::HomepageSectionsController < Spree::Api::BaseController + include Swagger::Blocks + include Response + + before_action :authenticate_user, except: [:index, :show] + + swagger_path "/homepage_sections" do + operation :get do + key :summary, "List Homepage Sections" + key :description, "Returns all visible homepage sections ordered by position" + key :tags, ['Homepage'] + + parameter do + key :name, :visible_only + key :in, :query + key :description, 'Filter to only visible sections (default: true)' + key :required, false + key :type, :boolean + end + + response 200 do + key :description, "Successful" + schema do + key :'$ref', :homepage_sections_response + end + end + end + end + + swagger_path "/homepage_sections/{id}" do + operation :get do + key :summary, "Get Homepage Section" + key :description, "Returns a single homepage section by ID" + key :tags, ['Homepage'] + + parameter do + key :name, :id + key :in, :path + key :description, 'Section ID' + key :required, true + key :type, :integer + end + + response 200 do + key :description, "Successful" + schema do + key :'$ref', :homepage_section_response + end + end + + response 404 do + key :description, "Not found" + end + end + end + + swagger_schema :homepage_sections_response do + key :required, [:response_code, :response_message] + property :response_code do + key :type, :integer + end + property :response_message do + key :type, :string + end + property :response_data do + key :type, :object + property :total_records do + key :type, :integer + end + property :offset do + key :type, :integer + end + property :homepage_sections do + key :type, :array + items do + key :'$ref', :homepage_section + end + end + end + end + + swagger_schema :homepage_section_response do + key :required, [:response_code, :response_message] + property :response_code do + key :type, :integer + end + property :response_message do + key :type, :string + end + property :response_data do + key :'$ref', :homepage_section + end + end + + swagger_schema :homepage_section do + property :id do + key :type, :integer + end + property :title do + key :type, :string + end + property :section_type do + key :type, :string + end + property :content do + key :type, :string + end + property :position do + key :type, :integer + end + property :is_visible do + key :type, :boolean + end + property :settings do + key :type, :object + end + property :created_at do + key :type, :string + key :format, 'date-time' + end + property :updated_at do + key :type, :string + key :format, 'date-time' + end + end + + def index + visible_only = params[:visible_only].nil? ? true : params[:visible_only] == 'true' + + @sections = visible_only ? HomepageSection.visible : HomepageSection.all + @sections = @sections.order(position: :asc) + + count = @sections.count + offset = params[:offset].to_i + + render_object_success(@sections, "Homepage sections retrieved successfully", :homepage_sections, count, offset) + end + + def show + @section = HomepageSection.find_by(id: params[:id]) + + if @section + singular_success_model(200, "Homepage section retrieved successfully", section_data(@section)) + else + error_model(404, "Homepage section not found") + end + end + + def create + @section = HomepageSection.new(homepage_section_params) + + if @section.save + singular_success_model(201, "Homepage section created successfully", section_data(@section)) + else + error_model(400, @section.errors.full_messages.join(', ')) + end + end + + def update + @section = HomepageSection.find_by(id: params[:id]) + + if @section.nil? + error_model(404, "Homepage section not found") + elsif @section.update(homepage_section_params) + singular_success_model(200, "Homepage section updated successfully", section_data(@section)) + else + error_model(400, @section.errors.full_messages.join(', ')) + end + end + + def destroy + @section = HomepageSection.find_by(id: params[:id]) + + if @section.nil? + error_model(404, "Homepage section not found") + elsif @section.destroy + success_model(200, "Homepage section deleted successfully") + else + error_model(400, "Failed to delete homepage section") + end + end + + private + + def homepage_section_params + params.require(:homepage_section).permit( + :title, + :section_type, + :content, + :position, + :is_visible, + settings: {} + ) + end + + def section_data(section) + { + id: section.id, + title: section.title, + section_type: section.section_type, + content: section.content, + position: section.position, + is_visible: section.is_visible, + settings: section.settings, + created_at: section.created_at, + updated_at: section.updated_at + } + end +end diff --git a/app/controllers/spree/api/v1/live_stream_controller.rb b/app/controllers/spree/api/v1/live_stream_controller.rb index e2d6b095..f6dec1a7 100644 --- a/app/controllers/spree/api/v1/live_stream_controller.rb +++ b/app/controllers/spree/api/v1/live_stream_controller.rb @@ -88,6 +88,32 @@ class LiveStreamController < Spree::Api::BaseController key :type, :integer end end + property :contact_ids do + key :type, :array + items do + key :type, :integer + end + end + property :actor do + key :'$ref', :user_detail + end + end + swagger_schema :user_detail do + property :id do + key :type, :integer + end + property :email do + key :type, :string + end + property :first_name do + key :type, :string + end + property :last_name do + key :type, :string + end + property :phone do + key :type, :string + end end def index @@ -153,5 +179,130 @@ def show error_model(400, Spree.t('live_stream.live_stream_not_found')) end end + + swagger_path "/live_stream/{id}/add_watcher" do + operation :post do + key :summary, "Add Watcher to Live Stream" + key :description, "Add a contact/visitor as a watcher to the live stream" + key :tags, ['LiveStream'] + parameter do + key :name, :'X-Spree-Token' + key :description, "User API Key" + key :type, :string + key :in, :header + key :required, false + end + parameter do + key :name, :id + key :in, :path + key :description, 'ID of live stream' + key :required, true + key :type, :integer + end + parameter do + key :name, :contact_id + key :in, :formData + key :description, 'Contact ID to add as watcher' + key :required, true + key :type, :integer + end + response 200 do + key :description, "Successfully added watcher" + schema do + key :'$ref', :single_live_stream_response + end + end + response 400 do + key :description, "Error" + schema do + key :'$ref', :common_response_model + end + end + end + end + + def add_watcher + live_stream = LiveStream.find_by_id(params[:id]) + unless live_stream + return error_model(400, Spree.t('live_stream.live_stream_not_found')) + end + + contact = Contact.find_by_id(params[:contact_id]) + unless contact + return error_model(400, "Contact not found") + end + + # Check if already watching + existing = LiveStreamContact.find_by(live_stream_id: live_stream.id, contact_id: contact.id) + if existing + return singular_success_model(200, "Already watching", live_stream_detail(live_stream.id)) + end + + # Add watcher + LiveStreamContact.create(live_stream_id: live_stream.id, contact_id: contact.id) + singular_success_model(200, "Watcher added successfully", live_stream_detail(live_stream.id)) + end + + swagger_path "/live_stream/{id}/remove_watcher" do + operation :post do + key :summary, "Remove Watcher from Live Stream" + key :description, "Remove a contact/visitor from watching the live stream" + key :tags, ['LiveStream'] + parameter do + key :name, :'X-Spree-Token' + key :description, "User API Key" + key :type, :string + key :in, :header + key :required, false + end + parameter do + key :name, :id + key :in, :path + key :description, 'ID of live stream' + key :required, true + key :type, :integer + end + parameter do + key :name, :contact_id + key :in, :formData + key :description, 'Contact ID to remove from watchers' + key :required, true + key :type, :integer + end + response 200 do + key :description, "Successfully removed watcher" + schema do + key :'$ref', :single_live_stream_response + end + end + response 400 do + key :description, "Error" + schema do + key :'$ref', :common_response_model + end + end + end + end + + def remove_watcher + live_stream = LiveStream.find_by_id(params[:id]) + unless live_stream + return error_model(400, Spree.t('live_stream.live_stream_not_found')) + end + + contact = Contact.find_by_id(params[:contact_id]) + unless contact + return error_model(400, "Contact not found") + end + + # Remove watcher + watcher = LiveStreamContact.find_by(live_stream_id: live_stream.id, contact_id: contact.id) + if watcher + watcher.destroy + singular_success_model(200, "Watcher removed successfully", live_stream_detail(live_stream.id)) + else + error_model(400, "Watcher not found") + end + end end end diff --git a/app/controllers/spree/api/v1/users_controller.rb b/app/controllers/spree/api/v1/users_controller.rb index e23a7940..2fa3ee67 100644 --- a/app/controllers/spree/api/v1/users_controller.rb +++ b/app/controllers/spree/api/v1/users_controller.rb @@ -4,7 +4,8 @@ module V1 class UsersController < Spree::Api::BaseController include Swagger::Blocks include Response - before_action :authenticate_user, :except => [:sign_up, :sign_in] + include Spree::Api::V1::GlobalHelper + before_action :authenticate_user, :except => [:sign_up, :sign_in, :profile] swagger_path "/users/sign_up" do operation :post do @@ -148,6 +149,283 @@ def sign_in end + swagger_path "/users/{id}/profile" do + operation :get do + key :summary, "GET USER PROFILE" + key :description, "Get public user profile with optional authentication for extended info" + key :tags, ['Users'] + + parameter do + key :name, :id + key :in, :path + key :description, 'User ID' + key :required, true + key :type, :integer + end + + parameter do + key :name, 'token' + key :in, :query + key :description, 'Authentication token (optional - provides extended info if viewing own profile)' + key :required, false + key :type, :string + end + + response 200 do + key :description, 'Successful' + schema do + key :'$ref', :user_profile_response + end + end + + response 404 do + key :description, 'User not found' + end + end + end + + swagger_schema :user_profile_response do + key :required, [:response_code, :response_message] + property :response_code do + key :type, :integer + end + property :response_message do + key :type, :string + end + property :response_data do + key :'$ref', :user_profile + end + end + + swagger_schema :user_profile do + property :id do + key :type, :integer + end + property :email do + key :type, :string + end + property :first_name do + key :type, :string + end + property :last_name do + key :type, :string + end + property :followers_count do + key :type, :integer + end + property :following_count do + key :type, :integer + end + property :is_following do + key :type, :boolean + key :description, 'True if current user is following this profile (requires authentication)' + end + property :public_favorites do + key :type, :array + items do + key :'$ref', :favorite_product + end + end + end + + swagger_schema :favorite_product do + property :id do + key :type, :integer + end + property :variant_id do + key :type, :integer + end + property :product_id do + key :type, :integer + end + property :name do + key :type, :string + end + property :slug do + key :type, :string + end + property :price do + key :type, :string + end + end + + def profile + user = Spree::User.find_by_id(params[:id]) + unless user + return error_model(404, "User not found") + end + + # Get public favorites with product details + public_favorites = user.favorites.public_favorites.includes(variant: :product).recent.map do |fav| + { + id: fav.id, + variant_id: fav.variant_id, + product_id: fav.product&.id, + name: fav.product&.name, + slug: fav.product&.slug, + price: fav.variant&.price&.to_s + } + end + + # Check if current user is following this profile + is_following = @current_api_user.present? ? UserFollow.following?(@current_api_user, user) : false + + profile_data = { + id: user.id, + email: user.email, + first_name: user.bill_address&.firstname || "", + last_name: user.bill_address&.lastname || "", + followers_count: user.followers.count, + following_count: user.followings.count, + is_following: is_following, + public_favorites: public_favorites + } + + singular_success_model(200, "User profile retrieved successfully", profile_data) + end + + swagger_path "/users/{id}/follow" do + operation :post do + key :summary, "FOLLOW USER" + key :description, "Follow a user" + key :tags, ['Users'] + + parameter do + key :name, :id + key :in, :path + key :description, 'User ID to follow' + key :required, true + key :type, :integer + end + + parameter do + key :name, 'token' + key :in, :query + key :description, 'Authentication token' + key :required, true + key :type, :string + end + + response 200 do + key :description, 'Successfully followed user' + schema do + key :'$ref', :follow_response + end + end + + response 400 do + key :description, 'Error' + end + + response 401 do + key :description, 'Unauthorized' + end + end + end + + swagger_path "/users/{id}/unfollow" do + operation :post do + key :summary, "UNFOLLOW USER" + key :description, "Unfollow a user" + key :tags, ['Users'] + + parameter do + key :name, :id + key :in, :path + key :description, 'User ID to unfollow' + key :required, true + key :type, :integer + end + + parameter do + key :name, 'token' + key :in, :query + key :description, 'Authentication token' + key :required, true + key :type, :string + end + + response 200 do + key :description, 'Successfully unfollowed user' + schema do + key :'$ref', :follow_response + end + end + + response 400 do + key :description, 'Error' + end + + response 401 do + key :description, 'Unauthorized' + end + end + end + + swagger_schema :follow_response do + key :required, [:response_code, :response_message] + property :response_code do + key :type, :integer + end + property :response_message do + key :type, :string + end + property :response_data do + property :is_following do + key :type, :boolean + end + property :followers_count do + key :type, :integer + end + end + end + + def follow + user_to_follow = Spree::User.find_by_id(params[:id]) + unless user_to_follow + return error_model(404, "User not found") + end + + if @current_api_user.id == user_to_follow.id + return error_model(400, "Cannot follow yourself") + end + + # Check if already following + if UserFollow.following?(@current_api_user, user_to_follow) + return singular_success_model(200, "Already following this user", { + is_following: true, + followers_count: user_to_follow.followers.count + }) + end + + # Create follow + UserFollow.create(follower: @current_api_user, following: user_to_follow) + + singular_success_model(200, "Successfully followed user", { + is_following: true, + followers_count: user_to_follow.followers.count + }) + end + + def unfollow + user_to_unfollow = Spree::User.find_by_id(params[:id]) + unless user_to_unfollow + return error_model(404, "User not found") + end + + # Find and destroy follow relationship + follow = UserFollow.find_by(follower: @current_api_user, following: user_to_unfollow) + if follow + follow.destroy + singular_success_model(200, "Successfully unfollowed user", { + is_following: false, + followers_count: user_to_unfollow.followers.count + }) + else + error_model(400, "Not following this user") + end + end + def user_params params.require(:user).permit(:email, :password, :password_confirmation) end diff --git a/app/controllers/spree/api/v2/storefront/cart_controller_decorator.rb b/app/controllers/spree/api/v2/storefront/cart_controller_decorator.rb new file mode 100644 index 00000000..dff7b208 --- /dev/null +++ b/app/controllers/spree/api/v2/storefront/cart_controller_decorator.rb @@ -0,0 +1,31 @@ +module Spree + module Api + module V2 + module Storefront + module CartControllerDecorator + def remove_line_item + spree_authorize! :update, spree_current_order, order_token + + line_item.destroy + spree_current_order.recalculate + + render_serialized_payload { serialized_current_order } + end + + private + + # Override to accept :id parameter instead of :line_item_id + def line_item + @line_item ||= spree_current_order.line_items.find(params[:id] || params[:line_item_id]) + end + end + end + end + end +end + +if defined?(Spree::Api::V2::Storefront::CartController) + Spree::Api::V2::Storefront::CartController.class_eval do + prepend Spree::Api::V2::Storefront::CartControllerDecorator + end +end \ No newline at end of file diff --git a/app/models/favorite.rb b/app/models/favorite.rb new file mode 100644 index 00000000..16a826f7 --- /dev/null +++ b/app/models/favorite.rb @@ -0,0 +1,34 @@ +class Favorite < Spree::Base + belongs_to :user, class_name: 'Spree::User' + belongs_to :variant, class_name: 'Spree::Variant' + + has_one :product, through: :variant + + validates :user_id, uniqueness: { scope: :variant_id, message: "has already favorited this variant" } + + # Ransackable attributes for search + self.whitelisted_ransackable_attributes = %w[user_id variant_id created_at is_public] + + # Scopes + scope :recent, -> { order(created_at: :desc) } + scope :for_user, ->(user) { where(user: user) } + scope :for_variant, ->(variant) { where(variant: variant) } + scope :public_favorites, -> { where(is_public: true) } + + # Check if user has favorited a variant + def self.favorited?(user, variant) + exists?(user: user, variant: variant) + end + + # Toggle favorite (add if not exists, remove if exists) + def self.toggle(user, variant) + favorite = find_by(user: user, variant: variant) + if favorite + favorite.destroy + false + else + create(user: user, variant: variant) + true + end + end +end diff --git a/app/models/homepage_section.rb b/app/models/homepage_section.rb new file mode 100644 index 00000000..53ffcd77 --- /dev/null +++ b/app/models/homepage_section.rb @@ -0,0 +1,55 @@ +class HomepageSection < Spree::Base + # Validations + validates :title, presence: true + validates :section_type, presence: true + validates :position, presence: true, numericality: { only_integer: true } + + # Scopes + default_scope { order(position: :asc) } + scope :visible, -> { where(is_visible: true) } + scope :by_type, ->(type) { where(section_type: type) } + + # Ransack searchable attributes + self.whitelisted_ransackable_attributes = %w[title section_type] + self.whitelisted_ransackable_scopes = %w[search_homepage_sections] + + # Available section types + SECTION_TYPES = %w[ + hero + features + products + content + testimonials + gallery + call_to_action + newsletter + video + custom + ].freeze + + validates :section_type, inclusion: { in: SECTION_TYPES } + + # Search scope + def self.search_homepage_sections(query) + if defined?(SpreeGlobalize) + joins(:translations).order(:title).where( + "LOWER(#{table_name}.title) LIKE LOWER(:query) OR LOWER(content) LIKE LOWER(:query)", + query: "%#{query}%" + ).distinct + else + where( + "LOWER(#{table_name}.title) LIKE LOWER(:query) OR LOWER(content) LIKE LOWER(:query)", + query: "%#{query}%" + ) + end + end + + # Set position before create + before_create :set_position + + private + + def set_position + self.position ||= HomepageSection.maximum(:position).to_i + 1 + end +end diff --git a/app/models/live_stream.rb b/app/models/live_stream.rb index 536edb48..983f13ac 100644 --- a/app/models/live_stream.rb +++ b/app/models/live_stream.rb @@ -1,6 +1,8 @@ class LiveStream < Spree::Base has_many :live_stream_products, dependent: :destroy has_many :products, class_name: 'Spree::Product', through: :live_stream_products, dependent: :destroy + has_many :live_stream_contacts, dependent: :destroy + has_many :contacts, through: :live_stream_contacts belongs_to :thread_table, optional: true belongs_to :actor, class_name: 'Spree::User' diff --git a/app/models/spree/user.rb b/app/models/spree/user.rb index e6b2132a..14bd7043 100644 --- a/app/models/spree/user.rb +++ b/app/models/spree/user.rb @@ -12,6 +12,15 @@ class User < Spree::Base acts_as_paranoid has_many :sent_messages, class_name: 'Message',as: :sender, dependent: :destroy has_many :received_messages, class_name: 'Message', as: :receiver, dependent: :destroy + has_many :favorites, dependent: :destroy + has_many :favorite_variants, through: :favorites, source: :variant + + # Follow relationships + has_many :follower_relationships, class_name: 'UserFollow', foreign_key: :following_id, dependent: :destroy + has_many :followers, through: :follower_relationships, source: :follower + has_many :following_relationships, class_name: 'UserFollow', foreign_key: :follower_id, dependent: :destroy + has_many :followings, through: :following_relationships, source: :following + after_destroy :scramble_email_and_password before_validation :set_login diff --git a/app/models/user_follow.rb b/app/models/user_follow.rb new file mode 100644 index 00000000..0ee464e3 --- /dev/null +++ b/app/models/user_follow.rb @@ -0,0 +1,44 @@ +class UserFollow < Spree::Base + belongs_to :follower, class_name: 'Spree::User' + belongs_to :following, class_name: 'Spree::User' + + validates :follower_id, presence: true + validates :following_id, presence: true + validates :follower_id, uniqueness: { scope: :following_id, message: "already following this user" } + validate :cannot_follow_self + + # Ransackable attributes for search + self.whitelisted_ransackable_attributes = %w[follower_id following_id created_at] + + # Scopes + scope :recent, -> { order(created_at: :desc) } + scope :for_follower, ->(user) { where(follower: user) } + scope :for_following, ->(user) { where(following: user) } + + # Check if user is following another user + def self.following?(follower, following) + exists?(follower: follower, following: following) + end + + # Toggle follow (add if not exists, remove if exists) + def self.toggle(follower, following) + return false if follower == following + + follow = find_by(follower: follower, following: following) + if follow + follow.destroy + false + else + create(follower: follower, following: following) + true + end + end + + private + + def cannot_follow_self + if follower_id == following_id + errors.add(:following_id, "cannot follow yourself") + end + end +end diff --git a/app/views/apidocs/swagger_ui.html.erb b/app/views/apidocs/swagger_ui.html.erb index 39d261ec..02b29b7d 100644 --- a/app/views/apidocs/swagger_ui.html.erb +++ b/app/views/apidocs/swagger_ui.html.erb @@ -54,7 +54,7 @@ // Begin Swagger UI call region const ui = SwaggerUIBundle({ - url: "<%= Rails.env == 'staging' ? 'https://dna-admin-staging.instinct.is/apidocs.json' : Rails.env == 'production' ? 'https://admin.instinct.is/apidocs.json' : 'https://dna-admin-dev.instinct.is/apidocs.json' %>", + url: "<%= Rails.env == 'staging' ? 'https://dna-admin-staging.instinct.is/apidocs.json' : Rails.env == 'production' ? 'https://admin.instinct.is/apidocs.json' : 'http://localhost:3001/apidocs.json' %>", dom_id: '#swagger-ui', deepLinking: true, presets: [ diff --git a/app/views/spree/admin/contacts/_edit_resource_links.html.erb b/app/views/spree/admin/contacts/_edit_resource_links.html.erb index 9708cbd2..d0e5202e 100644 --- a/app/views/spree/admin/contacts/_edit_resource_links.html.erb +++ b/app/views/spree/admin/contacts/_edit_resource_links.html.erb @@ -1,5 +1,5 @@
<%= button Spree.t('actions.update'), 'save.svg', 'submit', {class: 'btn-success', data: { disable_with: "#{ Spree.t(:updating) }..." }} %> - - <%#= button_link_to Spree.t('actions.cancel'), admin_contacts_path, icon: 'cancel.svg' %> + <%= Spree.t(:or) %> + <%= button_link_to Spree.t('actions.cancel'), admin_contacts_path, icon: 'cancel.svg' %>
diff --git a/app/views/spree/admin/contacts/_new_resource_links.html.erb b/app/views/spree/admin/contacts/_new_resource_links.html.erb index f36268eb..4dbd8af6 100644 --- a/app/views/spree/admin/contacts/_new_resource_links.html.erb +++ b/app/views/spree/admin/contacts/_new_resource_links.html.erb @@ -1,5 +1,5 @@
<%= button Spree.t('actions.create'), 'save.svg', 'submit', {class: 'btn-success', data: { disable_with: "#{ Spree.t(:saving) }..." }} %> <%= Spree.t(:or) %> - <%= button_link_to Spree.t('actions.cancel'), admin_live_stream_index_path, icon: 'cancel.svg' %> + <%= button_link_to Spree.t('actions.cancel'), admin_contacts_path, icon: 'cancel.svg' %>
diff --git a/app/views/spree/admin/homepage_sections/edit.html.erb b/app/views/spree/admin/homepage_sections/edit.html.erb new file mode 100644 index 00000000..6c5746ba --- /dev/null +++ b/app/views/spree/admin/homepage_sections/edit.html.erb @@ -0,0 +1,83 @@ +<%= render partial: 'spree/admin/shared/error_messages', locals: { target: @homepage_section } %> + +<% content_for :page_title do %> + <%= link_to 'Homepage Sections', spree.admin_homepage_sections_path %> / + Edit: <%= @homepage_section.title %> +<% end %> + +<%= form_for [:admin, @homepage_section], html: { class: 'form' } do |f| %> +
+ + <%= f.field_container :title, class: ['form-group'] do %> + <%= f.label :title, 'Title' %> * + <%= f.text_field :title, class: 'form-control', required: true %> + <%= f.error_message_on :title %> + <% end %> + + <%= f.field_container :section_type, class: ['form-group'] do %> + <%= f.label :section_type, 'Section Type' %> * + <%= f.select :section_type, + options_for_select(HomepageSection::SECTION_TYPES.map { |t| [t.titleize, t] }, @homepage_section.section_type), + {}, + { class: 'form-control select2', required: true } %> + <%= f.error_message_on :section_type %> + Choose the type of content section + <% end %> + + <%= f.field_container :content, class: ['form-group'] do %> + <%= f.label :content, 'Content (HTML/Text)' %> + <%= f.text_area :content, class: 'form-control', rows: 10, id: 'homepage_section_content' %> + <%= f.error_message_on :content %> + Main content for this section + <% end %> + + <%= f.field_container :settings, class: ['form-group'] do %> + <%= f.label :settings, 'Settings (JSON)' %> + <%= f.text_area :settings, class: 'form-control', rows: 6, placeholder: '{ "key": "value" }' %> + <%= f.error_message_on :settings %> + Additional configuration in JSON format. Example: {"background_color": "#fff", "text_color": "#000"} + <% end %> + + <%= f.field_container :position, class: ['form-group'] do %> + <%= f.label :position, 'Position' %> + <%= f.number_field :position, class: 'form-control', min: 1 %> + <%= f.error_message_on :position %> + Order position on the page (lower numbers appear first) + <% end %> + + <%= f.field_container :is_visible, class: ['form-group'] do %> +
+ <%= f.check_box :is_visible, class: 'form-check-input' %> + <%= f.label :is_visible, 'Visible on website', class: 'form-check-label' %> +
+ <%= f.error_message_on :is_visible %> + <% end %> + +
+ <%= button Spree.t(:update), 'save.svg' %> + <%= button_link_to Spree.t(:cancel), admin_homepage_sections_path, icon: 'cancel.svg' %> +
+ +
+<% end %> + + + diff --git a/app/views/spree/admin/homepage_sections/index.html.erb b/app/views/spree/admin/homepage_sections/index.html.erb new file mode 100644 index 00000000..48578e67 --- /dev/null +++ b/app/views/spree/admin/homepage_sections/index.html.erb @@ -0,0 +1,88 @@ +<% content_for :page_title do %> + <%= Spree.t(:homepage_sections) %> +<% end %> + +<% content_for :page_actions do %> + <%= button_link_to 'Add Homepage Section', spree.new_admin_homepage_section_path, class: "btn-success", icon: 'add.svg' if can?(:create, HomepageSection) %> +<% end %> + +<% content_for :table_filter do %> +
+ <%= search_form_for @q, url: admin_homepage_sections_path do |f| %> +
+
+
+ <%= f.label :title_cont, 'Title' %> + <%= f.text_field :title_cont, size: 15, class: "form-control js-quick-search-target js-filterable" %> +
+
+
+
+ <%= f.label :section_type_eq, 'Section Type' %> + <%= f.select :section_type_eq, options_for_select([['All', '']] + HomepageSection::SECTION_TYPES.map { |t| [t.titleize, t] }, params.dig(:q, :section_type_eq)), {}, class: "form-control" %> +
+
+
+
+ <%= button Spree.t(:search), 'search.svg' %> +
+ <% end %> +
+<% end %> + +<% if @collection.present? %> +
+ + + + + + + + + + + + + <% @collection.each do |section| %> + + + + + + + + + <% end %> + +
Position<%= sort_link(@q, :title, "Title", default_order: :asc) %>Section TypeVisibleOrderActions
<%= section.position %><%= link_to section.title, edit_admin_homepage_section_path(section) %><%= section.section_type.titleize %> + <% if section.is_visible %> + Visible + <% else %> + Hidden + <% end %> + +
+ <%= button_to '↑', move_up_admin_homepage_section_path(section), + method: :post, + class: 'btn btn-sm btn-outline-secondary', + disabled: section.position == @collection.minimum(:position), + title: 'Move Up' %> + <%= button_to '↓', move_down_admin_homepage_section_path(section), + method: :post, + class: 'btn btn-sm btn-outline-secondary', + disabled: section.position == @collection.maximum(:position), + title: 'Move Down' %> +
+
+ <%= link_to_with_icon 'edit.svg', 'Edit', edit_admin_homepage_section_path(section), class: 'btn btn-sm btn-primary', no_text: true if can?(:edit, section) %> + <%= link_to_with_icon 'delete.svg', 'Delete', admin_homepage_section_path(section), method: :delete, data: { confirm: 'Are you sure?' }, class: 'btn btn-sm btn-danger', no_text: true if can?(:delete, section) %> +
+
+ + <%= paginate @collection, theme: 'admin-twitter-bootstrap-4' %> +<% else %> + +<% end %> diff --git a/app/views/spree/admin/homepage_sections/new.html.erb b/app/views/spree/admin/homepage_sections/new.html.erb new file mode 100644 index 00000000..4e3710ee --- /dev/null +++ b/app/views/spree/admin/homepage_sections/new.html.erb @@ -0,0 +1,76 @@ +<%= render partial: 'spree/admin/shared/error_messages', locals: { target: @homepage_section } %> + +<% content_for :page_title do %> + <%= link_to 'Homepage Sections', spree.admin_homepage_sections_path %> / + New Section +<% end %> + +<%= form_for [:admin, @homepage_section], html: { class: 'form' } do |f| %> +
+ + <%= f.field_container :title, class: ['form-group'] do %> + <%= f.label :title, 'Title' %> * + <%= f.text_field :title, class: 'form-control', required: true %> + <%= f.error_message_on :title %> + <% end %> + + <%= f.field_container :section_type, class: ['form-group'] do %> + <%= f.label :section_type, 'Section Type' %> * + <%= f.select :section_type, + options_for_select(HomepageSection::SECTION_TYPES.map { |t| [t.titleize, t] }), + { prompt: 'Select section type...' }, + { class: 'form-control select2', required: true } %> + <%= f.error_message_on :section_type %> + Choose the type of content section + <% end %> + + <%= f.field_container :content, class: ['form-group'] do %> + <%= f.label :content, 'Content (HTML/Text)' %> + <%= f.text_area :content, class: 'form-control', rows: 10, id: 'homepage_section_content' %> + <%= f.error_message_on :content %> + Main content for this section + <% end %> + + <%= f.field_container :settings, class: ['form-group'] do %> + <%= f.label :settings, 'Settings (JSON)' %> + <%= f.text_area :settings, class: 'form-control', rows: 6, placeholder: '{ "key": "value" }' %> + <%= f.error_message_on :settings %> + Additional configuration in JSON format. Example: {"background_color": "#fff", "text_color": "#000"} + <% end %> + + <%= f.field_container :is_visible, class: ['form-group'] do %> +
+ <%= f.check_box :is_visible, class: 'form-check-input' %> + <%= f.label :is_visible, 'Visible on website', class: 'form-check-label' %> +
+ <%= f.error_message_on :visible %> + <% end %> + +
+ <%= button Spree.t(:create), 'save.svg' %> + <%= button_link_to Spree.t(:cancel), admin_homepage_sections_path, icon: 'cancel.svg' %> +
+ +
+<% end %> + + + diff --git a/app/views/spree/admin/messages/conversations.html.erb b/app/views/spree/admin/messages/conversations.html.erb index 0b393f05..6faf9e49 100644 --- a/app/views/spree/admin/messages/conversations.html.erb +++ b/app/views/spree/admin/messages/conversations.html.erb @@ -3,15 +3,71 @@ max-width: 100%!important; } - .list-group-item span { - width: 47%; - float: left; - margin-left: 3%; + .conversation-list { + max-height: calc(100vh - 200px); + overflow-y: auto; } - .list-group-item { - display: inline-block; + .conversation-item { + padding: 15px; + border-bottom: 1px solid #e9ecef; + cursor: pointer; + transition: background-color 0.2s; } + + .conversation-item:hover { + background-color: #f8f9fa; + } + + .conversation-item.active { + background-color: #e3f2fd; + } + + .participant-info { + display: flex; + align-items: center; + margin-bottom: 8px; + } + + .participant-avatar { + width: 40px; + height: 40px; + border-radius: 50%; + margin-right: 10px; + } + + .participant-details { + flex-grow: 1; + } + + .participant-name { + font-weight: 600; + font-size: 14px; + margin-bottom: 2px; + } + + .participant-role { + font-size: 11px; + color: #6c757d; + text-transform: uppercase; + letter-spacing: 0.5px; + } + + .participant-role.customer { + color: #007bff; + } + + .participant-role.support { + color: #28a745; + } + + .vs-separator { + font-size: 12px; + color: #adb5bd; + font-weight: 500; + margin: 5px 0; + } + .card{ width: 100%; height: 100%; @@ -60,6 +116,15 @@ .border-top { border-top: 1px solid #dee2e6!important; } + + .search-box { + position: sticky; + top: 0; + background: white; + z-index: 10; + padding: 15px; + border-bottom: 1px solid #dee2e6; + }
@@ -67,30 +132,77 @@
-
-
-
- -
-
+ - <% @users_array.each do |user_hash| %> - <% users = fetch_users(user_hash) %> - <%= link_to spree.conversations_admin_messages_path(users: users), class: "list-group-item list-group-item-action border-0" do %> - <% users.each do |user| %> - - -
- Vanessa Tucker -
- <%= user[:name] %> -
Online
-
+ +
+ <% @users_array.each do |user_hash| %> + <% users = fetch_users(user_hash) %> + <% + # Determine who is customer and who is support + user_1 = users[0] + user_2 = users[1] + + # Assume Contact = customer, Spree::User = support staff + customer = user_1[:type] == 'Contact' ? user_1 : user_2 + support = user_1[:type] == 'User' ? user_1 : user_2 + + # Determine if current conversation is active + is_active = params[:users].present? && + params[:users].any? { |u| u[:id].to_s == user_1[:id].to_s } && + params[:users].any? { |u| u[:id].to_s == user_2[:id].to_s } + %> + + <%= link_to spree.conversations_admin_messages_path(users: users), + class: "conversation-item #{is_active ? 'active' : ''}", + style: "text-decoration: none; color: inherit; display: block;", + data: { search_text: "#{customer[:name]} #{support[:name]}" } do %> + + +
+ <%= customer[:name] %> +
+
<%= customer[:name] %>
+
👤 Customer
+
+
+ +
↕️
+ + +
+ <%= support[:name] %> +
+
<%= support[:name] %>
+
🎧 Support Staff
- +
<% end %> <% end %> - <% end %> +
+ + +
@@ -106,25 +218,38 @@ <% if @threads.present? %> <% @threads.each do |thread| %> <% thread.each do |message| %> + <% + sender_name = message.sender_type == "Spree::User" ? message.sender.email : message.sender.full_name + is_support = message.sender_type == "Spree::User" + avatar_bg = is_support ? "28a745" : "007bff" + role_label = is_support ? "🎧 Support" : "👤 Customer" + %> + <% if message.sender == @user_1 %>
- Chris Wood -
<%= distance_of_time_in_words(message.created_at, Time.now) %>
+ <%= sender_name %> +
<%= distance_of_time_in_words(message.created_at, Time.now) %> ago
-
<%= sender = message.sender_type == "Spree::User" ? message.sender.email : message.sender.full_name %>
+
+ <%= sender_name %> + <%= role_label %> +
<%= message.message %>
<% elsif message.sender == @user_2 %>
- Sharon Lessman -
<%= distance_of_time_in_words(message.created_at, Time.now) %>
+ <%= sender_name %> +
<%= distance_of_time_in_words(message.created_at, Time.now) %> ago
-
<%= sender = message.sender_type == "Spree::User" ? message.sender.email : message.sender.full_name %>
+
+ <%= sender_name %> + <%= role_label %> +
<%= message.message %>
diff --git a/app/views/spree/admin/reports/dashboard/index.html.erb b/app/views/spree/admin/reports/dashboard/index.html.erb new file mode 100644 index 00000000..b3c948e2 --- /dev/null +++ b/app/views/spree/admin/reports/dashboard/index.html.erb @@ -0,0 +1,290 @@ +<% content_for :page_title do %> + <%= Spree.t(:analytics_dashboard) %> +<% end %> + +<% content_for :page_actions do %> +
  • + <%= form_tag admin_dashboard_analytics_path, method: :get, class: 'form-inline' do %> + <%= select_tag :date_range, options_for_select([ + ['Last 7 Days', '7'], + ['Last 30 Days', '30'], + ['Last 90 Days', '90'], + ['Last 365 Days', '365'] + ], params[:date_range] || '30'), class: 'form-control', onchange: 'this.form.submit()' %> + <% end %> +
  • +<% end %> + +
    + +
    +
    +
    +
    Total Orders
    +

    <%= @total_orders %>

    +

    Last <%= @date_range %> days

    +
    +
    +
    + +
    +
    +
    +
    Total Revenue
    +

    <%= number_to_currency(@total_revenue) %>

    +

    Avg: <%= number_to_currency(@average_order_value) %>

    +
    +
    +
    + +
    +
    +
    +
    New Users
    +

    <%= @new_users %>

    +

    Total: <%= @total_users %>

    +
    +
    +
    + +
    +
    +
    +
    Messages
    +

    <%= @total_messages %>

    +

    Unread: <%= @unread_messages %>

    +
    +
    +
    +
    + +
    + +
    +
    +
    +
    Orders & Revenue Over Time
    +
    +
    + +
    +
    +
    + + +
    +
    +
    +
    Order Status Distribution
    +
    +
    + +
    +
    +
    +
    + +
    + +
    +
    +
    +
    User & Contact Growth
    +
    +
    + +
    +
    +
    + + +
    +
    +
    +
    Message Activity
    +
    +
    + +
    +
    +
    +
    + +
    + +
    +
    +
    +
    Top Selling Products
    +
    +
    + + + + + + + + + + <% @top_products.each do |product| %> + + + + + + <% end %> + +
    SKUQuantity SoldTotal Sales
    <%= product.sku %><%= product.total_quantity %><%= number_to_currency(product.total_sales) %>
    +
    +
    +
    +
    + + + diff --git a/app/views/spree/admin/reports/index.html.erb b/app/views/spree/admin/reports/index.html.erb index 40b48597..1c946bc1 100644 --- a/app/views/spree/admin/reports/index.html.erb +++ b/app/views/spree/admin/reports/index.html.erb @@ -1,21 +1,290 @@ <% content_for :page_title do %> - <%= Spree.t(:reports) %> + <%= Spree.t(:analytics_dashboard) %> <% end %> -
    - - - - - - - - - <% @reports.each do |key, value| %> - - - - - <% end %> - -
    <%= Spree.t(:name) %><%= Spree.t(:description) %>
    <%= link_to value[:name], send("#{key}_admin_reports_url".to_sym) %><%= value[:description] %>
    + +<% content_for :page_actions do %> +
  • + <%= form_tag admin_reports_path, method: :get, class: 'form-inline' do %> + <%= select_tag :date_range, options_for_select([ + ['Last 7 Days', '7'], + ['Last 30 Days', '30'], + ['Last 90 Days', '90'], + ['Last 365 Days', '365'] + ], params[:date_range] || '30'), class: 'form-control', onchange: 'this.form.submit()' %> + <% end %> +
  • +<% end %> + +
    + +
    +
    +
    +
    Total Orders
    +

    <%= @total_orders %>

    +

    Last <%= @date_range %> days

    +
    +
    +
    + +
    +
    +
    +
    Total Revenue
    +

    <%= number_to_currency(@total_revenue) %>

    +

    Avg: <%= number_to_currency(@average_order_value) %>

    +
    +
    +
    + +
    +
    +
    +
    New Users
    +

    <%= @new_users %>

    +

    Total: <%= @total_users %>

    +
    +
    +
    + +
    +
    +
    +
    Messages
    +

    <%= @total_messages %>

    +

    Unread: <%= @unread_messages %>

    +
    +
    +
    +
    + +
    + +
    +
    +
    +
    Orders & Revenue Over Time
    +
    +
    + +
    +
    +
    + + +
    +
    +
    +
    Order Status Distribution
    +
    +
    + +
    +
    +
    +
    + +
    + +
    +
    +
    +
    User & Contact Growth
    +
    +
    + +
    +
    +
    + + +
    +
    +
    +
    Message Activity
    +
    +
    + +
    +
    +
    +
    + +
    + +
    +
    +
    +
    Top Selling Products
    +
    +
    + + + + + + + + + + <% @top_products.each do |product| %> + + + + + + <% end %> + +
    SKUQuantity SoldTotal Sales
    <%= product.sku %><%= product.total_quantity %><%= number_to_currency(product.total_sales) %>
    +
    +
    +
    + + + diff --git a/app/views/spree/admin/shared/_main_menu.html.erb b/app/views/spree/admin/shared/_main_menu.html.erb index d538bbf2..8eeb24c9 100644 --- a/app/views/spree/admin/shared/_main_menu.html.erb +++ b/app/views/spree/admin/shared/_main_menu.html.erb @@ -59,11 +59,17 @@ <% end %> <% if can? :admin, Message %> -