Rails app blueprint

Jumpstart Pro

Add Avo 3 to your Jumpstart Pro app blazing fast!

We are heavily used Avo Pro in order to build a new "admin first" platform that can be managed by our non-tech team with ease.

The interface is extremely intuitive and can be extended fast with custom actions.

Paul Werther
Paul Werther
CTO, greenhats GmbH
Run the following command to quickly apply this app blueprint

If you think this blueprint needs changes please submit a PR here.

Full blueprint

# Blueprint info:
#   Link: https://avohq.io/blueprints/jumpstart-pro
#   Repository: https://github.com/avo-hq/jumpstart-pro-app-template

# List of all Avo files with path and contents
files = {"app/avo/cards/active_subscriptions.rb"=>"class Avo::Cards::ActiveSubscriptions < Avo::Cards::MetricCard\n  self.id = \"active_subscriptions\"\n  self.label = \"Active subscriptions\"\n  self.description = \"Total number of active subscriptions\"\n\n  def query\n    result ::Pay::Subscription.active.count\n  end\nend\n", "app/avo/cards/total_revenue.rb"=>"class Avo::Cards::TotalRevenue < Avo::Cards::MetricCard\n  self.id = \"total_revenue\"\n  self.label = \"Total revenue\"\n  self.prefix = \"$\"\n\n  def query\n    case arguments[:period]\n    when :last_12_months\n      result last_12_months\n    when :last_month\n      result last_month\n    when :this_month\n      result this_month\n    else\n      result total_revenue\n    end\n  end\n\n  def total_revenue\n    revenue_in_cents = ::Pay::Charge.sum(:amount)\n    refunds_in_cents = ::Pay::Charge.sum(:amount_refunded)\n    (revenue_in_cents - refunds_in_cents) / 100.0\n  end\n\n  def last_12_months\n    revenue_for_range 12.months.ago..Time.current\n  end\n\n  def last_month\n    month = Time.current.prev_month\n    revenue_for_range month.beginning_of_month..month.end_of_month\n  end\n\n  def this_month\n    month = Time.current\n    revenue_for_range month.beginning_of_month..month.end_of_month\n  end\n\n  def revenue_for_range(range)\n    revenue_in_cents = ::Pay::Charge.where(created_at: range).sum(:amount)\n    refunds_in_cents = ::Pay::Charge.where(created_at: range).sum(:amount_refunded)\n    (revenue_in_cents - refunds_in_cents) / 100.0\n  end\nend\n", "app/avo/cards/users_count.rb"=>"class Avo::Cards::UsersCount < Avo::Cards::MetricCard\n  self.id = \"users_count\"\n  self.label = \"Users count\"\n  self.description = \"Total number of users registered\"\n\n  def query\n    result ::User.all.count\n  end\nend\n", "app/avo/dashboards/overview.rb"=>"class Avo::Dashboards::Overview < Avo::Dashboards::BaseDashboard\n  self.id = \"overview\"\n  self.name = \"Overview\"\n  self.grid_cols = 4\n\n  def cards\n    divider label: \"Revenue\"\n    card Avo::Cards::TotalRevenue,\n      arguments: {\n        period: :this_month\n      },\n      label: \"This month\"\n    card Avo::Cards::TotalRevenue,\n      arguments: {\n        period: :last_month\n      },\n      label: \"Last month\"\n    card Avo::Cards::TotalRevenue,\n      arguments: {\n        period: :last_12_months\n      },\n      label: \"Last 12 months\"\n    card Avo::Cards::TotalRevenue\n\n    divider\n\n    card Avo::Cards::UsersCount\n    card Avo::Cards::ActiveSubscriptions\n  end\nend\n", "app/avo/resources/account.rb"=>"class Avo::Resources::Account < Avo::BaseResource\n  self.title = :name\n  self.includes = [:owner, :users]\n  self.search = {\n    query: -> { query.ransack(id_eq: params[:q], name_cont: params[:q], m: \"or\").result(distinct: false) },\n    item: -> {\n      {\n        title: record.name,\n        image_url: record.avatar\n      }\n    }\n  }\n\n  def fields\n    main_panel do\n      field :id, as: :id\n      field :avatar,\n        as: :file,\n        is_image: true,\n        link_to_resource: true,\n        rounded: true,\n        hide_on: :forms\n      field :owner, as: :belongs_to\n      field :name, as: :text\n      field :personal, as: :boolean\n      field :users_count, as: :text do\n        record.users.length\n      end\n\n      sidebar do\n        field :extra_billing_info, as: :textarea\n        field :created_at, as: :date_time\n        field :updated_at, as: :date_time\n      end\n    end\n\n    tabs do\n      field :subscriptions, as: :has_many\n      field :pay_customers, as: :has_many\n      field :charges, as: :has_many\n      field :account_users, as: :has_many\n      field :users, as: :has_many\n    end\n  end\nend\n", "app/avo/resources/account_user.rb"=>"class Avo::Resources::AccountUser < Avo::BaseResource\n  self.title = :id\n  self.includes = []\n\n  def fields\n    main_panel do\n      field :id, as: :id\n      field :account, as: :belongs_to\n      field :user, as: :belongs_to\n      field :roles,\n        as: :boolean_group,\n        options: {\n          admin: \"Administrator\",\n          member: \"Member\",\n        }\n      field :admin?, as: :boolean do\n        record.roles[\"admin\"] == true\n      end\n\n      sidebar do\n        field :created_at, as: :date_time\n        field :updated_at, as: :date_time\n      end\n    end\n  end\nend\n", "app/avo/resources/announcement.rb"=>"class Avo::Resources::Announcement < Avo::BaseResource\n  self.title = :title\n  self.includes = []\n  self.search = {\n    query: -> { query.ransack(id_eq: params[:q], title_cont: params[:q], m: \"or\").result(distinct: false) },\n    item: -> {\n      {\n        title: record.title,\n      }\n    }\n  }\n\n  def fields\n    field :id, as: :id\n    field :title, as: :text\n    field :description, as: :trix, always_show: true\n    field :kind, as: :badge, options: {success: \"new\", info: \"improvement\", danger: \"fix\", warning: \"update\"}, hide_on: :forms\n    field :kind, as: :select, options: Announcement::TYPES.map { |t| [t, t] }.to_h, show_on: :forms\n    field :published_at, as: :date_time\n  end\nend\n", "app/avo/resources/charge.rb"=>"class Avo::Resources::Charge < Avo::BaseResource\n  self.title = :processor_id\n  self.includes = []\n  self.model_class = Pay::Charge\n  self.search = {\n    query: -> { query.ransack(id_eq: params[:q], processor_id_cont: params[:q], m: \"or\").result(distinct: false)},\n    item: -> {\n      {\n        title: record.processor_id\n      }\n    }\n  }\n\n  def fields\n    field :id, as: :id\n    field :customer, as: :belongs_to\n    field :amount, as: :text\n    field :amount_refunded, as: :text\n    field :created_at, as: :date_time\n\n    with_options only_on: :show do\n      field :processor_id, as: :text, format_using: -> do\n        link_to value, view_context.controller.charge_processor_url(model), target: :_blank\n      rescue\n        value\n      end\n      field :subscription, as: :belongs_to\n      field :updated_at, as: :date_time\n    end\n  end\nend\n", "app/avo/resources/connected_account.rb"=>"class Avo::Resources::ConnectedAccount < Avo::BaseResource\n  self.title = :id\n  self.includes = []\n  self.model_class = ConnectedAccount\n\n  def fields\n    field :id, as: :id\n    field :user, as: :belongs_to\n    field :image_url, as: :external_image\n    field :expired?, as: :boolean, hide_on: :forms\n  end\nend\n", "app/avo/resources/customer.rb"=>"class Avo::Resources::Customer < Avo::BaseResource\n  self.title = :name\n  self.includes = [:owner]\n  self.search = {\n    query: -> { query.ransack(id_eq: params[:q], processor_id_cont: params[:q], m: \"or\").result(distinct: false) },\n    item: -> {\n      {\n        title: record.name,\n      }\n    }\n  }\n  self.model_class = Pay::Customer\n\n  def fields\n    main_panel do\n      field :id, as: :id\n      field :owner, as: :belongs_to, polymorphic_as: :owner, types: [::Account]\n\n      field :processor, as: :badge, options: {success: \"stripe\"}\n      field :processor_id, as: :text, format_using: -> do\n        link_to(value, view_context.controller.customer_processor_url(model), target: :_blank)\n      rescue\n        value\n      end\n      field :default, as: :boolean\n      field :data, as: :key_value\n\n      sidebar do\n        field :created_at, as: :date_time\n        field :updated_at, as: :date_time\n        field :deleted_at, as: :date_time\n      end\n    end\n\n    tabs do\n      field :subscriptions, as: :has_many\n      field :charges, as: :has_many\n    end\n  end\nend\n", "app/avo/resources/payment_method.rb"=>"class Avo::Resources::PaymentMethod < Avo::BaseResource\n  self.includes = []\n  self.model_class = Pay::PaymentMethod\n  self.search = {\n    query: -> { query.ransack(id_eq: params[:q], processor_id_cont: params[:q], m: \"or\").result(distinct: false) },\n    item: -> {\n      {\n        title: record.id,\n      }\n    }\n  }\n\n  def fields\n    main_panel do\n      field :id, as: :id\n      field :customer, as: :belongs_to\n      field :processor_id, as: :text\n      field :type, as: :text\n      field :default, as: :boolean\n      field :data, as: :key_value\n\n      sidebar do\n        field :extra_billing_info, as: :key_value\n        field :created_at, as: :date_time\n        field :updated_at, as: :date_time\n      end\n    end\n  end\nend\n", "app/avo/resources/plan.rb"=>"class Avo::Resources::Plan < Avo::BaseResource\n  self.title = :name\n  self.includes = []\n  self.search = {\n    query: -> { query.ransack(id_eq: params[:q], name_cont: params[:q], description_cont: params[:q], m: \"or\").result(distinct: false) },\n    item: -> {\n      {\n        title: record.name,\n      }\n    }\n  }\n\n  def fields\n    main_panel do\n      field :id, as: :id\n      field :name, as: :text\n      field :description, as: :text, hide_on: :index\n      field :hidden, as: :boolean\n      field :amount, as: :number, help: \"Price in cents\", format_using: -> do\n        if view == :edit || view == :new\n          value\n        else\n          (BigDecimal(value.to_s) / 100).round(2)\n        end\n      end\n      field :currency, as: :select, options: Pay::Currency.all.map { |iso, v| [\"\#{iso.upcase} - \#{v[\"name\"]}\", iso] }.to_h\n      field :interval, as: :select, options: [:month, :year]\n      field :interval_count, as: :number\n      field :trial_period_days, as: :number\n\n      field :features, as: :tags\n\n      sidebar do\n        field :created_at, as: :date_time\n        field :updated_at, as: :date_time\n        field :processor_details, as: :heading, label: \"Processor details\"\n        field :stripe_id, as: :text, format_using: -> do\n          if view == :edit || view == :new\n            value\n          else\n            link_to value, \"https://dashboard.stripe.com/plans/\#{value}\", target: :_blank\n          end\n        rescue\n          value\n        end\n        field :braintree_id, as: :text, format_using: -> do\n          if view == :edit || view == :new\n            value\n          else\n            link_to value, \"https://sandbox.braintreegateway.com/merchants/\#{Pay::Braintree.merchant_id}/plans/\#{id}\", target: :_blank\n          end\n        rescue\n          value\n        end\n        field :paddle_id, as: :text\n        field :fake_processor_id, as: :text\n      end\n    end\n  end\nend\n", "app/avo/resources/subscription.rb"=>"class Avo::Resources::Subscription < Avo::BaseResource\n  self.title = :processor_id\n  self.includes = [:owner, :customer]\n  self.search = {\n    query: -> { query.ransack(id_eq: params[:q], name_cont: params[:q], processor_id_cont: params[:q], m: \"or\").result(distinct: false) },\n    item: -> {\n      {\n        title: record.processor_id,\n      }\n    }\n  }\n  self.model_class = Pay::Subscription\n\n  def fields\n    main_panel do\n      field :id, as: :id\n      with_options only_on: :index do\n        field :customer, as: :belongs_to\n        field :name, as: :text\n        field :active?, as: :boolean, name: \"Active\"\n        field :cancelled?, as: :boolean, name: \"Cancelled\"\n      end\n      field :owner, as: :has_one\n      field :processor_id, as: :text, format_using: ->(id) do\n        link_to id, view_context.controller.subscription_processor_url(model), target: :_blank\n      rescue\n        id\n      end\n      field :active?, as: :boolean\n      field :processor, as: :badge, options: {success: \"Stripe\"} do |model, *args|\n        name = record.payment_processor.class.to_s.split(\"::\").second\n        if name.downcase.include? \"fake\"\n          \"Fake\"\n        else\n          name\n        end\n      rescue\n        \"Unknown\"\n      end\n\n      sidebar do\n        field :status, as: :status, readonly: true, failed_when: [:canceled], loading_when: [:trialing]\n        field :trial_ends_at_ago, as: :text\n        field :created_at, as: :date_time, readonly: true\n        field :ends_at, as: :date_time, readonly: true\n        field :processor_id, as: :text, readonly: true, as_html: true do\n          case record.customer.processor\n          when \"stripe\"\n            link_to(record.processor_id, \"https://dashboard.stripe.com/subscriptions/\#{record.processor_id}\", target: :_blank)\n          else\n            record.processor_id\n          end\n        end\n        field :processor_plan, as: :text, readonly: true\n        field :quantity, as: :number, readonly: true\n      end\n    end\n  end\nend\n", "app/avo/resources/user.rb"=>"class Avo::Resources::User < Avo::BaseResource\n  self.title = :name\n  self.includes = [:accounts]\n  self.search = {\n    query: -> { query.ransack(id_eq: params[:q], name_cont: params[:q], email_cont: params[:q], m: \"or\").result(distinct: false) },\n    item: -> {\n      {\n        title: record.name,\n      }\n    }\n  }\n\n  def fields\n    field :id, as: :id\n    field :avatar, as: :file, is_image: true, hide_on: :forms, link_to_resource: true\n    field :name, as: :text\n    field :email, as: :text\n    field :admin, as: :boolean, help: \"Whether the user has admin capabilities.\"\n\n    with_options hide_on: :forms do\n      field :number_of_accounts, as: :text do\n        record.accounts.length\n      end\n    end\n\n    tabs do\n      field :accounts, as: :has_many\n      field :connected_accounts, as: :has_many\n    end\n\n    tabs do\n      tab \"General\" do\n        panel do\n          field :time_zone, as: :text\n          field :invitations_count, as: :text\n        end\n      end\n      tab \"Account management\" do\n        panel do\n          field :password, as: :password\n          field :password_confirmation, as: :password\n          field :reset_password_token, as: :password\n          field :reset_password_sent_at, as: :date_time\n          field :remember_created_at, as: :date_time\n          field :confirmation_token, as: :text\n          field :confirmed_at, as: :date_time\n          field :confirmation_sent_at, as: :date_time\n          field :unconfirmed_email, as: :text\n          field :created_at, as: :date_time\n          field :updated_at, as: :date_time\n          field :invitation_token, as: :text\n          field :invitation_created_at, as: :date_time\n          field :invitation_sent_at, as: :date_time\n          field :invitation_accepted_at, as: :date_time\n          field :invitation_limit, as: :number\n          field :terms_of_service, as: :boolean\n          field :accepted_terms_at, as: :date_time\n          field :accepted_privacy_at, as: :date_time\n        end\n      end\n    end\n  end\nend\n", "app/controllers/avo/account_users_controller.rb"=>"# This controller has been generated to enable Rails' resource routes.\n# You shouldn't need to modify it in order to use Avo.\nclass Avo::AccountUsersController < Avo::ResourcesController\nend\n", "app/controllers/avo/accounts_controller.rb"=>"class Avo::AccountsController < Avo::ResourcesController\nend\n", "app/controllers/avo/announcements_controller.rb"=>"class Avo::AnnouncementsController < Avo::ResourcesController\nend\n", "app/controllers/avo/charges_controller.rb"=>"# This controller has been generated to enable Rails' resource routes.\n# You shouldn't need to modify it in order to use Avo.\nclass Avo::ChargesController < Avo::ResourcesController\n  include Jumpstart::AdministrateHelpers\nend\n", "app/controllers/avo/connected_accounts_controller.rb"=>"# This controller has been generated to enable Rails' resource routes.\n# You shouldn't need to modify it in order to use Avo.\nclass Avo::ConnectedAccountsController < Avo::ResourcesController\nend\n", "app/controllers/avo/customers_controller.rb"=>"class Avo::CustomersController < Avo::ResourcesController\n  include Jumpstart::AdministrateHelpers\nend\n", "app/controllers/avo/payment_methods_controller.rb"=>"# This controller has been generated to enable Rails' resource routes.\n# You shouldn't need to modify it in order to use Avo.\nclass Avo::PaymentMethodsController < Avo::ResourcesController\nend\n", "app/controllers/avo/plans_controller.rb"=>"class Avo::PlansController < Avo::ResourcesController\n  include Jumpstart::AdministrateHelpers\nend\n", "app/controllers/avo/subscriptions_controller.rb"=>"class Avo::SubscriptionsController < Avo::ResourcesController\n  include Jumpstart::AdministrateHelpers\nend\n", "app/controllers/avo/users_controller.rb"=>"class Avo::UsersController < Avo::ResourcesController\nend\n", "config/initializers/avo.rb"=>"# For more information regarding these settings check out our docs https://docs.avohq.io\nAvo.configure do |config|\n  ## == Routing ==\n  config.root_path = \"/avo\"\n  # used only when you have custom `map` configuration in your config.ru\n  # config.prefix_path = \"/internal\"\n\n  # Where should the user be redirected when visting the `/avo` url\n  config.home_path = defined?(Avo::Pro) ? \"/avo/dashboards/overview\" : nil\n\n  ## == Licensing ==\n  # Add your license key here for Pro or Advanced licenses\n  # config.license_key = ENV['AVO_LICENSE_KEY']\n\n  ## == Set the context ==\n  config.set_context do\n    # Return a context object that gets evaluated in Avo::ApplicationController\n    {}\n  end\n\n  ## == Authentication ==\n  # config.current_user_method = {}\n  # config.authenticate_with do\n  # end\n\n  ## == Authorization ==\n  # config.authorization_methods = {\n  #   index: 'index?',\n  #   show: 'show?',\n  #   edit: 'edit?',\n  #   new: 'new?',\n  #   update: 'update?',\n  #   create: 'create?',\n  #   destroy: 'destroy?',\n  #   search: 'search?',\n  # }\n  # config.raise_error_on_missing_policy = false\n  # config.authorization_client = :pundit\n\n  ## == Localization ==\n  # config.locale = 'en-US'\n\n  ## == Resource options ==\n  # config.resource_controls_placement = :right\n  # config.model_resource_mapping = {}\n  # config.default_view_type = :table\n  # config.per_page = 24\n  # config.per_page_steps = [12, 24, 48, 72]\n  # config.via_per_page = 8\n  config.id_links_to_resource = true\n  # config.cache_resources_on_index_view = true\n  ## permanent enable or disable cache_resource_filters, default value is false\n  # config.cache_resource_filters = false\n  ## provide a lambda to enable or disable cache_resource_filters per user/resource.\n  # config.cache_resource_filters = ->(current_user:, resource:) { current_user.cache_resource_filters?}\n\n  ## == Customization ==\n  # config.app_name = 'Avocadelicious'\n  # config.timezone = 'UTC'\n  # config.currency = 'USD'\n  # config.hide_layout_when_printing = false\n  # config.full_width_container = false\n  # config.full_width_index_view = false\n  # config.search_debounce = 300\n  # config.view_component_path = \"app/components\"\n  # config.display_license_request_timeout_error = true\n  # config.disabled_features = []\n  # config.tabs_style = :tabs # can be :tabs or :pills\n  # config.buttons_on_form_footers = true\n  # config.field_wrapper_layout = true\n\n  ## == Branding ==\n  # config.branding = {\n  #   colors: {\n  #     background: \"248 246 242\",\n  #     100 => \"#CEE7F8\",\n  #     400 => \"#399EE5\",\n  #     500 => \"#0886DE\",\n  #     600 => \"#066BB2\",\n  #   },\n  #   chart_colors: [\"#0B8AE2\", \"#34C683\", \"#2AB1EE\", \"#34C6A8\"],\n  #   logo: \"/avo-assets/logo.png\",\n  #   logomark: \"/avo-assets/logomark.png\",\n  #   placeholder: \"/avo-assets/placeholder.svg\",\n  #   favicon: \"/avo-assets/favicon.ico\"\n  # }\n\n  ## == Breadcrumbs ==\n  # config.display_breadcrumbs = true\n  # config.set_initial_breadcrumbs do\n  #   add_breadcrumb \"Home\", '/avo'\n  # end\n\n  ## == Menus ==\n  config.main_menu = -> {\n    section \"Dashboards\", icon: \"dashboards\" do\n      all_dashboards\n    end\n\n    section \"Users\", icon: \"heroicons/outline/user-group\" do\n      resource :announcement\n      resource :user\n      resource :account\n      resource :account_user\n      resource :plan\n    end\n\n    section \"Pay\", icon: \"heroicons/outline/currency-dollar\" do\n      resource :customer\n      resource :charge\n      resource :payment_method\n      resource :subscription\n    end\n\n    if Rails.env.development?\n      link \"Jumpstart Config\", path: Avo::Current.view_context.main_app.jumpstart_path\n    end\n  }\n  config.profile_menu = -> {\n    link \"Profile\", path: \"/avo/profile\", icon: \"user-circle\"\n  }\nend\n"}

if ARGV.include? "--community-edition"
  edition = "community"
elsif ARGV.include? "--pro-edition"
  edition = "pro"
elsif ARGV.include? "--advanced-edition"
  edition = "advanced"
end

unless edition
  # === Fetch the Avo edition ===
  question = <<~QUESTION


    Which version of Avo would you like to install?
    1. Avo Community (default)
    2. Avo Pro
    3. Avo Advanced

    More information about version features here:
    https://avohq.io/pricing


  QUESTION
  puts question

  answer = ask("Which version of Avo would you like to install?", default: "1", limited_to: ["1", "2", "3"])

  edition = case answer
  when "1"
    "community"
  when "2"
    "pro"
  when "3"
    "advanced"
  end
end

# === Add gem to Gemfile ===
case edition
when "community"
  gem "avo", ">= 3.1.0"
when "pro"
  gem "avo", ">= 3.1.0"
  gem "avo-pro", source: "https://packager.dev/avo-hq"
when "advanced"
  gem "avo", ">= 3.1.0"
  gem "avo-advanced", source: "https://packager.dev/avo-hq"
end

# === Run bundle install ===
Bundler.with_unbundled_env { run "bundle install" }

# === Add route ===
route_contents = <<-ROUTES
  # Avo admin panel
  if defined?(Avo::Engine)
    authenticated :user, lambda { |u| u.admin? } do
      mount Avo::Engine, at: Avo.configuration.root_path
    end
  end
ROUTES
route route_contents

# === Copy template files ===
files.each do |path, contents|
  file path, contents
end