diff --git a/.rspec_parallel b/.rspec_parallel index 867e417e06..cee31855e2 100644 --- a/.rspec_parallel +++ b/.rspec_parallel @@ -1,4 +1,4 @@ ---format progress +--format Fuubar --format ParallelTests::RSpec::SummaryLogger --out tmp/spec_summary.log --format ParallelTests::RSpec::RuntimeLogger --out tmp/parallel_runtime_rspec.log --tag ~performance diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000..31ccc06991 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,33 @@ +# Contributing + +We love pull requests from everyone. Here are some instructions for +contributing code to Open Food Network. + +Fork, then clone the repo: + + git clone git@github.com:your-username/openfoodnetwork.git + +Follow the instructions in README.markdown to set up your machine. + +Make sure the tests pass: + + rspec spec + +Make your change. Add tests for your change. Make the tests pass: + + rspec spec + +Push to your fork and [submit a pull request][pr]. + +[pr]: https://github.com/openfoodfoundation/openfoodnetwork/compare/ + +At this point you're waiting on us. We may suggest some changes or +improvements or alternatives. + +To increase the chance that your pull request is swiftly accepted: + +* Write tests +* Use a style consistent with the rest of the codebase +* Before submitting, [rebase your work][rebase] on the current master branch + +[rebase]: https://www.atlassian.com/git/tutorials/merging-vs-rebasing/workflow-walkthrough diff --git a/Gemfile b/Gemfile index d8e4c08bc9..6445843148 100644 --- a/Gemfile +++ b/Gemfile @@ -32,6 +32,7 @@ gem 'newrelic_rpm' gem 'haml' gem 'sass', "~> 3.3" gem 'sass-rails', '~> 3.2.3', groups: [:default, :assets] +gem 'redcarpet' gem 'aws-sdk' gem 'db2fog' gem 'andand' @@ -51,7 +52,9 @@ gem 'custom_error_message', :github => 'jeremydurham/custom-err-msg' gem 'angularjs-file-upload-rails', '~> 1.1.0' gem 'roadie-rails', '~> 1.0.3' gem 'figaro' +gem 'blockenspiel' gem 'acts-as-taggable-on', '~> 3.4' +gem 'paper_trail', '~> 3.0.8' gem 'foreigner' gem 'immigrant' @@ -72,13 +75,14 @@ group :assets do gem 'turbo-sprockets-rails3' gem 'foundation-icons-sass-rails' gem 'momentjs-rails' - gem 'angular-rails-templates' + gem 'angular-rails-templates', '~> 0.2.0' end + gem "foundation-rails" gem 'foundation_rails_helper', github: 'willrjmarshall/foundation_rails_helper', branch: "rails3" gem 'jquery-rails' - +gem 'css_splitter' group :test, :development do @@ -95,8 +99,10 @@ group :test, :development do gem 'letter_opener' gem 'timecop' gem 'poltergeist' + gem 'rspec-retry' gem 'json_spec' gem 'unicorn-rails' + gem 'atomic' end group :test do diff --git a/Gemfile.lock b/Gemfile.lock index 244346a15f..333eab0648 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -147,14 +147,15 @@ GEM acts_as_list (0.1.4) addressable (2.3.3) andand (1.3.3) - angular-rails-templates (0.1.1) + angular-rails-templates (0.2.0) railties (>= 3.1) - sprockets + sprockets (~> 2) tilt angularjs-file-upload-rails (1.1.0) angularjs-rails (1.2.13) ansi (1.4.2) arel (3.0.3) + atomic (1.1.99) awesome_nested_set (2.1.5) activerecord (>= 3.0.0) awesome_print (1.0.2) @@ -165,6 +166,7 @@ GEM bcrypt (3.1.7) bcrypt-ruby (3.1.5) bcrypt (>= 3.1.3) + blockenspiel (0.4.5) bugsnag (1.5.2) httparty (>= 0.6, < 1.0) multi_json (~> 1.0) @@ -219,6 +221,8 @@ GEM safe_yaml (~> 0.9.0) css_parser (1.3.5) addressable + css_splitter (0.4.1) + sprockets (>= 2.0.0) daemons (1.2.2) dalli (2.7.2) database_cleaner (0.7.1) @@ -320,7 +324,7 @@ GEM jquery-rails (2.2.2) railties (>= 3.0, < 5.0) thor (>= 0.14, < 2.0) - json (1.8.2) + json (1.8.3) json_spec (1.1.1) multi_json (~> 1.0) rspec (~> 2.0) @@ -353,11 +357,14 @@ GEM net-scp (1.1.2) net-ssh (>= 2.6.5) net-ssh (2.6.8) - newrelic_rpm (3.6.7.152) + newrelic_rpm (3.12.0.288) nokogiri (1.6.6.2) mini_portile (~> 0.6.0) oj (2.1.2) orm_adapter (0.5.0) + paper_trail (3.0.8) + activerecord (>= 3.0, < 5.0) + activesupport (>= 3.0, < 5.0) paperclip (3.5.4) activemodel (>= 3.0.0) activesupport (>= 3.0.0) @@ -428,6 +435,7 @@ GEM ffi (>= 0.5.0) rdoc (3.12.2) json (~> 1.4) + redcarpet (3.2.3) ref (1.0.5) representative (1.0.5) activesupport (>= 2.2.2) @@ -458,6 +466,8 @@ GEM rspec-core (~> 2.14.0) rspec-expectations (~> 2.14.0) rspec-mocks (~> 2.14.0) + rspec-retry (0.4.2) + rspec-core ruby-hmac (0.4.0) ruby-progressbar (1.7.1) safe_yaml (0.9.5) @@ -538,16 +548,19 @@ DEPENDENCIES active_model_serializers acts-as-taggable-on (~> 3.4) andand - angular-rails-templates + angular-rails-templates (~> 0.2.0) angularjs-file-upload-rails (~> 1.1.0) angularjs-rails (= 1.2.13) + atomic awesome_print aws-sdk + blockenspiel bugsnag capybara coffee-rails (~> 3.2.1) comfortable_mexican_sofa compass-rails + css_splitter custom_error_message! daemons dalli @@ -580,6 +593,7 @@ DEPENDENCIES newrelic_rpm nokogiri oj + paper_trail (~> 3.0.8) paperclip parallel_tests pg @@ -590,9 +604,11 @@ DEPENDENCIES rack-ssl rails (= 3.2.21) rails-i18n (~> 3.0.0) + redcarpet representative_view roadie-rails (~> 1.0.3) rspec-rails + rspec-retry sass (~> 3.3) sass-rails (~> 3.2.3) shoulda-matchers diff --git a/README.markdown b/README.markdown index b9aaf8193c..15c6920be1 100644 --- a/README.markdown +++ b/README.markdown @@ -12,10 +12,15 @@ We're part of global movement - get involved! * Find out more and join in the conversation - http://openfoodnetwork.org +## Getting started + +Below are instructions for setting up a development environment for Open Food Network. If you're interested in provisioning a server, see [the project's Ansible playbooks](https://github.com/openfoodfoundation/ofn_deployment). + + ## Dependencies * Rails 3.2.x -* Ruby >= 1.9.3 +* Ruby 1.9.3 * PostgreSQL database * PhantomJS (for testing) * See Gemfile for a list of gems required @@ -32,19 +37,20 @@ You can view the code at: You can download the source with the command: - git clone git@github.com:openfoodfoundation/openfoodnetwork + git clone https://github.com/openfoodfoundation/openfoodnetwork.git ## Get it running For those new to Rails, the following tutorial will help get you up to speed with configuring a Rails environment: http://guides.rubyonrails.org/getting_started.html . -First, check your dependencies: Ensure that you have Ruby 1.9.x installed: +First, check your dependencies: Ensure that you have Ruby >= 1.9.3 installed: ruby --version Install the project's gem dependencies: + cd openfoodnetwork bundle install Configure the site: diff --git a/app/assets/images/case-studies/South_East_Food_Hub.png b/app/assets/images/case-studies/South_East_Food_Hub.png new file mode 100644 index 0000000000..1760a68d72 Binary files /dev/null and b/app/assets/images/case-studies/South_East_Food_Hub.png differ diff --git a/app/assets/images/case-studies/baw-baw.png b/app/assets/images/case-studies/baw-baw.png new file mode 100644 index 0000000000..56dd2c94f1 Binary files /dev/null and b/app/assets/images/case-studies/baw-baw.png differ diff --git a/app/assets/images/case-studies/bfc_logo_square.png b/app/assets/images/case-studies/bfc_logo_square.png new file mode 100644 index 0000000000..90f54df507 Binary files /dev/null and b/app/assets/images/case-studies/bfc_logo_square.png differ diff --git a/app/assets/images/case-studies/bonnie-beef-growers.png b/app/assets/images/case-studies/bonnie-beef-growers.png new file mode 100644 index 0000000000..ad9f5b14ad Binary files /dev/null and b/app/assets/images/case-studies/bonnie-beef-growers.png differ diff --git a/app/assets/images/case-studies/jindivick.jpg b/app/assets/images/case-studies/jindivick.jpg new file mode 100644 index 0000000000..b94ab00166 Binary files /dev/null and b/app/assets/images/case-studies/jindivick.jpg differ diff --git a/app/assets/images/case-studies/jonai.png b/app/assets/images/case-studies/jonai.png new file mode 100644 index 0000000000..6cdf7dd20b Binary files /dev/null and b/app/assets/images/case-studies/jonai.png differ diff --git a/app/assets/images/case-studies/longley.png b/app/assets/images/case-studies/longley.png new file mode 100644 index 0000000000..0d20ae93df Binary files /dev/null and b/app/assets/images/case-studies/longley.png differ diff --git a/app/assets/images/case-studies/mt-alexander.png b/app/assets/images/case-studies/mt-alexander.png new file mode 100644 index 0000000000..efb86d2463 Binary files /dev/null and b/app/assets/images/case-studies/mt-alexander.png differ diff --git a/app/assets/images/case-studies/wandiful.png b/app/assets/images/case-studies/wandiful.png new file mode 100644 index 0000000000..f7c4469022 Binary files /dev/null and b/app/assets/images/case-studies/wandiful.png differ diff --git a/app/assets/images/enterprise-type.png b/app/assets/images/enterprise-type.png new file mode 100644 index 0000000000..0c5741a82f Binary files /dev/null and b/app/assets/images/enterprise-type.png differ diff --git a/app/assets/images/favicon.ico b/app/assets/images/favicon.ico deleted file mode 100644 index 4011e1ba8b..0000000000 Binary files a/app/assets/images/favicon.ico and /dev/null differ diff --git a/app/assets/images/favicon.png b/app/assets/images/favicon.png deleted file mode 100644 index 758c60d7e2..0000000000 Binary files a/app/assets/images/favicon.png and /dev/null differ diff --git a/app/assets/images/groups.svg b/app/assets/images/groups.svg index f4ca32ec27..a2e353d47f 100644 --- a/app/assets/images/groups.svg +++ b/app/assets/images/groups.svg @@ -1,1565 +1 @@ - - - - + \ No newline at end of file diff --git a/app/assets/images/home/background-blurred-oranges.jpg b/app/assets/images/home/background-blurred-oranges.jpg new file mode 100644 index 0000000000..61019198e3 Binary files /dev/null and b/app/assets/images/home/background-blurred-oranges.jpg differ diff --git a/app/assets/images/home/home-apples.jpg b/app/assets/images/home/home-apples.jpg new file mode 100644 index 0000000000..5cf22d4a50 Binary files /dev/null and b/app/assets/images/home/home-apples.jpg differ diff --git a/app/assets/images/home/home-oranges.jpg b/app/assets/images/home/home-oranges.jpg new file mode 100644 index 0000000000..6cd547c97f Binary files /dev/null and b/app/assets/images/home/home-oranges.jpg differ diff --git a/app/assets/images/home/home-strawberries.jpg b/app/assets/images/home/home-strawberries.jpg new file mode 100644 index 0000000000..e837c69065 Binary files /dev/null and b/app/assets/images/home/home-strawberries.jpg differ diff --git a/app/assets/images/home/home.jpg b/app/assets/images/home/home.jpg new file mode 100644 index 0000000000..cb59fbef31 Binary files /dev/null and b/app/assets/images/home/home.jpg differ diff --git a/app/assets/images/home/home1.jpg b/app/assets/images/home/home1.jpg new file mode 100644 index 0000000000..0e37397127 Binary files /dev/null and b/app/assets/images/home/home1.jpg differ diff --git a/app/assets/images/home/home2.jpg b/app/assets/images/home/home2.jpg new file mode 100644 index 0000000000..fa548cb519 Binary files /dev/null and b/app/assets/images/home/home2.jpg differ diff --git a/app/assets/images/home/home3.jpg b/app/assets/images/home/home3.jpg new file mode 100644 index 0000000000..26fdf1037c Binary files /dev/null and b/app/assets/images/home/home3.jpg differ diff --git a/app/assets/images/home/ofn_bg_1.jpg b/app/assets/images/home/ofn_bg_1.jpg deleted file mode 100644 index 416ebf35fb..0000000000 Binary files a/app/assets/images/home/ofn_bg_1.jpg and /dev/null differ diff --git a/app/assets/images/home/shopping-bg.jpg b/app/assets/images/home/shopping-bg.jpg deleted file mode 100644 index 0a02b0f6f4..0000000000 Binary files a/app/assets/images/home/shopping-bg.jpg and /dev/null differ diff --git a/app/assets/images/home/tagline-bg.jpg b/app/assets/images/home/tagline-bg.jpg deleted file mode 100644 index 68366a9569..0000000000 Binary files a/app/assets/images/home/tagline-bg.jpg and /dev/null differ diff --git a/app/assets/images/hubs-bg.jpg b/app/assets/images/hubs-bg.jpg new file mode 100644 index 0000000000..2d1c49e230 Binary files /dev/null and b/app/assets/images/hubs-bg.jpg differ diff --git a/app/assets/images/icon-mask-apple.png b/app/assets/images/icon-mask-apple.png new file mode 100644 index 0000000000..87bfa91bb8 Binary files /dev/null and b/app/assets/images/icon-mask-apple.png differ diff --git a/app/assets/images/icon-mask-bread.png b/app/assets/images/icon-mask-bread.png new file mode 100644 index 0000000000..d5c29d96ff Binary files /dev/null and b/app/assets/images/icon-mask-bread.png differ diff --git a/app/assets/images/icon-mask-magnifier.png b/app/assets/images/icon-mask-magnifier.png new file mode 100644 index 0000000000..7f53b7db9d Binary files /dev/null and b/app/assets/images/icon-mask-magnifier.png differ diff --git a/app/assets/images/icon-mask-truck.png b/app/assets/images/icon-mask-truck.png new file mode 100644 index 0000000000..40550f98c2 Binary files /dev/null and b/app/assets/images/icon-mask-truck.png differ diff --git a/app/assets/images/logo-australia.png b/app/assets/images/logo-australia.png new file mode 100644 index 0000000000..46b8ef071a Binary files /dev/null and b/app/assets/images/logo-australia.png differ diff --git a/app/assets/images/logo-black.png b/app/assets/images/logo-black.png new file mode 100644 index 0000000000..31ec1422f1 Binary files /dev/null and b/app/assets/images/logo-black.png differ diff --git a/app/assets/images/logo-black.svg b/app/assets/images/logo-black.svg new file mode 100644 index 0000000000..5f69f364c4 --- /dev/null +++ b/app/assets/images/logo-black.svg @@ -0,0 +1,82 @@ + + diff --git a/app/assets/images/logo-color.png b/app/assets/images/logo-color.png new file mode 100644 index 0000000000..e464781a54 Binary files /dev/null and b/app/assets/images/logo-color.png differ diff --git a/app/assets/images/logo-color.svg b/app/assets/images/logo-color.svg new file mode 100644 index 0000000000..84de1b4ceb --- /dev/null +++ b/app/assets/images/logo-color.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/assets/images/logo-global-white.png b/app/assets/images/logo-global-white.png new file mode 100644 index 0000000000..8761bafe9d Binary files /dev/null and b/app/assets/images/logo-global-white.png differ diff --git a/app/assets/images/logo-white-notext.png b/app/assets/images/logo-white-notext.png new file mode 100644 index 0000000000..bfd590c621 Binary files /dev/null and b/app/assets/images/logo-white-notext.png differ diff --git a/app/assets/images/logo-white.png b/app/assets/images/logo-white.png new file mode 100644 index 0000000000..ffd1c6f73a Binary files /dev/null and b/app/assets/images/logo-white.png differ diff --git a/app/assets/images/logo-white.svg b/app/assets/images/logo-white.svg new file mode 100644 index 0000000000..c2c22bab40 --- /dev/null +++ b/app/assets/images/logo-white.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/assets/images/logo.jpg b/app/assets/images/logo.jpg deleted file mode 100644 index 49e232eb55..0000000000 Binary files a/app/assets/images/logo.jpg and /dev/null differ diff --git a/app/assets/images/logo.png b/app/assets/images/logo.png deleted file mode 100644 index aca7aac1dc..0000000000 Binary files a/app/assets/images/logo.png and /dev/null differ diff --git a/app/assets/images/noimage/large.png b/app/assets/images/noimage/large.png index 29dcff5ea9..166a74488a 100644 Binary files a/app/assets/images/noimage/large.png and b/app/assets/images/noimage/large.png differ diff --git a/app/assets/images/noimage/mini.png b/app/assets/images/noimage/mini.png index 5094c92a18..db94c3ce7f 100644 Binary files a/app/assets/images/noimage/mini.png and b/app/assets/images/noimage/mini.png differ diff --git a/app/assets/images/noimage/product.png b/app/assets/images/noimage/product.png index ca06da639b..6ca94eadb2 100644 Binary files a/app/assets/images/noimage/product.png and b/app/assets/images/noimage/product.png differ diff --git a/app/assets/images/noimage/small.png b/app/assets/images/noimage/small.png index ca06da639b..02088d8da0 100644 Binary files a/app/assets/images/noimage/small.png and b/app/assets/images/noimage/small.png differ diff --git a/app/assets/images/ofn-o.png b/app/assets/images/ofn-o.png new file mode 100644 index 0000000000..d6498ddb4d Binary files /dev/null and b/app/assets/images/ofn-o.png differ diff --git a/app/assets/images/ofn-o.svg b/app/assets/images/ofn-o.svg new file mode 100644 index 0000000000..2082ae6176 --- /dev/null +++ b/app/assets/images/ofn-o.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/assets/images/ofn_logo.png b/app/assets/images/ofn_logo.png deleted file mode 100644 index cdf690d997..0000000000 Binary files a/app/assets/images/ofn_logo.png and /dev/null differ diff --git a/app/assets/images/ofn_logo_beta.png b/app/assets/images/ofn_logo_beta.png deleted file mode 100644 index 420a51768d..0000000000 Binary files a/app/assets/images/ofn_logo_beta.png and /dev/null differ diff --git a/app/assets/images/ofn_logo_black.png b/app/assets/images/ofn_logo_black.png deleted file mode 100644 index d5f250daba..0000000000 Binary files a/app/assets/images/ofn_logo_black.png and /dev/null differ diff --git a/app/assets/images/ofn_logo_small.png b/app/assets/images/ofn_logo_small.png deleted file mode 100644 index 90277ab384..0000000000 Binary files a/app/assets/images/ofn_logo_small.png and /dev/null differ diff --git a/app/assets/images/ofw.png b/app/assets/images/ofw.png deleted file mode 100644 index ac571bacd0..0000000000 Binary files a/app/assets/images/ofw.png and /dev/null differ diff --git a/app/assets/images/open-food-network-beta-black.png b/app/assets/images/open-food-network-beta-black.png deleted file mode 100644 index 8e5f810023..0000000000 Binary files a/app/assets/images/open-food-network-beta-black.png and /dev/null differ diff --git a/app/assets/images/open-food-network-beta.png b/app/assets/images/open-food-network-beta.png deleted file mode 100644 index 965a248162..0000000000 Binary files a/app/assets/images/open-food-network-beta.png and /dev/null differ diff --git a/app/assets/images/open-food-network-beta.svg b/app/assets/images/open-food-network-beta.svg deleted file mode 100644 index eb882701d4..0000000000 --- a/app/assets/images/open-food-network-beta.svg +++ /dev/null @@ -1,840 +0,0 @@ - - - - diff --git a/app/assets/images/pickup.png b/app/assets/images/pickup.png deleted file mode 100644 index aabceb508e..0000000000 Binary files a/app/assets/images/pickup.png and /dev/null differ diff --git a/app/assets/images/pin_bg.png b/app/assets/images/pin_bg.png deleted file mode 100644 index cfdd66162d..0000000000 Binary files a/app/assets/images/pin_bg.png and /dev/null differ diff --git a/app/assets/images/producers.svg b/app/assets/images/producers.svg new file mode 100644 index 0000000000..9804557f4b --- /dev/null +++ b/app/assets/images/producers.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/assets/images/store/cart.png b/app/assets/images/store/cart.png deleted file mode 100755 index 8650c39936..0000000000 Binary files a/app/assets/images/store/cart.png and /dev/null differ diff --git a/app/assets/images/subtle_white_feathers.png b/app/assets/images/subtle_white_feathers.png deleted file mode 100644 index dd699f66ba..0000000000 Binary files a/app/assets/images/subtle_white_feathers.png and /dev/null differ diff --git a/app/assets/images/tile-wide.png b/app/assets/images/tile-wide.png new file mode 100644 index 0000000000..f4e348b73c Binary files /dev/null and b/app/assets/images/tile-wide.png differ diff --git a/app/assets/javascripts/admin/accounts_and_billing_settings/accounts_and_billing_settings.js.coffee b/app/assets/javascripts/admin/accounts_and_billing_settings/accounts_and_billing_settings.js.coffee new file mode 100644 index 0000000000..d4f544e300 --- /dev/null +++ b/app/assets/javascripts/admin/accounts_and_billing_settings/accounts_and_billing_settings.js.coffee @@ -0,0 +1 @@ +angular.module("admin.accounts_and_billing_settings", []) diff --git a/app/assets/javascripts/admin/accounts_and_billing_settings/directives/method_settings.js.coffee b/app/assets/javascripts/admin/accounts_and_billing_settings/directives/method_settings.js.coffee new file mode 100644 index 0000000000..32ef50bb64 --- /dev/null +++ b/app/assets/javascripts/admin/accounts_and_billing_settings/directives/method_settings.js.coffee @@ -0,0 +1,14 @@ +angular.module("admin.accounts_and_billing_settings").directive "methodSettingsFor", -> + template: "
" + restrict: 'A' + scope: { + enterprise_id: '=methodSettingsFor' + } + link: (scope, element, attrs) -> + scope.include_html = "" + + scope.$watch "enterprise_id", (newVal, oldVal)-> + if !newVal? || newVal == "" + scope.include_html = "" + else + scope.include_html = "/admin/accounts_and_billing_settings/show_methods?enterprise_id=#{newVal};" diff --git a/app/assets/javascripts/admin/accounts_and_billing_settings/directives/watchValueAs.js.coffee b/app/assets/javascripts/admin/accounts_and_billing_settings/directives/watchValueAs.js.coffee new file mode 100644 index 0000000000..a14288db55 --- /dev/null +++ b/app/assets/javascripts/admin/accounts_and_billing_settings/directives/watchValueAs.js.coffee @@ -0,0 +1,11 @@ +angular.module("admin.accounts_and_billing_settings").directive "watchValueAs", -> + restrict: 'A' + scope: { + value: "=watchValueAs" + } + link: (scope, element, attrs) -> + scope.value = element.val() + + element.on "change blur load", -> + scope.$apply -> + scope.value = element.val() diff --git a/app/assets/javascripts/admin/all.js b/app/assets/javascripts/admin/all.js index e44e6ab96d..cbaccac66d 100644 --- a/app/assets/javascripts/admin/all.js +++ b/app/assets/javascripts/admin/all.js @@ -12,13 +12,17 @@ //= require angular //= require angular-resource //= require angular-animate +//= require angular-sanitize //= require admin/spree_core //= require admin/spree_auth //= require admin/spree_promo //= require admin/spree_paypal_express //= require ../shared/ng-infinite-scroll.min.js //= require ../shared/ng-tags-input.min.js +//= require angular-rails-templates +//= require_tree ../templates/admin //= require ./admin +//= require ./accounts_and_billing_settings/accounts_and_billing_settings //= require ./customers/customers //= require ./dropdown/dropdown //= require ./enterprises/enterprises @@ -33,5 +37,6 @@ //= require ./users/users //= require textAngular.min.js //= require textAngular-sanitize.min.js +//= require ../shared/bindonce.min.js //= require_tree . diff --git a/app/assets/javascripts/admin/bulk_order_management.js.coffee b/app/assets/javascripts/admin/bulk_order_management.js.coffee index 5c2b5b8984..8cd3d5b204 100644 --- a/app/assets/javascripts/admin/bulk_order_management.js.coffee +++ b/app/assets/javascripts/admin/bulk_order_management.js.coffee @@ -30,7 +30,7 @@ angular.module("ofn.admin").controller "AdminOrderMgmtCtrl", [ variant: { name: "Variant", visible: true } quantity: { name: "Quantity", visible: true } max: { name: "Max", visible: true } - unit_value: { name: "Weight/Volume", visible: false } + final_weight_volume: { name: "Weight/Volume", visible: false } price: { name: "Price", visible: false } $scope.initialise = -> $scope.initialiseVariables() @@ -166,10 +166,10 @@ angular.module("ofn.admin").controller "AdminOrderMgmtCtrl", [ $scope.weightAdjustedPrice = (lineItem, oldValue) -> if oldValue <= 0 - oldValue = lineItem.units_variant.unit_value - if lineItem.unit_value <= 0 - lineItem.unit_value = lineItem.units_variant.unit_value - lineItem.price = lineItem.price * lineItem.unit_value / oldValue + oldValue = lineItem.units_variant.unit_value * line_item.quantity + if lineItem.final_weight_volume <= 0 + lineItem.final_weight_volume = lineItem.units_variant.unit_value * lineItem.quantity + lineItem.price = lineItem.price * lineItem.final_weight_volume / oldValue #$scope.bulk_order_form.line_item.price.$setViewValue($scope.bulk_order_form.line_item.price.$viewValue) $scope.unitValueLessThanZero = (lineItem) -> @@ -178,6 +178,13 @@ angular.module("ofn.admin").controller "AdminOrderMgmtCtrl", [ else false + $scope.updateOnQuantity = (lineItem, oldQuantity) -> + if lineItem.quantity <= 0 + lineItem.quantity = 1 + # reset price to original unit value + lineItem.price = lineItem.price * (oldQuantity * lineItem.units_variant.unit_value) / lineItem.final_weight_volume + lineItem.final_weight_volume = lineItem.units_variant.unit_value * lineItem.quantity + $scope.$watch "orderCycleFilter", (newVal, oldVal) -> unless $scope.orderCycleFilter == "0" || angular.equals(newVal, oldVal) $scope.startDate = $scope.orderCyclesByID[$scope.orderCycleFilter].first_order diff --git a/app/assets/javascripts/admin/controllers/enterprise_relationships_controller.js.coffee b/app/assets/javascripts/admin/controllers/enterprise_relationships_controller.js.coffee index 88524fb330..47083d443a 100644 --- a/app/assets/javascripts/admin/controllers/enterprise_relationships_controller.js.coffee +++ b/app/assets/javascripts/admin/controllers/enterprise_relationships_controller.js.coffee @@ -9,3 +9,26 @@ angular.module("ofn.admin").controller "AdminEnterpriseRelationshipsCtrl", ($sco $scope.delete = (enterprise_relationship) -> if confirm("Are you sure?") $scope.EnterpriseRelationships.delete enterprise_relationship + + $scope.toggleKeyword = (string, key) -> + string = '' unless string + words = string.split ' ' + words = words.filter (s) -> + s + index = words.indexOf key + if index > -1 + words.splice index, 1 + else + words.push key + words.join ' ' + + $scope.allPermissionsChecked = -> + for i in EnterpriseRelationships.all_permissions + if !$scope.permissions[i] + return false + return true + + $scope.checkAllPermissions = -> + newValue = !$scope.allPermissionsChecked() + EnterpriseRelationships.all_permissions.forEach (p) -> + $scope.permissions[p] = newValue diff --git a/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee b/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee index 03cd7d4943..d48f2a42f8 100644 --- a/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee +++ b/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee @@ -1,6 +1,6 @@ angular.module("admin.enterprises") - .controller "enterpriseCtrl", ($scope, NavigationCheck, Enterprise, EnterprisePaymentMethods, EnterpriseShippingMethods, SideMenu) -> - $scope.Enterprise = Enterprise.enterprise + .controller "enterpriseCtrl", ($scope, NavigationCheck, enterprise, EnterprisePaymentMethods, EnterpriseShippingMethods, SideMenu) -> + $scope.Enterprise = enterprise $scope.PaymentMethods = EnterprisePaymentMethods.paymentMethods $scope.ShippingMethods = EnterpriseShippingMethods.shippingMethods $scope.navClear = NavigationCheck.clear diff --git a/app/assets/javascripts/admin/enterprises/controllers/enterprise_index_row_controller.js.coffee b/app/assets/javascripts/admin/enterprises/controllers/enterprise_index_row_controller.js.coffee new file mode 100644 index 0000000000..63b8daf07c --- /dev/null +++ b/app/assets/javascripts/admin/enterprises/controllers/enterprise_index_row_controller.js.coffee @@ -0,0 +1,49 @@ +angular.module("admin.enterprises").controller "EnterpriseIndexRowCtrl", ($scope) -> + $scope.status = -> + if $scope.enterprise.issues.length > 0 + "issue" + else if $scope.enterprise.warnings.length > 0 + "warning" + else + "ok" + + + $scope.producerText = -> + switch $scope.enterprise.is_primary_producer + when true + "Producer" + else + "Non-Producer" + + $scope.packageText = -> + switch $scope.enterprise.is_primary_producer + when true + switch $scope.enterprise.sells + when "none" + "Profile" + when "own" + "Shop" + when "any" + "Hub" + else + "Choose" + else + switch $scope.enterprise.sells + when "none" + "Profile" + when "any" + "Hub" + else + "Choose" + + $scope.updateRowText = -> + $scope.producer = $scope.producerText() + $scope.package = $scope.packageText() + $scope.producerError = ($scope.producer == "Choose") + $scope.packageError = ($scope.package == "Choose") + + + $scope.updateRowText() + + $scope.$on "enterprise:updated", -> + $scope.updateRowText() diff --git a/app/assets/javascripts/admin/enterprises/controllers/enterprises_controller.js.coffee b/app/assets/javascripts/admin/enterprises/controllers/enterprises_controller.js.coffee new file mode 100644 index 0000000000..dca4e65800 --- /dev/null +++ b/app/assets/javascripts/admin/enterprises/controllers/enterprises_controller.js.coffee @@ -0,0 +1,13 @@ +angular.module("admin.enterprises").controller 'enterprisesCtrl', ($scope, Enterprises, Columns) -> + Enterprises.loaded = false + $scope.allEnterprises = Enterprises.index() + + $scope.loaded = -> + Enterprises.loaded + + $scope.columns = Columns.setColumns + name: { name: "Name", visible: true } + producer: { name: "Producer", visible: true } + package: { name: "Package", visible: true } + status: { name: "Status", visible: true } + manage: { name: "Manage", visible: true } diff --git a/app/assets/javascripts/admin/enterprises/controllers/index_package_panel_controller.js.coffee b/app/assets/javascripts/admin/enterprises/controllers/index_package_panel_controller.js.coffee new file mode 100644 index 0000000000..f191dc9e7a --- /dev/null +++ b/app/assets/javascripts/admin/enterprises/controllers/index_package_panel_controller.js.coffee @@ -0,0 +1,2 @@ +angular.module("admin.enterprises").controller 'indexPackagePanelCtrl', ($scope, $controller) -> + angular.extend this, $controller('indexPanelCtrl', {$scope: $scope}) diff --git a/app/assets/javascripts/admin/enterprises/controllers/index_panel_controller.js.coffee b/app/assets/javascripts/admin/enterprises/controllers/index_panel_controller.js.coffee new file mode 100644 index 0000000000..6f568ca5ea --- /dev/null +++ b/app/assets/javascripts/admin/enterprises/controllers/index_panel_controller.js.coffee @@ -0,0 +1,23 @@ +angular.module("admin.enterprises").controller 'indexPanelCtrl', ($scope, Enterprises) -> + $scope.enterprise = $scope.object + $scope.saving = false + + $scope.saved = -> + Enterprises.saved($scope.enterprise) + + $scope.save = -> + unless $scope.saved() + $scope.saving = true + Enterprises.save($scope.enterprise).then (data) -> + $scope.$emit("enterprise:updated") + $scope.saving = false + , (response) -> + $scope.saving = false + if response.status == 422 && response.data.errors? + message = 'Please resolve the following errors:\n' + for attr, msg of response.data.errors + message += "#{attr} #{msg}\n" + alert(message) + + $scope.resetAttribute = (attribute) -> + Enterprises.resetAttribute($scope.enterprise, attribute) diff --git a/app/assets/javascripts/admin/enterprises/controllers/index_producer_panel_controller.js.coffee b/app/assets/javascripts/admin/enterprises/controllers/index_producer_panel_controller.js.coffee new file mode 100644 index 0000000000..75fd9f1ec5 --- /dev/null +++ b/app/assets/javascripts/admin/enterprises/controllers/index_producer_panel_controller.js.coffee @@ -0,0 +1,14 @@ +angular.module("admin.enterprises").controller 'indexProducerPanelCtrl', ($scope, $controller) -> + angular.extend this, $controller('indexPanelCtrl', {$scope: $scope}) + + $scope.changeToProducer = -> + $scope.resetAttribute('sells') + $scope.resetAttribute('producer_profile_only') + $scope.enterprise.is_primary_producer = true + + $scope.changeToNonProducer = -> + if $scope.enterprise.sells == 'own' + $scope.enterprise.sells = 'any' + if $scope.enterprise.producer_profile_only = true + $scope.enterprise.producer_profile_only = false + $scope.enterprise.is_primary_producer = false diff --git a/app/assets/javascripts/admin/enterprises/controllers/index_status_panel_controller.js.coffee b/app/assets/javascripts/admin/enterprises/controllers/index_status_panel_controller.js.coffee new file mode 100644 index 0000000000..987a206bd0 --- /dev/null +++ b/app/assets/javascripts/admin/enterprises/controllers/index_status_panel_controller.js.coffee @@ -0,0 +1,3 @@ +angular.module("admin.enterprises").controller 'indexStatusPanelCtrl', ($scope, $filter) -> + $scope.issues = $scope.object.issues + $scope.warnings = $scope.object.warnings diff --git a/app/assets/javascripts/admin/enterprises/controllers/side_menu_controller.js.coffee b/app/assets/javascripts/admin/enterprises/controllers/side_menu_controller.js.coffee index d5d0e1681a..45a5d068a4 100644 --- a/app/assets/javascripts/admin/enterprises/controllers/side_menu_controller.js.coffee +++ b/app/assets/javascripts/admin/enterprises/controllers/side_menu_controller.js.coffee @@ -1,6 +1,6 @@ angular.module("admin.enterprises") - .controller "sideMenuCtrl", ($scope, $parse, Enterprise, SideMenu, enterprisePermissions) -> - $scope.Enterprise = Enterprise.enterprise + .controller "sideMenuCtrl", ($scope, $parse, enterprise, SideMenu, enterprisePermissions) -> + $scope.Enterprise = enterprise $scope.menu = SideMenu $scope.select = SideMenu.select diff --git a/app/assets/javascripts/admin/enterprises/enterprises.js.coffee b/app/assets/javascripts/admin/enterprises/enterprises.js.coffee index 5a7942d639..6be7e00ffa 100644 --- a/app/assets/javascripts/admin/enterprises/enterprises.js.coffee +++ b/app/assets/javascripts/admin/enterprises/enterprises.js.coffee @@ -1 +1 @@ -angular.module("admin.enterprises", [ "admin.payment_methods", "admin.utils", "admin.shipping_methods", "admin.users", "textAngular", "admin.side_menu", "admin.taxons"] ) \ No newline at end of file +angular.module("admin.enterprises", [ "admin.payment_methods", "admin.utils", "admin.shipping_methods", "admin.users", "textAngular", "admin.side_menu", "admin.taxons", 'admin.indexUtils', 'admin.dropdown', 'pasvaz.bindonce', 'ngSanitize'] ) \ No newline at end of file diff --git a/app/assets/javascripts/admin/enterprises/services/enterprise_payment_methods.js.coffee b/app/assets/javascripts/admin/enterprises/services/enterprise_payment_methods.js.coffee index b1a88f82fb..d1808e5001 100644 --- a/app/assets/javascripts/admin/enterprises/services/enterprise_payment_methods.js.coffee +++ b/app/assets/javascripts/admin/enterprises/services/enterprise_payment_methods.js.coffee @@ -1,11 +1,11 @@ angular.module("admin.enterprises") - .factory "EnterprisePaymentMethods", (Enterprise, PaymentMethods) -> + .factory "EnterprisePaymentMethods", (enterprise, PaymentMethods) -> new class EnterprisePaymentMethods paymentMethods: PaymentMethods.paymentMethods constructor: -> for payment_method in @paymentMethods - payment_method.selected = payment_method.id in Enterprise.enterprise.payment_method_ids + payment_method.selected = payment_method.id in enterprise.payment_method_ids displayColor: -> if @paymentMethods.length > 0 && @selectedCount() > 0 diff --git a/app/assets/javascripts/admin/enterprises/services/enterprise_resource.js.coffee b/app/assets/javascripts/admin/enterprises/services/enterprise_resource.js.coffee new file mode 100644 index 0000000000..357302d2b8 --- /dev/null +++ b/app/assets/javascripts/admin/enterprises/services/enterprise_resource.js.coffee @@ -0,0 +1,8 @@ +angular.module("admin.enterprises").factory 'EnterpriseResource', ($resource) -> + $resource('/admin/enterprises/:id.json', {}, { + 'index': + method: 'GET' + isArray: true + 'update': + method: 'PUT' + }) diff --git a/app/assets/javascripts/admin/enterprises/services/enterprise_shipping_methods.js.coffee b/app/assets/javascripts/admin/enterprises/services/enterprise_shipping_methods.js.coffee index 3f64c07442..50d15ff002 100644 --- a/app/assets/javascripts/admin/enterprises/services/enterprise_shipping_methods.js.coffee +++ b/app/assets/javascripts/admin/enterprises/services/enterprise_shipping_methods.js.coffee @@ -1,11 +1,11 @@ angular.module("admin.enterprises") - .factory "EnterpriseShippingMethods", (Enterprise, ShippingMethods) -> + .factory "EnterpriseShippingMethods", (enterprise, ShippingMethods) -> new class EnterpriseShippingMethods shippingMethods: ShippingMethods.shippingMethods constructor: -> for shipping_method in @shippingMethods - shipping_method.selected = shipping_method.id in Enterprise.enterprise.shipping_method_ids + shipping_method.selected = shipping_method.id in enterprise.shipping_method_ids displayColor: -> if @shippingMethods.length > 0 && @selectedCount() > 0 diff --git a/app/assets/javascripts/admin/enterprises/services/enterprises.js.coffee b/app/assets/javascripts/admin/enterprises/services/enterprises.js.coffee new file mode 100644 index 0000000000..3411c4a6c4 --- /dev/null +++ b/app/assets/javascripts/admin/enterprises/services/enterprises.js.coffee @@ -0,0 +1,39 @@ +angular.module("admin.enterprises").factory 'Enterprises', ($q, EnterpriseResource) -> + new class Enterprises + enterprises: [] + enterprises_by_id: {} + pristine_by_id: {} + loaded: false + + index: (params={}, callback=null) -> + EnterpriseResource.index params, (data) => + for enterprise in data + @enterprises.push enterprise + @pristine_by_id[enterprise.id] = angular.copy(enterprise) + + @loaded = true + (callback || angular.noop)(@enterprises) + + @enterprises + + save: (enterprise) -> + deferred = $q.defer() + enterprise.$update({id: enterprise.permalink}) + .then( (data) => + @pristine_by_id[enterprise.id] = angular.copy(enterprise) + deferred.resolve(data) + ).catch (response) -> + deferred.reject(response) + deferred.promise + + saved: (enterprise) -> + @diff(enterprise).length == 0 + + diff: (enterprise) -> + changed = [] + for attr, value of enterprise when not angular.equals(value, @pristine_by_id[enterprise.id][attr]) + changed.push attr unless attr is "$$hashKey" + changed + + resetAttribute: (enterprise, attribute) -> + enterprise[attribute] = @pristine_by_id[enterprise.id][attribute] diff --git a/app/assets/javascripts/admin/enterprises/services/permalink_checker.js.coffee b/app/assets/javascripts/admin/enterprises/services/permalink_checker.js.coffee index cb3fde9324..e1b62e6f6e 100644 --- a/app/assets/javascripts/admin/enterprises/services/permalink_checker.js.coffee +++ b/app/assets/javascripts/admin/enterprises/services/permalink_checker.js.coffee @@ -2,6 +2,7 @@ angular.module("admin.enterprises").factory 'PermalinkChecker', ($q, $http) -> new class PermalinkChecker deferredRequest: null deferredAbort: null + MAX_PERMALINK_LENGTH: 255 check: (permalink) => @abort(@deferredAbort) if @deferredRequest && @deferredRequest.promise @@ -15,9 +16,14 @@ angular.module("admin.enterprises").factory 'PermalinkChecker', ($q, $http) -> timeout: deferredAbort.promise ) .success( (data) => - deferredRequest.resolve - permalink: data - available: "Available" + if data.length > @MAX_PERMALINK_LENGTH || !data.match(/^[\w-]+$/) + deferredRequest.resolve + permalink: permalink + available: "Error" + else + deferredRequest.resolve + permalink: data + available: "Available" ).error (data,status) => if status == 409 deferredRequest.resolve diff --git a/app/assets/javascripts/admin/filters/keywords_filter.js.coffee b/app/assets/javascripts/admin/filters/keywords_filter.js.coffee new file mode 100644 index 0000000000..7d207518f0 --- /dev/null +++ b/app/assets/javascripts/admin/filters/keywords_filter.js.coffee @@ -0,0 +1,7 @@ +angular.module("ofn.admin").filter "keywords", ($filter) -> + return (array, query) -> + return array unless query + keywords = query.split ' ' + keywords.forEach (key) -> + array = $filter('filter')(array, key) + array diff --git a/app/assets/javascripts/admin/index_utils/directives/panel_row.js.coffee b/app/assets/javascripts/admin/index_utils/directives/panel_row.js.coffee new file mode 100644 index 0000000000..eb5a4171f4 --- /dev/null +++ b/app/assets/javascripts/admin/index_utils/directives/panel_row.js.coffee @@ -0,0 +1,37 @@ +angular.module("admin.indexUtils").directive "panelRow", (Panels, Columns) -> + restrict: "C" + templateUrl: "admin/panel.html" + scope: + object: "=" + panels: "=" + link: (scope, element, attrs) -> + scope.template = "" + selected = null + scope.columnCount = Columns.visibleCount + + scope.$on "columnCount:changed", (event, count) -> + scope.columnCount = count + + setTemplate = -> + if selected? + scope.template = 'admin/panels/' + scope.panels[selected] + '.html' + else + scope.template = "" + + scope.getSelected = -> + selected + + scope.setSelected = (name) -> + scope.$apply -> + selected = name + setTemplate() + + scope.open = (name) -> + element.show 0, -> + scope.setSelected name + + scope.close = -> + element.hide 0, -> + scope.setSelected null + + Panels.register(scope.object.id, scope) diff --git a/app/assets/javascripts/admin/index_utils/directives/panel_toggle.js.coffee b/app/assets/javascripts/admin/index_utils/directives/panel_toggle.js.coffee new file mode 100644 index 0000000000..df81328905 --- /dev/null +++ b/app/assets/javascripts/admin/index_utils/directives/panel_toggle.js.coffee @@ -0,0 +1,12 @@ +angular.module("admin.indexUtils").directive "panelToggle", -> + restrict: "C" + transclude: true + template: '' + require: "^panelToggleRow" + scope: + name: "@" + link: (scope, element, attrs, ctrl) -> + scope.selected = ctrl.register(scope.name, element) + + element.on "click", -> + scope.selected = ctrl.select(scope.name) diff --git a/app/assets/javascripts/admin/index_utils/directives/panel_toggle_row.js.coffee b/app/assets/javascripts/admin/index_utils/directives/panel_toggle_row.js.coffee new file mode 100644 index 0000000000..d2d9c90ff8 --- /dev/null +++ b/app/assets/javascripts/admin/index_utils/directives/panel_toggle_row.js.coffee @@ -0,0 +1,29 @@ +angular.module("admin.indexUtils").directive "panelToggleRow", (Panels) -> + restrict: "C" + scope: + object: "=" + selected: "@?" + controller: ($scope) -> + panelToggles = {} + + this.register = (name, element) -> + panelToggles[name] = element + panelToggles[name].addClass("selected") if $scope.selected == name + $scope.selected == name + + this.select = (name) -> + panelToggle.removeClass("selected") for panelName, panelToggle of panelToggles + + switch $scope.selected = Panels.toggle($scope.object.id, name) + when null + panelToggles[name].parent(".panel-toggle-row").removeClass("expanded") + else + panelToggles[$scope.selected].addClass("selected") + panelToggles[$scope.selected].parent(".panel-toggle-row").addClass("expanded") + + $scope.selected == name + + this + # + # link: (scope, element, attrs) -> + # Panels.registerInitialSelection(scope.object.id, scope.selected) diff --git a/app/assets/javascripts/admin/index_utils/directives/toggle_column.js.coffee b/app/assets/javascripts/admin/index_utils/directives/toggle_column.js.coffee index d6239ff47f..2910e9a7a1 100644 --- a/app/assets/javascripts/admin/index_utils/directives/toggle_column.js.coffee +++ b/app/assets/javascripts/admin/index_utils/directives/toggle_column.js.coffee @@ -1,11 +1,8 @@ -angular.module("admin.indexUtils").directive "ofnToggleColumn", -> +angular.module("admin.indexUtils").directive "ofnToggleColumn", (Columns) -> link: (scope, element, attrs) -> element.addClass "selected" if scope.column.visible + element.click "click", -> scope.$apply -> - if scope.column.visible - scope.column.visible = false - element.removeClass "selected" - else - scope.column.visible = true - element.addClass "selected" + Columns.toggleColumn(scope.column) + element.toggleClass "selected" diff --git a/app/assets/javascripts/admin/index_utils/index_utils.js.coffee b/app/assets/javascripts/admin/index_utils/index_utils.js.coffee index 0925bf45ed..adcd68e3c5 100644 --- a/app/assets/javascripts/admin/index_utils/index_utils.js.coffee +++ b/app/assets/javascripts/admin/index_utils/index_utils.js.coffee @@ -1 +1 @@ -angular.module("admin.indexUtils", ['ngResource']).config ($httpProvider) -> $httpProvider.defaults.headers.common["X-CSRF-Token"] = $("meta[name=csrf-token]").attr("content"); $httpProvider.defaults.headers.common["Accept"] = "application/json, text/javascript, */*"; \ No newline at end of file +angular.module("admin.indexUtils", ['ngResource', 'templates']).config ($httpProvider) -> $httpProvider.defaults.headers.common["X-CSRF-Token"] = $("meta[name=csrf-token]").attr("content"); $httpProvider.defaults.headers.common["Accept"] = "application/json, text/javascript, */*"; \ No newline at end of file diff --git a/app/assets/javascripts/admin/index_utils/services/columns.js.coffee b/app/assets/javascripts/admin/index_utils/services/columns.js.coffee index 46e7ccd3b8..8bd99bf2f2 100644 --- a/app/assets/javascripts/admin/index_utils/services/columns.js.coffee +++ b/app/assets/javascripts/admin/index_utils/services/columns.js.coffee @@ -1,8 +1,18 @@ -angular.module("admin.indexUtils").factory 'Columns', -> +angular.module("admin.indexUtils").factory 'Columns', ($rootScope) -> new class Columns columns: {} + visibleCount: 0 - setColumns: (columns) -> + setColumns: (columns) => @columns = {} @columns[name] = column for name, column of columns + @calculateVisibleCount() @columns + + toggleColumn: (column) => + column.visible = !column.visible + @calculateVisibleCount() + + calculateVisibleCount: => + @visibleCount = (column for name, column of @columns when column.visible).length + $rootScope.$broadcast "columnCount:changed", @visibleCount diff --git a/app/assets/javascripts/admin/index_utils/services/panels.js.coffee b/app/assets/javascripts/admin/index_utils/services/panels.js.coffee new file mode 100644 index 0000000000..27852bed12 --- /dev/null +++ b/app/assets/javascripts/admin/index_utils/services/panels.js.coffee @@ -0,0 +1,19 @@ +angular.module("admin.indexUtils").factory 'Panels', -> + new class Panels + panels: {} + + register: (id, scope) -> + if id? && scope? + @panels[id] = scope + + toggle: (id, name) -> + scope = @panels[id] + selected = scope.getSelected() + switch selected + when name + scope.close() + when null + scope.open(name) + else + scope.setSelected(name) + scope.getSelected() diff --git a/app/assets/javascripts/admin/services/bulk_products.js.coffee b/app/assets/javascripts/admin/services/bulk_products.js.coffee index 6085d48fcc..4e06a87bd2 100644 --- a/app/assets/javascripts/admin/services/bulk_products.js.coffee +++ b/app/assets/javascripts/admin/services/bulk_products.js.coffee @@ -19,6 +19,7 @@ angular.module("ofn.admin").factory "BulkProducts", (PagedFetcher, dataFetcher) # when a respond_overrride for the clone action is used. id = data.product.id dataFetcher("/api/products/" + id + "?template=bulk_show").then (newProduct) => + @unpackProduct newProduct @insertProductAfter(product, newProduct) updateVariantLists: (serverProducts, productsWithUnsavedVariants) -> diff --git a/app/assets/javascripts/admin/services/enterprise_relationships.js.coffee b/app/assets/javascripts/admin/services/enterprise_relationships.js.coffee index e6dcbf15df..8c7d798138 100644 --- a/app/assets/javascripts/admin/services/enterprise_relationships.js.coffee +++ b/app/assets/javascripts/admin/services/enterprise_relationships.js.coffee @@ -26,7 +26,7 @@ angular.module("ofn.admin").factory 'EnterpriseRelationships', ($http, enterpris permission_presentation: (permission) -> switch permission - when "add_to_order_cycle" then "to add to order cycle" - when "manage_products" then "to manage products" - when "edit_profile" then "to edit profile" - when "create_variant_overrides" then "to override variant details" + when "add_to_order_cycle" then "add to order cycle" + when "manage_products" then "manage products" + when "edit_profile" then "edit profile" + when "create_variant_overrides" then "override variant details" diff --git a/app/assets/javascripts/darkswarm/all.js.coffee b/app/assets/javascripts/darkswarm/all.js.coffee index 68bc01b6c1..f4303f8037 100644 --- a/app/assets/javascripts/darkswarm/all.js.coffee +++ b/app/assets/javascripts/darkswarm/all.js.coffee @@ -15,6 +15,7 @@ #= require ../shared/bindonce.min.js #= require ../shared/ng-infinite-scroll.min.js #= require ../shared/angular-local-storage.js +#= require ../shared/angular-slideables.js #= require angularjs-file-upload diff --git a/app/assets/javascripts/darkswarm/controllers/enterprises_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/enterprises_controller.js.coffee index 1e43c17465..3883eca8d5 100644 --- a/app/assets/javascripts/darkswarm/controllers/enterprises_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/enterprises_controller.js.coffee @@ -1,18 +1,69 @@ -Darkswarm.controller "EnterprisesCtrl", ($scope, Enterprises, Search, $document, $rootScope, HashNavigation, FilterSelectorsService, EnterpriseModal) -> +Darkswarm.controller "EnterprisesCtrl", ($scope, $rootScope, $timeout, Enterprises, Search, $document, HashNavigation, FilterSelectorsService, EnterpriseModal, enterpriseMatchesNameQueryFilter, distanceWithinKmFilter) -> $scope.Enterprises = Enterprises - $scope.totalActive = FilterSelectorsService.totalActive - $scope.clearAll = FilterSelectorsService.clearAll - $scope.filterText = FilterSelectorsService.filterText - $scope.FilterSelectorsService = FilterSelectorsService + $scope.totalActive = FilterSelectorsService.totalActive + $scope.clearAll = FilterSelectorsService.clearAll + $scope.filterText = FilterSelectorsService.filterText + $scope.FilterSelectorsService = FilterSelectorsService $scope.query = Search.search() $scope.openModal = EnterpriseModal.open $scope.activeTaxons = [] $scope.show_profiles = false $scope.filtersActive = false + $scope.distanceMatchesShown = false + $scope.$watch "query", (query)-> + Enterprises.flagMatching query Search.search query + $rootScope.$broadcast 'enterprisesChanged' + $scope.distanceMatchesShown = false + + $timeout -> + Enterprises.calculateDistance query, $scope.firstNameMatch() + $rootScope.$broadcast 'enterprisesChanged' + + + $rootScope.$on "enterprisesChanged", -> + $scope.filterEnterprises() + $scope.updateVisibleMatches() + + + # When filter settings change, this could change which name match is at the top, or even + # result in no matches. This affects the reference point that the distance matches are + # calculated from, so we need to recalculate distances. + $scope.$watch '[activeTaxons, shippingTypes, show_profiles]', -> + $timeout -> + Enterprises.calculateDistance $scope.query, $scope.firstNameMatch() + $rootScope.$broadcast 'enterprisesChanged' + , true + $rootScope.$on "$locationChangeSuccess", (newRoute, oldRoute) -> if HashNavigation.active "hubs" $document.scrollTo $("#hubs"), 100, 200 + + + $scope.filterEnterprises = -> + es = Enterprises.hubs + $scope.nameMatches = enterpriseMatchesNameQueryFilter(es, true) + $scope.distanceMatches = enterpriseMatchesNameQueryFilter(es, false) + $scope.distanceMatches = distanceWithinKmFilter($scope.distanceMatches, 50) + + + $scope.updateVisibleMatches = -> + $scope.visibleMatches = if $scope.nameMatches.length == 0 || $scope.distanceMatchesShown + $scope.nameMatches.concat $scope.distanceMatches + else + $scope.nameMatches + + + $scope.showDistanceMatches = -> + $scope.distanceMatchesShown = true + $scope.updateVisibleMatches() + + + $scope.firstNameMatch = -> + if $scope.nameMatchesFiltered? + $scope.nameMatchesFiltered[0] + else + undefined diff --git a/app/assets/javascripts/darkswarm/controllers/home_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/home_controller.js.coffee new file mode 100644 index 0000000000..928f9a4fa1 --- /dev/null +++ b/app/assets/javascripts/darkswarm/controllers/home_controller.js.coffee @@ -0,0 +1,5 @@ +Darkswarm.controller "HomeCtrl", ($scope) -> + $scope.brandStoryExpanded = false + + $scope.toggleBrandStory = -> + $scope.brandStoryExpanded = !$scope.brandStoryExpanded diff --git a/app/assets/javascripts/darkswarm/controllers/line_item_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/line_item_controller.js.coffee index ea62163868..864f25177a 100644 --- a/app/assets/javascripts/darkswarm/controllers/line_item_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/line_item_controller.js.coffee @@ -1,4 +1,5 @@ Darkswarm.controller "LineItemCtrl", ($scope)-> - $scope.$watch "line_item.quantity", (newValue, oldValue)-> + $scope.$watch '[line_item.quantity, line_item.max_quantity]', (newValue, oldValue)-> if newValue != oldValue $scope.Cart.orderChanged() + , true \ No newline at end of file diff --git a/app/assets/javascripts/darkswarm/controllers/order_cycle_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/order_cycle_controller.js.coffee index 2f1394e90a..42d4c1c44d 100644 --- a/app/assets/javascripts/darkswarm/controllers/order_cycle_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/order_cycle_controller.js.coffee @@ -1,6 +1,4 @@ -# TODO this SUCKS. Fix it - -Darkswarm.controller "OrderCycleCtrl", ($scope, OrderCycle, $timeout) -> +Darkswarm.controller "OrderCycleCtrl", ($scope, $timeout, OrderCycle) -> $scope.order_cycle = OrderCycle.order_cycle $scope.OrderCycle = OrderCycle @@ -9,11 +7,26 @@ Darkswarm.controller "OrderCycleCtrl", ($scope, OrderCycle, $timeout) -> # That takes an expression instead of a trigger, and binds to that $timeout => if !$scope.OrderCycle.selected() - $("#order_cycle_id").trigger("openTrigger") + $("#order_cycle_id").trigger("openTrigger") -Darkswarm.controller "OrderCycleChangeCtrl", ($scope, OrderCycle, Products, $timeout) -> +Darkswarm.controller "OrderCycleChangeCtrl", ($scope, $timeout, OrderCycle, Products, Variants, Cart) -> + # Track previous order cycle id for use with revertOrderCycle() + $scope.previous_order_cycle_id = OrderCycle.order_cycle.order_cycle_id + $scope.$watch 'order_cycle.order_cycle_id', (newValue, oldValue)-> + $scope.previous_order_cycle_id = oldValue + $scope.changeOrderCycle = -> - OrderCycle.push_order_cycle Products.update + OrderCycle.push_order_cycle $scope.orderCycleChanged $timeout -> - $("#order_cycle_id").trigger("closeTrigger") + $("#order_cycle_id").trigger("closeTrigger") + + $scope.revertOrderCycle = -> + $scope.order_cycle.order_cycle_id = $scope.previous_order_cycle_id + + $scope.orderCycleChanged = -> + # push_order_cycle clears the cart server-side. Here we call Cart.clear() to clear the + # client-side cart. + Variants.clear() + Cart.clear() + Products.update() diff --git a/app/assets/javascripts/darkswarm/darkswarm.js.coffee b/app/assets/javascripts/darkswarm/darkswarm.js.coffee index e8ea9dce3c..a062ead058 100644 --- a/app/assets/javascripts/darkswarm/darkswarm.js.coffee +++ b/app/assets/javascripts/darkswarm/darkswarm.js.coffee @@ -10,6 +10,7 @@ window.Darkswarm = angular.module("Darkswarm", ["ngResource", 'google-maps', 'duScroll', 'angularFileUpload', + 'angularSlideables' ]).config ($httpProvider, $tooltipProvider, $locationProvider, $anchorScrollProvider) -> $httpProvider.defaults.headers.post['X-CSRF-Token'] = $('meta[name="csrf-token"]').attr('content') $httpProvider.defaults.headers.put['X-CSRF-Token'] = $('meta[name="csrf-token"]').attr('content') diff --git a/app/assets/javascripts/darkswarm/directives/change_hub.js.coffee b/app/assets/javascripts/darkswarm/directives/change_hub.js.coffee new file mode 100644 index 0000000000..a699e18902 --- /dev/null +++ b/app/assets/javascripts/darkswarm/directives/change_hub.js.coffee @@ -0,0 +1,16 @@ +Darkswarm.directive "ofnChangeHub", (CurrentHub, Cart) -> + # Compares scope.hub with CurrentHub. Will trigger an confirmation if they are different, + # and Cart isn't empty + restrict: "A" + scope: + hub: "=ofnChangeHub" + link: (scope, elm, attr)-> + cart_will_need_emptying = -> + CurrentHub.hub?.id and CurrentHub.hub.id isnt scope.hub.id and !Cart.empty() + + if cart_will_need_emptying() + elm.bind 'click', (ev)-> + if confirm "Are you sure? This will change your selected hub and remove any items in your shopping cart." + Cart.clear() + else + ev.preventDefault() diff --git a/app/assets/javascripts/darkswarm/directives/change_order_cycle.js.coffee b/app/assets/javascripts/darkswarm/directives/change_order_cycle.js.coffee new file mode 100644 index 0000000000..a747e5c219 --- /dev/null +++ b/app/assets/javascripts/darkswarm/directives/change_order_cycle.js.coffee @@ -0,0 +1,22 @@ +Darkswarm.directive "ofnChangeOrderCycle", (OrderCycle, Cart, storage) -> + # Compares chosen order cycle with pre-set OrderCycle. Will trigger + # a confirmation if they are different, and Cart isn't empty + restrict: "A" + scope: true + link: (scope, elm, attr)-> + order_cycle_id = -> + parseInt elm.val() + + cart_needs_emptying = -> + OrderCycle.order_cycle?.order_cycle_id && OrderCycle.order_cycle.order_cycle_id != order_cycle_id() && !Cart.empty() + + elm.bind 'change', (ev)-> + if cart_needs_emptying() + if confirm "Are you sure? This will change your selected order cycle and remove any items in your shopping cart." + Cart.clear() + scope.changeOrderCycle() + else + scope.$apply -> + scope.revertOrderCycle() + else + scope.changeOrderCycle() diff --git a/app/assets/javascripts/darkswarm/directives/empties_cart.js.coffee b/app/assets/javascripts/darkswarm/directives/empties_cart.js.coffee deleted file mode 100644 index 51e960867b..0000000000 --- a/app/assets/javascripts/darkswarm/directives/empties_cart.js.coffee +++ /dev/null @@ -1,13 +0,0 @@ -Darkswarm.directive "ofnEmptiesCart", (CurrentHub, Cart, Navigation, storage) -> - # Compares scope.hub with CurrentHub. Will trigger an confirmation if they are different, - # and Cart isn't empty - restrict: "A" - scope: - hub: "=ofnEmptiesCart" - link: (scope, elm, attr)-> - if CurrentHub.hub?.id and CurrentHub.hub.id isnt scope.hub.id and !Cart.empty() - elm.bind 'click', (ev)-> - ev.preventDefault() - if confirm "Are you sure? This will change your selected Hub and remove any items in your shopping cart." - storage.clearAll() # One day this will have to be moar GRANULAR - Navigation.go scope.hub.path diff --git a/app/assets/javascripts/darkswarm/directives/filter_selector.js.coffee b/app/assets/javascripts/darkswarm/directives/filter_selector.js.coffee index 95aee1b413..818af3b785 100644 --- a/app/assets/javascripts/darkswarm/directives/filter_selector.js.coffee +++ b/app/assets/javascripts/darkswarm/directives/filter_selector.js.coffee @@ -22,17 +22,11 @@ Darkswarm.directive "filterSelector", (FilterSelectorsService)-> .map (selector)-> selector.object.id - # This can be called from a parent scope - # when data has been loaded, in order to pass - # selectors up - scope.$on 'loadFilterSelectors', -> - scope.allSelectors = scope.selectors() if attr.allSelectors? - - scope.$watchCollection "selectors()", (newValue, oldValue) -> - scope.allSelectors = scope.selectors() if attr.allSelectors? + scope.$watchCollection "objects()", (newValue, oldValue) -> + scope.allSelectors = scope.buildSelectors() # Build a list of selectors - scope.selectors = -> + scope.buildSelectors = -> # Generate a selector for each object. # NOTE: THESE ARE MEMOIZED to stop new selectors from being created constantly, otherwise function always returns non-identical results # This means the $digest cycle can never close and times out diff --git a/app/assets/javascripts/darkswarm/directives/integer.js.coffee b/app/assets/javascripts/darkswarm/directives/integer.js.coffee new file mode 100644 index 0000000000..e162246122 --- /dev/null +++ b/app/assets/javascripts/darkswarm/directives/integer.js.coffee @@ -0,0 +1,5 @@ +Darkswarm.directive "integer", -> + restrict: 'A' + link: (scope, elem, attr) -> + elem.bind 'input', -> + elem.val Math.round(elem.val()) diff --git a/app/assets/javascripts/darkswarm/directives/page_alert.js.coffee b/app/assets/javascripts/darkswarm/directives/page_alert.js.coffee new file mode 100644 index 0000000000..659bd227e9 --- /dev/null +++ b/app/assets/javascripts/darkswarm/directives/page_alert.js.coffee @@ -0,0 +1,14 @@ +Darkswarm.directive "ofnPageAlert", ($timeout) -> + restrict: 'A' + scope: true + link: (scope, elem, attrs) -> + container_elems = $(".off-canvas-wrap .inner-wrap, .off-canvas-wrap .inner-wrap .fixed, .page-alert") + + # Wait a moment after page load before showing the alert. Otherwise we often miss the + # start of the animation. + $timeout -> + container_elems.addClass("move-down") + , 1000 + + scope.close = -> + container_elems.removeClass("move-down") diff --git a/app/assets/javascripts/darkswarm/directives/scroll_after_load.js.coffee b/app/assets/javascripts/darkswarm/directives/scroll_after_load.js.coffee index 55c5a311da..e29dcbb8f8 100644 --- a/app/assets/javascripts/darkswarm/directives/scroll_after_load.js.coffee +++ b/app/assets/javascripts/darkswarm/directives/scroll_after_load.js.coffee @@ -2,10 +2,9 @@ Darkswarm.directive 'scrollAfterLoad', ($timeout, $location, $document)-> # Scroll to an element on page load restrict: "A" link: (scope, element, attr) -> - if scope.$last is true - $(window).load -> - $timeout -> - elem = $("##{$location.hash()}") - if elem.length > 0 - $document.scrollTo elem , 100, 200, (x)-> - x * (2 - x) + elem = element + $(window).load -> + $timeout -> + if elem? + $document.scrollTo elem, 100, 200, (x) -> + x * (2 - x) diff --git a/app/assets/javascripts/darkswarm/filters/distance_within_km.js.coffee b/app/assets/javascripts/darkswarm/filters/distance_within_km.js.coffee new file mode 100644 index 0000000000..c6a37a2b40 --- /dev/null +++ b/app/assets/javascripts/darkswarm/filters/distance_within_km.js.coffee @@ -0,0 +1,5 @@ +Darkswarm.filter 'distanceWithinKm', -> + (enterprises, range) -> + enterprises ||= [] + enterprises.filter (enterprise) -> + enterprise.distance / 1000 <= range diff --git a/app/assets/javascripts/darkswarm/filters/enterpriseMatchesNameQuery.js.coffee b/app/assets/javascripts/darkswarm/filters/enterpriseMatchesNameQuery.js.coffee new file mode 100644 index 0000000000..6e786a9f62 --- /dev/null +++ b/app/assets/javascripts/darkswarm/filters/enterpriseMatchesNameQuery.js.coffee @@ -0,0 +1,4 @@ +Darkswarm.filter 'enterpriseMatchesNameQuery', -> + (enterprises, matches_name_query) -> + enterprises.filter (enterprise) -> + enterprise.matches_name_query == matches_name_query diff --git a/app/assets/javascripts/darkswarm/services/cart.js.coffee b/app/assets/javascripts/darkswarm/services/cart.js.coffee index bbb0b5fab4..7550b151f0 100644 --- a/app/assets/javascripts/darkswarm/services/cart.js.coffee +++ b/app/assets/javascripts/darkswarm/services/cart.js.coffee @@ -1,4 +1,4 @@ -Darkswarm.factory 'Cart', (CurrentOrder, Variants, $timeout, $http)-> +Darkswarm.factory 'Cart', (CurrentOrder, Variants, $timeout, $http, storage)-> # Handles syncing of current cart/order state to server new class Cart dirty: false @@ -20,7 +20,7 @@ Darkswarm.factory 'Cart', (CurrentOrder, Variants, $timeout, $http)-> $http.post('/orders/populate', @data()).success (data, status)=> @saved() .error (response, status)=> - # TODO what shall we do here? + @scheduleRetry() data: => variants = {} @@ -30,6 +30,12 @@ Darkswarm.factory 'Cart', (CurrentOrder, Variants, $timeout, $http)-> max_quantity: li.max_quantity {variants: variants} + scheduleRetry: => + console.log "Error updating cart: #{status}. Retrying in 3 seconds..." + $timeout => + console.log "Retrying cart update" + @orderChanged() + , 3000 saved: => @dirty = false @@ -63,6 +69,10 @@ Darkswarm.factory 'Cart', (CurrentOrder, Variants, $timeout, $http)-> exists = @line_items.some (li)-> li.variant == variant @create_line_item(variant) unless exists + clear: -> + @line_items = [] + storage.clearAll() # One day this will have to be moar GRANULAR + create_line_item: (variant)-> variant.extended_name = @extendedVariantName(variant) variant.line_item = diff --git a/app/assets/javascripts/darkswarm/services/enterprise_registration_service.js.coffee b/app/assets/javascripts/darkswarm/services/enterprise_registration_service.js.coffee index 77b2204316..702a382eb0 100644 --- a/app/assets/javascripts/darkswarm/services/enterprise_registration_service.js.coffee +++ b/app/assets/javascripts/darkswarm/services/enterprise_registration_service.js.coffee @@ -26,7 +26,11 @@ Darkswarm.factory "EnterpriseRegistrationService", ($http, RegistrationService, RegistrationService.select('about') ).error((data) => Loading.clear() - alert('Failed to create your enterprise.\nPlease ensure all fields are completely filled out.') + if data?.errors? + errors = ("#{k.capitalize()} #{v[0]}" for k, v of data.errors when v.length > 0) + alert "Failed to create your enterprise.\n" + errors.join('\n') + else + alert('Failed to create your enterprise.\nPlease ensure all fields are completely filled out.') ) # RegistrationService.select('about') diff --git a/app/assets/javascripts/darkswarm/services/enterprises.js.coffee b/app/assets/javascripts/darkswarm/services/enterprises.js.coffee index 6040f1d150..2dddfbc7ef 100644 --- a/app/assets/javascripts/darkswarm/services/enterprises.js.coffee +++ b/app/assets/javascripts/darkswarm/services/enterprises.js.coffee @@ -1,4 +1,4 @@ -Darkswarm.factory 'Enterprises', (enterprises, CurrentHub, Taxons, Dereferencer, visibleFilter)-> +Darkswarm.factory 'Enterprises', (enterprises, CurrentHub, Taxons, Dereferencer, visibleFilter, Matcher, Geo, $rootScope)-> new class Enterprises enterprises_by_id: {} constructor: -> @@ -28,3 +28,36 @@ Darkswarm.factory 'Enterprises', (enterprises, CurrentHub, Taxons, Dereferencer, Dereferencer.dereference enterprise.taxons, Taxons.taxons_by_id Dereferencer.dereference enterprise.supplied_taxons, Taxons.taxons_by_id + flagMatching: (query) -> + for enterprise in @enterprises + enterprise.matches_name_query = if query? && query.length > 0 + Matcher.match([enterprise.name], query) + else + false + + calculateDistance: (query, firstMatching) -> + if query?.length > 0 + if firstMatching? + @setDistanceFrom firstMatching + else + @calculateDistanceGeo query + else + @resetDistance() + + calculateDistanceGeo: (query) -> + Geo.geocode query, (results, status) => + $rootScope.$apply => + if status == Geo.OK + #console.log "Geocoded #{query} -> #{results[0].geometry.location}." + @setDistanceFrom results[0].geometry.location + else + console.log "Geocoding failed for the following reason: #{status}" + @resetDistance() + + setDistanceFrom: (locatable) -> + for enterprise in @enterprises + enterprise.distance = Geo.distanceBetween enterprise, locatable + $rootScope.$broadcast 'enterprisesChanged' + + resetDistance: -> + enterprise.distance = null for enterprise in @enterprises diff --git a/app/assets/javascripts/darkswarm/services/geo.js.erb.coffee b/app/assets/javascripts/darkswarm/services/geo.js.erb.coffee new file mode 100644 index 0000000000..2f96722b08 --- /dev/null +++ b/app/assets/javascripts/darkswarm/services/geo.js.erb.coffee @@ -0,0 +1,23 @@ +Darkswarm.service "Geo", -> + new class Geo + OK: google.maps.GeocoderStatus.OK + + # Usage: + # Geo.geocode address, (results, status) -> + # if status == Geo.OK + # console.log results[0].geometry.location + # else + # console.log "Error: #{status}" + geocode: (address, callback) -> + geocoder = new google.maps.Geocoder() + geocoder.geocode {'address': address, 'region': "<%= Spree::Country.find_by_id(Spree::Config[:default_country_id]).iso %>"}, callback + + distanceBetween: (src, dst) -> + google.maps.geometry.spherical.computeDistanceBetween @toLatLng(src), @toLatLng(dst) + + # Wrap an object in a google.maps.LatLng if it has not been already + toLatLng: (locatable) -> + if locatable.lat? + locatable + else + new google.maps.LatLng locatable.latitude, locatable.longitude \ No newline at end of file diff --git a/app/assets/javascripts/darkswarm/services/map.js.coffee b/app/assets/javascripts/darkswarm/services/map.js.coffee index 7ff9f553f2..96768e8379 100644 --- a/app/assets/javascripts/darkswarm/services/map.js.coffee +++ b/app/assets/javascripts/darkswarm/services/map.js.coffee @@ -9,8 +9,8 @@ Darkswarm.factory "OfnMap", (Enterprises, EnterpriseModal, visibleFilter) -> # Adding methods to each enterprise extend: (enterprise) -> new class MapMarker - # We're whitelisting attributes because GMaps tries to crawl - # our data, and our data is recursive, so it breaks + # We cherry-pick attributes because GMaps tries to crawl + # our data, and our data is cyclic, so it breaks latitude: enterprise.latitude longitude: enterprise.longitude icon: enterprise.icon diff --git a/app/assets/javascripts/darkswarm/services/map_configuration.js.coffee b/app/assets/javascripts/darkswarm/services/map_configuration.js.coffee index 8f1d735357..9c5a375d8c 100644 --- a/app/assets/javascripts/darkswarm/services/map_configuration.js.coffee +++ b/app/assets/javascripts/darkswarm/services/map_configuration.js.coffee @@ -7,5 +7,5 @@ Darkswarm.factory "MapConfiguration", -> zoom: 12 additional_options: {} #mapTypeId: 'satellite' - styles: [{"featureType":"landscape","stylers":[{"saturation":-100},{"lightness":65},{"visibility":"on"}]},{"featureType":"poi","stylers":[{"saturation":-100},{"lightness":51},{"visibility":"simplified"}]},{"featureType":"road.highway","stylers":[{"saturation":-100},{"visibility":"simplified"}]},{"featureType":"road.arterial","stylers":[{"saturation":-100},{"lightness":30},{"visibility":"on"}]},{"featureType":"road.local","stylers":[{"saturation":-100},{"lightness":40},{"visibility":"on"}]},{"featureType":"transit","stylers":[{"saturation":-100},{"visibility":"simplified"}]},{"featureType":"administrative.province","stylers":[{"visibility":"off"}]},{"featureType":"water","elementType":"labels","stylers":[{"visibility":"on"},{"lightness":-25},{"saturation":-100}]},{"featureType":"water","elementType":"geometry","stylers":[{"hue":"#ffff00"},{"lightness":-25},{"saturation":-97}]}] + styles: [{"featureType":"landscape","stylers":[{"saturation":-100},{"lightness":65},{"visibility":"on"}]},{"featureType":"poi","stylers":[{"saturation":-100},{"lightness":51},{"visibility":"simplified"}]},{"featureType":"road.highway","stylers":[{"saturation":-100},{"visibility":"simplified"}]},{"featureType":"road.arterial","stylers":[{"saturation":-100},{"lightness":30},{"visibility":"on"}]},{"featureType":"road.local","stylers":[{"saturation":-100},{"lightness":40},{"visibility":"on"}]},{"featureType":"transit","stylers":[{"saturation":-100},{"visibility":"simplified"}]},{"featureType":"administrative.province","stylers":[{"visibility":"off"}]},{"featureType":"water","elementType":"labels","stylers":[{"visibility":"on"},{"lightness":-25},{"saturation":-100}]},{"featureType":"water","elementType":"geometry","stylers":[{"hue":"#ffff00"},{"lightness":-25},{"saturation":-97}]},{"featureType":"road","elementType": "labels.icon","stylers":[{"visibility":"off"}]}] diff --git a/app/assets/javascripts/darkswarm/services/products.js.coffee b/app/assets/javascripts/darkswarm/services/products.js.coffee index 4785adae85..475ed5be0b 100644 --- a/app/assets/javascripts/darkswarm/services/products.js.coffee +++ b/app/assets/javascripts/darkswarm/services/products.js.coffee @@ -10,12 +10,26 @@ Darkswarm.factory 'Products', ($resource, Enterprises, Dereferencer, Taxons, Pro update: => @loading = true - @products = $resource("/shop/products").query (products)=> - @extend() && @dereference() + @products = [] + $resource("/shop/products").query (products)=> + @products = products + + @extend() + @dereference() @registerVariants() @registerVariantsWithCart() @loading = false - @ + + extend: -> + for product in @products + if product.variants?.length > 0 + prices = (v.price for v in product.variants) + product.price = Math.min.apply(null, prices) + product.hasVariants = product.variants?.length > 0 + + product.primaryImage = product.images[0]?.small_url if product.images + product.primaryImageOrMissing = product.primaryImage || "/assets/noimage/small.png" + product.largeImage = product.images[0]?.large_url if product.images dereference: -> for product in @products @@ -42,14 +56,3 @@ Darkswarm.factory 'Products', ($resource, Enterprises, Dereferencer, Taxons, Pro for variant in product.variants Cart.register_variant variant Cart.register_variant product.master if product.master - - extend: -> - for product in @products - if product.variants?.length > 0 - prices = (v.price for v in product.variants) - product.price = Math.min.apply(null, prices) - product.hasVariants = product.variants?.length > 0 - - product.primaryImage = product.images[0]?.small_url if product.images - product.primaryImageOrMissing = product.primaryImage || "/assets/noimage/small.png" - product.largeImage = product.images[0]?.large_url if product.images diff --git a/app/assets/javascripts/darkswarm/services/variants.js.coffee b/app/assets/javascripts/darkswarm/services/variants.js.coffee index 6562bd9e0b..3048049ad5 100644 --- a/app/assets/javascripts/darkswarm/services/variants.js.coffee +++ b/app/assets/javascripts/darkswarm/services/variants.js.coffee @@ -1,6 +1,10 @@ Darkswarm.factory 'Variants', -> new class Variants variants: {} + + clear: -> + @variants = {} + register: (variant)-> @variants[variant.id] ||= @extend variant diff --git a/app/assets/javascripts/darkswarm/util.js.coffee b/app/assets/javascripts/darkswarm/util.js.coffee new file mode 100644 index 0000000000..e6485284b3 --- /dev/null +++ b/app/assets/javascripts/darkswarm/util.js.coffee @@ -0,0 +1,2 @@ +String.prototype.capitalize = -> + this.charAt(0).toUpperCase() + this.slice(1) diff --git a/app/assets/javascripts/shared/angular-slideables.js b/app/assets/javascripts/shared/angular-slideables.js new file mode 100644 index 0000000000..0279c0f4f2 --- /dev/null +++ b/app/assets/javascripts/shared/angular-slideables.js @@ -0,0 +1,55 @@ +/* + * Angular Slideables - A "pure" Angular implementation of jQuery-style slideToggle() + * Source: https://github.com/EricWVGG/AngularSlideables + * By Eric Jacobsen, used under MIT licence + */ + +angular.module('angularSlideables', []) +.directive('slideable', function () { + return { + restrict:'C', + compile: function (element, attr) { + // wrap tag + var contents = element.html(); + element.html(''); + + return function postLink(scope, element, attrs) { + // default properties + attrs.duration = (!attrs.duration) ? '1s' : attrs.duration; + attrs.easing = (!attrs.easing) ? 'ease-in-out' : attrs.easing; + element.css({ + 'overflow': 'hidden', + 'height': '0px', + 'transitionProperty': 'height', + 'transitionDuration': attrs.duration, + 'transitionTimingFunction': attrs.easing + }); + }; + } + }; +}) +.directive('slideToggle', function() { + return { + restrict: 'A', + link: function(scope, element, attrs) { + var target, content; + + attrs.expanded = false; + + element.bind('click', function() { + if (!target) target = document.querySelector(attrs.slideToggle); + if (!content) content = target.querySelector('.slideable_content'); + + if(!attrs.expanded) { + content.style.border = '1px solid rgba(0,0,0,0)'; + var y = content.clientHeight; + content.style.border = 0; + target.style.height = y + 'px'; + } else { + target.style.height = '0px'; + } + attrs.expanded = !attrs.expanded; + }); + } + } +}); diff --git a/app/assets/javascripts/shared/ng-tags-input.min.js b/app/assets/javascripts/shared/ng-tags-input.min.js old mode 100755 new mode 100644 diff --git a/app/assets/javascripts/templates/admin/panel.html.haml b/app/assets/javascripts/templates/admin/panel.html.haml new file mode 100644 index 0000000000..be0c98109f --- /dev/null +++ b/app/assets/javascripts/templates/admin/panel.html.haml @@ -0,0 +1,2 @@ +%td{ colspan: "{{columnCount}}" } + .panel{ ng: { include: "template" } } diff --git a/app/assets/javascripts/templates/admin/panels/enterprise_package.html.haml b/app/assets/javascripts/templates/admin/panels/enterprise_package.html.haml new file mode 100644 index 0000000000..c42d8910e1 --- /dev/null +++ b/app/assets/javascripts/templates/admin/panels/enterprise_package.html.haml @@ -0,0 +1,133 @@ +.row.enterprise_package_panel{ ng: { controller: 'indexPackagePanelCtrl' } } + .alpha.eight.columns + %div{ ng: { if: "!enterprise.is_primary_producer", switch: "enterprise.sells" } } + .info{ ng: { switch: { when: "none" } } } + %h3 Hub Profile + + %p + %strong COST: ALWAYS FREE + + %p People can find and contact you on the Open Food Network. Your enterprise will be visible on the map, and will be searchable in listings. + + %p Having a profile, and making connections within your local food system through the Open Food Network will always be free. + + .info{ ng: { switch: { when: "any" } } } + %h3 Hub Shop + + %p + %strong COST: 2% OF SALES, CAPPED AT $50 PER MONTH + + %p Your enterprise is the backbone of your local food system. You aggregate produce from other enterprises and can sell it through your shop on the Open Food Network. + + %p Hubs can take many forms, whether they be a food co-op, a buying group, a veggie-box program, or a local grocery store. + + %p If you also want to sell your own products, you will need to switch this enterprise to be a producer. + + .info{ ng: { switch: { default: true } } } + %h3 + Please Choose a Package + %i.icon-arrow-right + + %p + %strong Your enterprise will not be fully activated until a package is selected from the options on the left. + + %p + Click on an option to see more detailed information about each package, and hit the red SAVE button when you are done! + + + + %div{ ng: { if: "enterprise.is_primary_producer", switch: "enterprise.sells" } } + .info{ ng: { switch: { when: "none" } } } + %h3 Profile Only + + %p + %strong COST: ALWAYS FREE + + %p A profile makes you visible and contactable to others and is a way to share your story. + + %p If you prefer to focus on producing food, and want to leave the work of selling it to someone else, you won't require a shop on the Open Food Network. + + %p Add your products to Open Food Network, allowing hubs to stock your products in their stores. + + .info{ ng: { switch: { when: "own" } } } + %h3 Producer Shop + + %p + %strong COST: 2% OF SALES, CAPPED AT $50 PER MONTH + + %p Sell your products directly to customers through your very own Open Food Network shopfront. + + %p A Producer Shop is for your produce only, if you want to sell produce grown/produced off site, please select 'Producer Hub'. + + .info{ ng: { switch: { when: "any" } } } + %h3 Producer Hub + + %p + %strong COST: 2% OF SALES, CAPPED AT $50 PER MONTH + + %p Your enterprise is the backbone of your local food system. You can sell your own produce as well as produce aggregated from other enterprises through your shopfront on the Open Food Network. + + %p Producer Hubs can take many forms, whether they be a CSA, a veggie-box program, or a food co-op with a rooftop garden. + + %p The Open Food Network aims to support as many hub models as possible, so no matter your situation, we want to provide the tools you need to run your organisation or local food business. + + .info{ ng: { switch: { default: true } } } + %h3 + Please Choose a Package + %i.icon-arrow-right + + %p + %strong Your producer enterprise will not be fully activated until a package is selected from the options on the left. + + %p + Click on an option to see more detailed information about each package, and hit the red SAVE button when you are done! + + .omega.eight.columns{ ng: { switch: "enterprise.is_primary_producer" } } + %div{ ng: { switch: { when: "false" } } } + %a.button.selector.hub-profile{ ng: { click: "enterprise.owned && (enterprise.sells='none')", class: "{selected: enterprise.sells=='none', disabled: !enterprise.owned}" } } + .top + %h3 Profile Only + %p Get a listing + .bottom ALWAYS FREE + %a.button.selector.hub{ ng: { click: "enterprise.owned && (enterprise.sells='any')", class: "{selected: enterprise.sells=='any', disabled: !enterprise.owned}" } } + .top + %h3 Hub Shop + %p Sell produce from others + .bottom + \2% OF SALES + %br + CAPPED AT $50 PER MONTH + + %div{ ng: { switch: { when: "true" } } } + %a.button.selector.producer-profile{ ng: { click: "enterprise.owned && (enterprise.sells='none')", class: "{selected: enterprise.sells=='none', disabled: !enterprise.owned}" } } + .top + %h3 Profile Only + %p Get a listing + .bottom ALWAYS FREE + %a.button.selector.producer-shop{ ng: { click: "enterprise.owned && (enterprise.sells='own')", class: "{selected: enterprise.sells=='own', disabled: !enterprise.owned}" } } + .top + %h3 Producer Shop + %p Sell your own produce + .bottom + \2% OF SALES + %br + CAPPED AT $50 PER MONTH + %a.button.selector.producer-hub{ ng: { click: "enterprise.owned && (enterprise.sells='any')", class: "{selected: enterprise.sells=='any', disabled: !enterprise.owned}" } } + .top + %h3 Producer Hub + %p Sell produce from self and others + .bottom + \2% OF SALES + %br + CAPPED AT $50 PER MONTH + + %a.button.update.fullwidth{ ng: { show: "enterprise.owned", class: "{disabled: saved() && !saving, saving: saving}", click: "save()" } } + %span{ ng: {hide: "saved() || saving" } } + SAVE + %i.icon-save + %span{ ng: {show: "saved() && !saving" } } + SAVED + %i.icon-ok-sign + %span{ ng: {show: "saving" } } + SAVING + %i.icon-refresh diff --git a/app/assets/javascripts/templates/admin/panels/enterprise_producer.html.haml b/app/assets/javascripts/templates/admin/panels/enterprise_producer.html.haml new file mode 100644 index 0000000000..ea8554892f --- /dev/null +++ b/app/assets/javascripts/templates/admin/panels/enterprise_producer.html.haml @@ -0,0 +1,39 @@ +.row.enterprise_producer_panel{ ng: { controller: 'indexProducerPanelCtrl' } } + + .alpha.eight.columns + .info{ ng: { show: "enterprise.is_primary_producer==true" } } + %h3 Producer + %p Producers make yummy things to eat &/or drink. You're a producer if you grow it, raise it, brew it, bake it, ferment it, milk it or mould it. + + %p Producers can also perform other functions, such as aggregating food from other enterprises and selling it through a shop on the Open Food Network. + + + .info{ ng: { show: "enterprise.is_primary_producer==false" } } + %h3 Non-Producer + %p Non-producers do not produce any food themselves, meaning that they cannot create their own products for sale through the Open Food Network. + + %p Instead, non-producers specialise in linking producers to the end eater, whether it be by aggregating, grading, packing, selling or delivering food. + + .omega.eight.columns + %a.button.selector.producer{ ng: { click: 'enterprise.owned && changeToProducer()', class: "{selected: enterprise.is_primary_producer==true, disabled: !enterprise.owned}" } } + .top + %h3 PRODUCER + %p Producers of food + .bottom eg. GROWERS, BAKERS, BREWERS, MAKERS + + %a.button.selector.non-producer{ ng: { click: 'enterprise.owned && changeToNonProducer()', class: "{selected: enterprise.is_primary_producer==false, disabled: !enterprise.owned}" } } + .top + %h3 Non-Producer + %p All other food enterprises + .bottom eg. Grocery stores, Food co-ops, Buying groups + + %a.button.update.fullwidth{ ng: { show: "enterprise.owned", class: "{disabled: saved() && !saving, saving: saving}", click: "save()" } } + %span{ ng: {hide: "saved() || saving" } } + SAVE + %i.icon-save + %span{ ng: {show: "saved() && !saving" } } + SAVED + %i.icon-ok-sign + %span{ ng: {show: "saving" } } + SAVING + %i.icon-refresh diff --git a/app/assets/javascripts/templates/admin/panels/enterprise_status.html.haml b/app/assets/javascripts/templates/admin/panels/enterprise_status.html.haml new file mode 100644 index 0000000000..39392a497d --- /dev/null +++ b/app/assets/javascripts/templates/admin/panels/enterprise_status.html.haml @@ -0,0 +1,29 @@ +.row.enterprise_status_panel{ ng: { controller: 'indexStatusPanelCtrl' } } + .alpha.omega.sixteen.columns + + %h4.status-ok.text-center{ ng: { show: "issues.length == 0 && warnings.length == 0" } } + %i.icon-ok-sign + {{ object.name }} is set up and ready to go! + + %table{ ng: { show: "issues.length > 0 || warnings.length > 0" } } + %thead + %th.severity + Severity + %th.description + Description + %th.resolve + Resolve + %tr{ ng: { repeat: "issue in issues"} } + %td.severity + %i.icon-warning-sign.issue + %td.description + %span{ bo: { bind: "issue.description" } } + %td.resolve + %div{ ng: { bind: { html: "issue.link" } } } + %tr{ ng: { repeat: "warning in warnings"} } + %td.severity + %i.icon-warning-sign.warning + %td.description + %span{ bo: { bind: "warning.description" } } + %td.resolve + %div{ ng: { bind: { html: "warning.link" } } } diff --git a/app/assets/javascripts/templates/enterprise_modal.html.haml b/app/assets/javascripts/templates/enterprise_modal.html.haml index 6b13f6ee78..350cd6ed80 100644 --- a/app/assets/javascripts/templates/enterprise_modal.html.haml +++ b/app/assets/javascripts/templates/enterprise_modal.html.haml @@ -1,4 +1,5 @@ %ng-include{src: "'partials/enterprise_header.html'"} %ng-include{src: "'partials/enterprise_details.html'"} %ng-include{src: "'partials/hub_details.html'"} +%ng-include{src: "'partials/producer_details.html'"} %ng-include{src: "'partials/close.html'"} diff --git a/app/assets/javascripts/templates/filter_selector.html.haml b/app/assets/javascripts/templates/filter_selector.html.haml index c6990c369f..a53c4f44db 100644 --- a/app/assets/javascripts/templates/filter_selector.html.haml +++ b/app/assets/javascripts/templates/filter_selector.html.haml @@ -1,4 +1,4 @@ -%div{bindonce:true, style: "display: inline-block" } +%ul{ bindonce: true } %active-selector{ ng: { repeat: "selector in allSelectors", show: "ifDefined(selector.fits, true)" } } %render-svg{path: "{{selector.object.icon}}", ng: { if: "selector.object.icon"} } %span{"bo-text" => "selector.object.name"} diff --git a/app/assets/javascripts/templates/partials/enterprise_header.html.haml b/app/assets/javascripts/templates/partials/enterprise_header.html.haml index c0d9d5d9e0..44175a57fe 100644 --- a/app/assets/javascripts/templates/partials/enterprise_header.html.haml +++ b/app/assets/javascripts/templates/partials/enterprise_header.html.haml @@ -2,7 +2,7 @@ .highlight-top.row .small-12.medium-7.large-8.columns %h3{"ng-if" => "enterprise.is_distributor"} - %a{"bo-href" => "enterprise.path", "ofn-empties-cart" => "enterprise"} + %a{"bo-href" => "enterprise.path", "ofn-change-hub" => "enterprise"} %i{"ng-class" => "enterprise.icon_font"} %span{"bo-text" => "enterprise.name"} %h3{"ng-if" => "!enterprise.is_distributor", "ng-class" => "{'is_producer' : enterprise.is_primary_producer}"} @@ -10,4 +10,4 @@ %span{"bo-text" => "enterprise.name"} .small-12.medium-5.large-4.columns.text-right.small-only-text-left %p{"bo-bind" => "[enterprise.address.city, enterprise.address.state_name] | printArray"} - %img.hero-img{"bo-src" => "enterprise.promo_image"} + %img.hero-img{"bo-src" => "enterprise.promo_image"} diff --git a/app/assets/javascripts/templates/partials/hub_actions.html.haml b/app/assets/javascripts/templates/partials/hub_actions.html.haml index f5b451ef3c..c17f9a86f5 100644 --- a/app/assets/javascripts/templates/partials/hub_actions.html.haml +++ b/app/assets/javascripts/templates/partials/hub_actions.html.haml @@ -1,16 +1,15 @@ .row.pad-top{bindonce: true, "ng-if" => "enterprise.hubs.length > 0 && enterprise.is_distributor"} .cta-container.small-12.columns - %label - Shop for - %strong{"bo-text" => "enterprise.name"} + %label + Shop for + %strong{"bo-text" => "enterprise.name"} products at: - %a.cta-hub{"ng-repeat" => "hub in enterprise.hubs", - "bo-href" => "hub.path", + %a.cta-hub{"ng-repeat" => "hub in enterprise.hubs", + "bo-href" => "hub.path", "bo-class" => "{primary: hub.active, secondary: !hub.active}", - "ofn-empties-cart" => "hub"} + "ofn-change-hub" => "hub"} %i.ofn-i_033-open-sign{"bo-if" => "hub.active"} %i.ofn-i_032-closed-sign{"bo-if" => "!hub.active"} - .hub-name{"bo-text" => "hub.name"} + .hub-name{"bo-text" => "hub.name"} .button-address{"bo-bind" => "[hub.address.city, hub.address.state_name] | printArray"} / %i.ofn-i_007-caret-right - diff --git a/app/assets/javascripts/templates/partials/hub_details.html.haml b/app/assets/javascripts/templates/partials/hub_details.html.haml index 8be5ceddac..17fabe846a 100644 --- a/app/assets/javascripts/templates/partials/hub_details.html.haml +++ b/app/assets/javascripts/templates/partials/hub_details.html.haml @@ -5,18 +5,18 @@ %label{"active-table-hub-link" => "enterprise", change: "Change shop to:", shop: "Shop now at:"} .small-8.columns.right %label.right{"bo-if" => "enterprise.pickup || enterprise.delivery"} - Delivery options: - %span{"bo-if" => "enterprise.pickup"} + Delivery options: + %span{"bo-if" => "enterprise.pickup"} %i.ofn-i_038-takeaway Pickup - %span{"bo-if" => "enterprise.delivery"} + %span{"bo-if" => "enterprise.delivery"} %i.ofn-i_039-delivery Delivery .row .columns.small-12 - %a.cta-hub{"bo-href" => "enterprise.path", + %a.cta-hub{"bo-href" => "enterprise.path", "ng-class" => "{primary: enterprise.active, secondary: !enterprise.active}", - "ofn-empties-cart" => "enterprise"} + "ofn-change-hub" => "enterprise"} %i.ofn-i_033-open-sign{"bo-if" => "enterprise.active"} %i.ofn-i_032-closed-sign{"bo-if" => "!enterprise.active"} .hub-name{"bo-text" => "enterprise.name"} diff --git a/app/assets/javascripts/templates/partials/producer_details.html.haml b/app/assets/javascripts/templates/partials/producer_details.html.haml new file mode 100644 index 0000000000..08ac6ea1f1 --- /dev/null +++ b/app/assets/javascripts/templates/partials/producer_details.html.haml @@ -0,0 +1,23 @@ +-# Show places to buy products from this producer, when there are any +-# Do not show this for producer shops selling only their own produce, +-# Since a shopping link will already have been displayed in hub_details.html.haml +.row.active_table_row.pad-top{bindonce: true, "ng-if" => "enterprise.is_primary_producer && enterprise.hubs.length > 0 && !(enterprise.hubs.length == 1 && enterprise.hubs[0] == enterprise)"} + .columns.small-12 + .row + .columns.small-12.fat + %div{"bo-if" => "enterprise.name"} + %label + Shop for + %span.turquoise{"bo-text" => "enterprise.name"} + products at: + %div.show-for-medium-up{"bo-if" => "!enterprise.name"} + + .row.cta-container + .columns.small-12 + %a.cta-hub{"ng-repeat" => "hub in enterprise.hubs | filter:{id: '!'+enterprise.id} | orderBy:'-active'", + "bo-href" => "hub.path", "ofn-empties-cart" => "hub", + "bo-class" => "{primary: hub.active, secondary: !hub.active}"} + %i.ofn-i_033-open-sign{"bo-if" => "hub.active"} + %i.ofn-i_032-closed-sign{"bo-if" => "!hub.active"} + .hub-name{"bo-text" => "hub.name"} + .button-address{"bo-bind" => "[hub.address.city, hub.address.state_name] | printArray"} diff --git a/app/assets/javascripts/templates/product_modal.html.haml b/app/assets/javascripts/templates/product_modal.html.haml index 3db8035acc..481fa67c20 100644 --- a/app/assets/javascripts/templates/product_modal.html.haml +++ b/app/assets/javascripts/templates/product_modal.html.haml @@ -4,17 +4,15 @@ %h3{"bo-text" => "product.name"} %span %em from - %span.avenir{"bo-text" => "enterprise.name"} + %span{"bo-text" => "enterprise.name"} %br .filter-shopfront.taxon-selectors.inline-block - %ul - %filter-selector{ objects: "[product] | taxonsOf" } + %filter-selector{ objects: "[product] | taxonsOf" } .filter-shopfront.property-selectors.inline-block - %ul - %filter-selector{ objects: "[product] | propertiesWithValuesOf" } + %filter-selector{ objects: "[product] | propertiesWithValuesOf" } %div{"ng-if" => "product.description"} %hr diff --git a/app/assets/javascripts/templates/shop_variant.html.haml b/app/assets/javascripts/templates/shop_variant.html.haml index 6d050d0bde..04048f3cd0 100644 --- a/app/assets/javascripts/templates/shop_variant.html.haml +++ b/app/assets/javascripts/templates/shop_variant.html.haml @@ -11,6 +11,7 @@ .small-5.medium-3.large-3.columns.text-right{"bo-if" => "!variant.product.group_buy"} %input{type: :number, + integer: true, value: nil, min: 0, placeholder: "0", @@ -26,14 +27,17 @@ %span.bulk-input %input.bulk.first{type: :number, value: nil, + integer: true, min: 0, "ng-model" => "variant.line_item.quantity", placeholder: "min", "ofn-disable-scroll" => true, max: "{{variant.on_demand && 9999 || variant.count_on_hand }}", name: "variants[{{variant.id}}]", id: "variants_{{variant.id}}"} - %span.bulk-input{"bo-if" => "variant.product.group_buy"} + %span.bulk-input %input.bulk.second{type: :number, + "ng-disabled" => "!variant.line_item.quantity", + integer: true, min: 0, "ng-model" => "variant.line_item.max_quantity", placeholder: "max", diff --git a/app/assets/javascripts/templates/single_line_selectors.html.haml b/app/assets/javascripts/templates/single_line_selectors.html.haml index d54ce57bbb..d8d5ae6f3a 100644 --- a/app/assets/javascripts/templates/single_line_selectors.html.haml +++ b/app/assets/javascripts/templates/single_line_selectors.html.haml @@ -1,8 +1,8 @@ -%ul - -# In order for the single-line-selector scope to have access to the available selectors, - %filter-selector{objects: "objects()", "active-selectors" => "activeSelectors", "all-selectors" => "allSelectors" } +-# In order for the single-line-selector scope to have access to the available selectors, +%filter-selector{objects: "objects()", "active-selectors" => "activeSelectors", "all-selectors" => "allSelectors" } - %li.more{ ng: { show: "overFlowSelectors().length > 0 || fitting" } } +%ul{ ng: { if: "overFlowSelectors().length > 0 || fitting" } } + %li.more %a.dropdown{ data: { dropdown: "{{ 'show-more-' + selectorName }}" }, ng: { class: "{active: selectedOverFlowSelectors().length > 0}" } } %span + {{ overFlowSelectors().length }} more diff --git a/app/assets/stylesheets/admin/account.css.scss b/app/assets/stylesheets/admin/account.css.scss new file mode 100644 index 0000000000..7d58147d91 --- /dev/null +++ b/app/assets/stylesheets/admin/account.css.scss @@ -0,0 +1,17 @@ +.row.invoice_title { + margin-bottom: 0px; +} + +table.invoice_summary { + margin-bottom: 70px; + + tr.total { + font-weight: bold; + } +} + +.invoice_title { + .balance { + color: #9fc820; + } +} diff --git a/app/assets/stylesheets/admin/enterprise_index_panels.css.scss b/app/assets/stylesheets/admin/enterprise_index_panels.css.scss new file mode 100644 index 0000000000..2ce2916497 --- /dev/null +++ b/app/assets/stylesheets/admin/enterprise_index_panels.css.scss @@ -0,0 +1,113 @@ +.enterprise_package_panel, .enterprise_producer_panel { + .info { + p { + font-size: 1rem; + margin: 10px 0px; + } + } + + a.update { + cursor: pointer; + margin-bottom: 10px; + font-size: 1.3rem; + background-color: #DA5354; + &:hover { + background-color: #CD4E4F; + } + &.disabled { + background-color: #C1C1C1; + } + &.saving { + background-color: #FF9848; + i.icon-refresh { + -webkit-animation: spin 2s infinite linear; + animation: spin 2s infinite linear; + } + } + span{ + i{ + font-size: 1.5rem; + margin-left: 10px; + } + } + } + + a.selector { + display: block; + position: relative; + margin-bottom: 20px; + border: 2px solid black; + text-align: center; + // width: 100%; + cursor: pointer; + &, & * { + color: white; + } + &:hover { + &:after { + border-top-color: #9fc820; + } + } + &.disabled{ + background-color: #C1C1C1; + } + .bottom { + background: repeating-linear-gradient(60deg, rgba(84, 152, 218, 0), rgba(84, 152, 218, 0) 5px, rgba(255, 255, 255, 0.25) 5px, rgba(255, 255, 255, 0.25) 10px); + margin-top: 1em; + margin-left: -15px; + margin-right: -15px; + padding: 5px; + text-transform: uppercase; + } + &.selected { + background-color: #000000; + + &:after { + top: 50%; + left: 0; + border: solid transparent; + content: " "; + height: 0; + width: 0; + position: absolute; + pointer-events: none; + + border-top: 20px solid transparent; + border-bottom: 20px solid transparent; + border-right: 20px solid #000000; + margin-top: -20px; + margin-left: -20px; + } + } + } +} + +.enterprise_status_panel { + .status-ok { + margin: 30px 0px; + i.icon-ok-sign { + color: #9fc820; + font-size: 1.5rem; + } + } + + td.description{ + font-size: 0.9rem; + } + + td.severity { + text-align: center; + + i { + font-size: 1.5rem; + + &.issue{ + color: #da5354; + } + + &.warning{ + color: #ff9848; + } + } + } +} diff --git a/app/assets/stylesheets/admin/index_panels.css.scss b/app/assets/stylesheets/admin/index_panels.css.scss new file mode 100644 index 0000000000..a910e41d14 --- /dev/null +++ b/app/assets/stylesheets/admin/index_panels.css.scss @@ -0,0 +1,130 @@ +tr.panel-toggle-row { + td.panel-toggle{ + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + + position: relative; + + i.icon-chevron::before { + font-size: 1.2rem; + content: "\f078"; + } + + &.error::before { + font-family: FontAwesome; + font-weight: normal; + font-style: normal; + display: inline-block; + text-decoration: inherit; + position: absolute; + top: 5px; + right: 5px; + font-size: 2rem; + -webkit-font-smoothing: antialiased; + content: "\f071"; + color: #da5354; + } + + &.status { + i.icon-status::before { + font-size: 1.5rem; + opacity: 0.5; + } + + i.issue::before { + content: "\f071"; + color: #da5354; + } + + i.warning::before { + content: "\f071"; + color: #ff9848; + } + + i.ok::before { + content: "\f058"; + color: #9fc820; + } + } + + &:hover { + cursor: pointer; + background-color: #d0e2f6; + * { + color: #1b3c56; + } + + i.icon-status::before { + opacity: 1.0; + } + } + } + + &.expanded{ + td { + border-bottom: 2px solid #444444; + + &.selected { + background-color: #ffffff; + border-left: 2px solid #444444; + border-right: 2px solid #444444; + border-top: 2px solid #444444; + border-bottom: none; + + &:hover { + background-color: #ffffff; + } + + * { + color: #1b3c56; + } + + i.icon-status::before { + opacity: 1.0; + } + + i.icon-chevron::before { + content: "\f077"; + } + } + } + } +} + +tr.panel-row { + display: none; + + &:hover { + td { + background-color: #ffffff; + } + } + + >td { + border-color: #444444; + padding: 0; + .panel { + border-left: 1px solid #444444; + border-right: 1px solid #444444; + border-bottom: 1px solid #444444; + + .row{ + margin: 0px -4px; + + padding: 20px 0px; + + .column.alpha, .columns.alpha { + padding-left: 20px; + } + + .column.omega, .columns.omega { + padding-right: 20px; + } + } + } + } +} diff --git a/app/assets/stylesheets/darkswarm/_shop-filters.css.sass b/app/assets/stylesheets/darkswarm/_shop-filters.css.sass index 3b31481ee1..cf80c63035 100644 --- a/app/assets/stylesheets/darkswarm/_shop-filters.css.sass +++ b/app/assets/stylesheets/darkswarm/_shop-filters.css.sass @@ -92,24 +92,23 @@ span.filter-label opacity: 0.75 -.filter-shopfront.taxon-selectors, .filter-shopfront.property-selectors - background: transparent - - single-line-selectors - overflow-x: hidden - white-space: nowrap - - .f-dropdown - overflow-x: auto - white-space: normal - - ul - margin: 0 - ul, ul li - list-style: none - - .filter-shopfront + &.taxon-selectors, &.property-selectors + background: transparent + + single-line-selectors + overflow-x: hidden + white-space: nowrap + + .f-dropdown + overflow-x: auto + white-space: normal + + ul + margin: 0 + display: inline-block + ul, ul li + list-style: none // Shopfront taxons &.taxon-selectors diff --git a/app/assets/stylesheets/darkswarm/_shop-navigation.css.sass b/app/assets/stylesheets/darkswarm/_shop-navigation.css.sass index 17b68a3e46..a2f2304d3e 100644 --- a/app/assets/stylesheets/darkswarm/_shop-navigation.css.sass +++ b/app/assets/stylesheets/darkswarm/_shop-navigation.css.sass @@ -1,3 +1,4 @@ +@import typography .darkswarm @@ -20,7 +21,7 @@ margin-right: 12px location - font-family: "AvenirBla_IE", "AvenirBla" + @include headingFont @media all and (max-width: 768px) location, location + small display: block @@ -63,6 +64,7 @@ @media all and (max-width: 768px) font-size: 0.875em closing + @include headingFont @media all and (max-width: 768px) font-size: 1.2em padding-bottom: 10px diff --git a/app/assets/stylesheets/darkswarm/all.scss b/app/assets/stylesheets/darkswarm/all.scss index 80e43cdc8b..6cfc32e605 100644 --- a/app/assets/stylesheets/darkswarm/all.scss +++ b/app/assets/stylesheets/darkswarm/all.scss @@ -2,13 +2,15 @@ * This is a manifest file that'll automatically include all the stylesheets available in this directory * and any sub-directories. You're free to add application-wide styles to this file and they'll appear at * the top of the compiled file, but it's generally better to create a new file per style scope. - + *= require_self - *= require foundation - *= require_tree . */ +@import 'variables'; +@import 'foundation'; @import 'foundation-icons'; +@import '*'; + ofn-modal { display: block; } \ No newline at end of file diff --git a/app/assets/stylesheets/darkswarm/all_split2.css b/app/assets/stylesheets/darkswarm/all_split2.css new file mode 100644 index 0000000000..e231ca5d02 --- /dev/null +++ b/app/assets/stylesheets/darkswarm/all_split2.css @@ -0,0 +1,3 @@ +/* + *= require 'darkswarm/all' + */ diff --git a/app/assets/stylesheets/darkswarm/animations.sass b/app/assets/stylesheets/darkswarm/animations.sass index 62e87e7224..d452b69d39 100644 --- a/app/assets/stylesheets/darkswarm/animations.sass +++ b/app/assets/stylesheets/darkswarm/animations.sass @@ -55,7 +55,7 @@ 100% opacity: 1 -@-webkit-keyframes spin +@-webkit-keyframes spin 0% -webkit-transform: rotate(0deg) transform: rotate(0deg) @@ -104,12 +104,10 @@ .animate-repeat - -webkit-transform: translateZ(0) - transform: translateZ(0) &.ng-move, &.ng-enter, &.ng-leave - -webkit-transition: all 300ms linear - transition: all 300ms linear - + -webkit-transition: all 300ms linear + transition: all 300ms linear + &.ng-leave opacity: 1 &.ng-leave-active @@ -178,7 +176,7 @@ product.animate-repeat overflow: hidden max-height: 0 opacity: 0 !important - + // &.ng-hide-add-active, &.ng-hide-remove-active &.ng-hide-add, &.ng-hide-remove @@ -197,7 +195,7 @@ product.animate-repeat &.ng-hide opacity: 0 !important - + // &.ng-hide-add-active, &.ng-hide-remove-active &.ng-hide-add, &.ng-hide-remove @@ -206,8 +204,8 @@ product.animate-repeat it as hidden. */ display: block !important - - + + @mixin csstrans @@ -217,7 +215,3 @@ product.animate-repeat -o-transition: all 300ms ease transition: all 300ms ease -webkit-transform-style: preserve-3d - - - - diff --git a/app/assets/stylesheets/darkswarm/big-input.sass b/app/assets/stylesheets/darkswarm/big-input.sass index 165404fff3..0660fda460 100644 --- a/app/assets/stylesheets/darkswarm/big-input.sass +++ b/app/assets/stylesheets/darkswarm/big-input.sass @@ -6,14 +6,14 @@ //Big search used in active table search \\ @mixin big-input($input, $inputhvr, $inputactv) - @include avenir + @include headingFont @include csstrans @include border-radius(0.5rem) background: rgba(255,255,255,0.1) border: 2px solid $input font-size: 2rem box-shadow: 0 - padding: 0.75rem 1rem 0.35rem + padding: 0.5rem 1rem height: auto width: 100% margin-bottom: 0.5rem @@ -33,7 +33,7 @@ background: white background: rgba(255,255,255,0.5) text-shadow: 0 0 10px #ffffff - padding: 1.5rem 1rem 1rem + padding: 1rem letter-spacing: 0.02rem outline: none @@ -44,7 +44,7 @@ letter-spacing: 0 @mixin medium-input($input, $inputhvr, $inputactv) - @include avenir + @include headingFont @include csstrans @include border-radius(0.5rem) background: rgba(255,255,255,0.1) diff --git a/app/assets/stylesheets/darkswarm/branding.css.sass b/app/assets/stylesheets/darkswarm/branding.css.sass index da2600818e..76614599cf 100644 --- a/app/assets/stylesheets/darkswarm/branding.css.sass +++ b/app/assets/stylesheets/darkswarm/branding.css.sass @@ -1,3 +1,11 @@ +$ofn-brand: #f27052 +// e.g. australia, uk, norway specific color + +$ofn-grey: #808184 + + +// old colors: + $clr-brick: #c1122b $clr-brick-light: #f5e6e7 $clr-brick-light-trans: rgba(245, 230, 231, 0.9) diff --git a/app/assets/stylesheets/darkswarm/footer.sass b/app/assets/stylesheets/darkswarm/footer.sass index 9e3f9dac55..76dd0f4384 100644 --- a/app/assets/stylesheets/darkswarm/footer.sass +++ b/app/assets/stylesheets/darkswarm/footer.sass @@ -1,15 +1,78 @@ @import branding @import mixins +@import animations footer - background: $dark-grey - border-top: 1px dotted white - @include panepadding .row - &, & * + p a + font-size: 0.875rem + a, a * + @include csstrans color: white - a, a * - color: $clr-brick-light-bright &:hover, &:active, &:focus - color: $clr-brick-bright - @include textsoftpress + color: rgba(white, 1) + text-decoration: underline + + .footer-global + background-color: $ofn-grey + padding-top: 60px + padding-bottom: 40px + .logo + width: 200px + height: 200px + background: $ofn-grey + @include border-radius(120px) + margin: -140px auto 0 auto + img + margin-top: 36px + + .alert-box + background-color: transparent + border: none + padding: 0 + a.big-alert + @include csstrans + width: 100% + border: 1px solid rgba($dark-grey, 0.35) + background-image: url("/assets/tile-wide.png") + background-position: center center + background-color: #bbb + padding: 12px 0 8px 0 + display: block + &, & * + @include csstrans + color: #333 + strong + letter-spacing: 0.5px + &:hover, &:active, &:focus + text-decoration: none + border-color: white + &, & * + color: rgba(white, 1) + .row + &, p, h1, h2, h3, h4, h5, h6 + color: $disabled-bright + + .footer-local + background: lighten($dark-grey, 3%) + @include panepadding + .row + &, p, h1, h2, h3, h4, h5, h6 + color: $disabled-med + p.secure-icon i + font-size: 10rem + color: rgba(white, 0.1) + p.secure-text + color: rgba($disabled-med, 0.35) + .social-icons + margin-bottom: 0.25rem + margin-top: 0.75rem + a + i + font-size: 1.5rem + color: white + &:hover, &:active, &:focus + text-decoration: none + i + color: lighten($dark-grey, 60%) + text-shadow: 2px 2px 0 black diff --git a/app/assets/stylesheets/darkswarm/groups.css.sass b/app/assets/stylesheets/darkswarm/groups.css.sass index 7f70bf7211..97b984eb34 100644 --- a/app/assets/stylesheets/darkswarm/groups.css.sass +++ b/app/assets/stylesheets/darkswarm/groups.css.sass @@ -1,14 +1,16 @@ @import branding @import mixins +@import typography // Search page #groups - background-color: $clr-brick-light + background-color: lighten($clr-brick, 56%) background-image: url("/assets/groups.svg") - background-position: center 15px + background-position: center 50px background-repeat: no-repeat - padding-bottom: 20px - + background-size: 922px 922px + @include sidepaddingSm + @include panepadding a > .group-name &:hover, &:focus, &:active text-decoration: underline @@ -65,7 +67,7 @@ margin-bottom: -2px margin-right: 2px text-transform: capitalize - @include avenir + @include headingFont @include border-radius(1em 0.25em 0 0) @include gradient($disabled-light, $disabled-bright) @media screen and (min-width: 768px) diff --git a/app/assets/stylesheets/darkswarm/header.css.sass b/app/assets/stylesheets/darkswarm/header.css.sass index f0bf6072d6..523afca824 100644 --- a/app/assets/stylesheets/darkswarm/header.css.sass +++ b/app/assets/stylesheets/darkswarm/header.css.sass @@ -1,18 +1,20 @@ +@import variables + nav.top-bar margin-bottom: 0px a.icon &:hover text-decoration: none - height: 45px + height: $topbar-height color: white i font-size: 29px - line-height: 45px + line-height: $topbar-height span font-size: 13px display: inline-block - line-height: 45px - height: 45px + line-height: $topbar-height + height: $topbar-height vertical-align: top body > section[role='main'] diff --git a/app/assets/stylesheets/darkswarm/home.css.sass b/app/assets/stylesheets/darkswarm/home.css.sass deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/app/assets/stylesheets/darkswarm/home_panes.css.sass b/app/assets/stylesheets/darkswarm/home_panes.css.sass index 568b6ebc66..ed74450317 100644 --- a/app/assets/stylesheets/darkswarm/home_panes.css.sass +++ b/app/assets/stylesheets/darkswarm/home_panes.css.sass @@ -1,57 +1,149 @@ @import branding @import mixins - -// Styling for big panes on homepage - -#beta.pane - .row - @include panepadding - background-image: url("/assets/home/macbook.png") - background-repeat: no-repeat - background-position: center bottom - padding-bottom: 280px - &, & * - max-width: 610px +@import typography +@import animations +@import variables -#map.pane - @include darkbg - .row - @include panepadding - background-image: url("/assets/home/maps-bg.svg") - background-repeat: no-repeat - background-position: left center - -#groups.pane - @include darkbg - .row - @include panepadding - background-image: url("/assets/home/groups-bg.svg") - background-repeat: no-repeat - background-position: left center - -#producers.pane - @include turqbg - @include panepadding - background-image: url("/assets/home/producers-bg.svg") - background-repeat: no-repeat - background-position: center center - background-size: 80% 80% - - @media all and (max-width: 768px) - background-position: center top - background-size: 100% 100% - .row - .pricing-table - .title - color: $clr-turquoise-light - .price - background-color: rgba(240, 240, 240, 0.6) - .description, .bullet-item, .cta-button - background-color: rgba(255, 255, 255, 0.8) - -// Responsive -@media all and (max-width: 768px) - #map.pane, #groups.pane, #producers.pane +// Styling for big panes on homepage \\ +#panes + .pane .row - background-position: center center \ No newline at end of file + @include panepadding + padding-top: 75px + padding-bottom: 75px + &.header + padding-bottom: 0 + &.content + padding-top: 0 + + +// Background styles \\ +#system.pane + background-color: white + +#brand-story.pane, #cta.pane, #shops.pane + @include tiledPane + +#stats.pane + background-image: url("/assets/home/background-blurred-oranges.jpg") + background-position: center center + background-color: $ofn-grey + @include fullbg + @include paneWhiteText + + +// Content styles \\ +#brand-story.pane + .row + h2 + font-weight: 300 + font-size: 88px + p + @include bodyFont + font-size: 1.5rem + font-weight: 300 + @media all and (max-width: 768px) + h2 + font-size: 52px + p + font-size: 1.3rem + + a.text-vbig i + font-size: 75px + +#system.pane + .row .row + padding-bottom: 0 + @media all and (max-width: 640px) + .row .row + padding: 0 + + .home-icon-box + background-image: url("/assets/ofn-o.png") + background-position: center center + background-repeat: no-repeat + background-size: auto 100% + padding: 3rem 0 + text-align: center + margin-top: 2rem + @media all and (min-width: 642px) + margin-top: 0 + i + font-size: 4rem + a + display: block + width: 64px + height: 64px + margin: 0 auto + background-color: $brand-colour + background-position: center center + background-repeat: no-repeat + background-size: auto 100% + &.search + background-image: url("/assets/icon-mask-magnifier.png") + &.shop + background-image: url("/assets/icon-mask-apple.png") + &.pick-up-delivery + background-image: url("/assets/icon-mask-truck.png") + + h2 + font-size: 70px + font-weight: 300 + color: $brand-colour + @media all and (max-width: 640px) + font-size: 45px + + + a + color: $brand-colour + + .home-icon-box-bottom + margin-top: 1rem + width: 100% + padding-left: 1rem + padding-right: 1rem + @media all and (min-width: 480px) + padding-left: 3rem + padding-right: 3rem + @media all and (min-width: 642px) + padding-left: 1rem + padding-right: 1rem + h4 + color: $brand-colour + border-bottom: 2px solid lighten($brand-colour, 20%) + text-align: center + padding: 1rem 0 + margin: 1.5rem 0 + +#cta.pane, #stats.pane + h2 + font-weight: 300 + font-size: 45px + margin-bottom: 2rem + @media all and (max-width: 830px) + font-size: 35px + +#stats.pane + .row.header + padding-bottom: 2rem + + h4 + font-weight: 300 + text-transform: uppercase + margin: 1.5rem 0 + display: inline-block + strong + display: block + font-weight: normal + font-size: 75px + +#shops.pane + @include paneWhiteText + h2 + margin-bottom: 2rem + font-size: 4.4rem + font-weight: 300 + +#shops-signup.pane + background-color: $brand-colour diff --git a/app/assets/stylesheets/darkswarm/home_tagline.css.sass b/app/assets/stylesheets/darkswarm/home_tagline.css.sass index ed85e07d60..fadaae70b0 100644 --- a/app/assets/stylesheets/darkswarm/home_tagline.css.sass +++ b/app/assets/stylesheets/darkswarm/home_tagline.css.sass @@ -1,25 +1,33 @@ @import branding @import mixins +@import variables // Styling for brand intro / tagline on homepage #tagline - background-color: black - background-image: url("/assets/home/ofn_bg_1.jpg") - @include fullbg - height: 400px - padding: 40px 0px - h1, h2, p - color: white - h1 - margin-bottom: 1em - h2 - font-size: 1.6875rem - max-width: 610px - margin: 0 auto + width: 100% + &:before + content: "" + @include fullbg + background-color: $ofn-grey + background-image: url("/assets/home/home.jpg") + position: fixed + left: 0 + right: 0 + bottom: 0 + z-index: -1 + width: 100% + height: 100% - a - color: white - &:hover, &:active, &:focus - color: $clr-brick-light-bright - @include textsoftpress + h1 + margin-top: 2rem + @media all and (min-width: 768px) + margin-top: 10rem + img + max-width: 45% + @media all and (min-height: 500px) + max-width: 80% + + margin-bottom: 2rem + @media all and (min-width: 768px) + margin-bottom: 5rem diff --git a/app/assets/stylesheets/darkswarm/hub_node.css.sass b/app/assets/stylesheets/darkswarm/hub_node.css.sass index 94950fa22c..4d175681f2 100644 --- a/app/assets/stylesheets/darkswarm/hub_node.css.sass +++ b/app/assets/stylesheets/darkswarm/hub_node.css.sass @@ -30,9 +30,18 @@ float: right margin-left: 0.5rem + //Hub Link + @media all and (max-width: 640px) + a.hub + display: block + //Hub Name span.hub-name-listing font-weight: 700 + &:after + content: ">>" + display: inline-block + margin-left: 5px //CLOSED row &.closed diff --git a/app/assets/stylesheets/darkswarm/hubs.css.sass b/app/assets/stylesheets/darkswarm/hubs.css.sass index 43f2b8cbea..a351170d99 100644 --- a/app/assets/stylesheets/darkswarm/hubs.css.sass +++ b/app/assets/stylesheets/darkswarm/hubs.css.sass @@ -2,8 +2,9 @@ @import mixins #hubs - background-repeat: repeat - background-image: url("/assets/subtle_white_feathers.png") - // background: $clr-brick-ultra-light url("/assets/home/shopping-bg.jpg") - // @include fullwidthbg + background-color: lighten($ofn-grey, 43%) @include panepadding + @include sidepaddingSm + + .name-matches, .distance-matches + margin-top: 4em \ No newline at end of file diff --git a/app/assets/stylesheets/darkswarm/menu.css.sass b/app/assets/stylesheets/darkswarm/menu.css.sass index b256c6165c..41b86f72ff 100644 --- a/app/assets/stylesheets/darkswarm/menu.css.sass +++ b/app/assets/stylesheets/darkswarm/menu.css.sass @@ -1,49 +1,75 @@ +@import compass @import branding @import mixins @import typography +@import variables -.fixed .top-bar - @include box-shadow(0 2px 3px 0 rgba(0,0,0,0.25)) - nav @include textpress + text-shadow: none + + // Create center style for nav ul (foundation provides left and right) + text-align: center + .top-bar-section + // Avoid menu items blocking logo + li:not(.has-form), li:not(.has-form) a:not(.button), li:not(.has-form) a:not(.button):hover + background-color: transparent + + ul.center + display: inline-block + // By default, we center between the left and right uls, but we want to be centered + // relative to the whole page. The difference in width between the other uls is 74px, + // so we offset by that amount here. + margin-left: -74px + .joyride-tip-guide .button text-shadow: none // Default overrides - big menu - .top-bar-section ul li.ofn-logo > a - display: table-cell - vertical-align: middle - .top-bar-section .has-dropdown > a - padding-right: 15px !important - + padding-right: $topbar-height / 3 !important + i.ofn-i_022-cog font-size: 24px - line-height: 45px - + line-height: $topbar-height + .top-bar-section .has-dropdown > a:after display: none .top-bar-section ul li > a font-size: 0.75rem - height: 45px + height: $topbar-height opacity: 0.8 &:hover, &:focus, &:active opacity: 1 + @include transition(all 0.3s ease-in-out) + + .top-bar-section ul li.ofn-logo > a + display: table-cell + vertical-align: middle + opacity: 1 .nav-branded - color: $clr-brick-light-bright + color: $brand-colour span font-size: 13px .nav-primary - @include avenir + @include headingFont font-size: 0.875rem + font-weight: 300 + ul .nav-primary + text-transform: uppercase + ul.dropdown + border: 1px solid $smoke + border-top: none // Mobile Menu +.tab-bar + background-color: white + .off-canvas-wrap.move-right .tab-bar .menu-icon @include box-shadow(inset 0 0 6px 2px rgba(0,0,0,0.5)) @@ -55,29 +81,57 @@ nav -webkit-box-shadow: 0 0px 0 1px #666, 0 7px 0 1px #666, 0 14px 0 1px #666 box-shadow: 0 0px 0 1px #666, 0 7px 0 1px #666, 0 14px 0 1px #666 +.tab-bar .menu-icon span::after + box-shadow: 0 0 0 1px black, 0 7px 0 1px black, 0 14px 0 1px black + +.tab-bar .ofn-logo + padding: 9px 0 0 9px + +.left-off-canvas-menu + background-color: white + .off-canvas-wrap.move-right ul.off-canvas-list font-size: 0.875rem .li-menu - @include avenir + @include headingFont font-size: 1rem a - color: rgba(255, 255, 255, 0.9) + color: rgba(0, 0, 0, 0.9) + li a + color: rgba(0, 0, 0, 0.9) + &:hover + background-color: transparent + color: $brand-colour + @include transition(all 0.3s ease-in-out) .off-canvas-wrap.move-right ul.off-canvas-list i font-size: 1.5rem margin-right: 0.25rem -// Responsive + +// Responsive + +@media screen and (max-width: 1450px) + nav .top-bar-section + ul li a, .has-dropdown > a + padding: 0 $topbar-height / 8 !important + + ul.center + margin-left: -24px + @media screen and (min-width: 1025px) body.off-canvas // padding required to placehold for fixed menu bar - padding-top: 45px + padding-top: $topbar-height + + @media screen and (max-width: 1025px) - section.right + body.off-canvas + // padding required to placehold for fixed menu bar + padding-top: 0 + section.right .nav-branded padding: 0 1em - - \ No newline at end of file diff --git a/app/assets/stylesheets/darkswarm/mixins.sass b/app/assets/stylesheets/darkswarm/mixins.sass index 6925e84f72..4e779bd2a5 100644 --- a/app/assets/stylesheets/darkswarm/mixins.sass +++ b/app/assets/stylesheets/darkswarm/mixins.sass @@ -5,10 +5,46 @@ // Generic \\ +@mixin tiledPane + background-image: url("/assets/tile-wide.png") + background-color: $brand-colour + background-position: center center + @include paneWhiteText + @mixin panepadding padding-top: 100px padding-bottom: 100px +@mixin paneWhiteText + &, & * + color: white + +@mixin sidepaddingSm + padding-left: 10px + padding-right: 10px + @media all and (min-width: 768px) + padding-left: 20px + padding-right: 20px + @media all and (min-width: 1024px) + padding-left: 50px + padding-right: 50px + @media all and (min-width: 1200px) + padding-left: 100px + padding-right: 100px + +@mixin sidepaddingBg + padding-left: 20px + padding-right: 20px + @media all and (min-width: 768px) + padding-left: 40px + padding-right: 40px + @media all and (min-width: 1024px) + padding-left: 100px + padding-right: 100px + @media all and (min-width: 1200px) + padding-left: 200px + padding-right: 200px + @mixin disabled color: $disabled-bright @@ -53,9 +89,6 @@ // Typography \\ -@mixin avenir - font-family: "AvenirBla_IE", "AvenirBla" - @mixin textpress text-shadow: 0 -1px 1px #111111, 0 1px 2px #222222 @@ -108,7 +141,7 @@ color: $clr-turquoise-bright @mixin fullbg - background-position: center center + background-position: center center background-repeat: no-repeat -webkit-background-size: cover -moz-background-size: cover @@ -116,7 +149,7 @@ background-size: cover @mixin fullwidthbg - background-position: center top + background-position: center top background-repeat: no-repeat background-size: 100% auto @@ -137,4 +170,3 @@ // W3C filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='$gradient-clr1', endColorstr='$gradient-clr2',GradientType=0 ) // IE6-8 - diff --git a/app/assets/stylesheets/darkswarm/page_alert.css.sass b/app/assets/stylesheets/darkswarm/page_alert.css.sass new file mode 100644 index 0000000000..b7d6ea643e --- /dev/null +++ b/app/assets/stylesheets/darkswarm/page_alert.css.sass @@ -0,0 +1,58 @@ +@import branding +@import animations +@import "compass/css3/transition" + +// Basic style \\ +.page-alert + .alert-box + height: 55px + overflow: hidden + border: 1px solid rgba($dark-grey, 0.35) + border-left: none + border-right: none + background-color: #bbb + background-image: url("/assets/tile-wide.png") + background-position: center center + padding: 12px 0 8px 0 + margin: 0 + + h6 + @media all and (max-width: 480px) + font-size: 10px + line-height: 24px + + a.alert-cta + &, & * + @include csstrans + color: #333 + strong + letter-spacing: 0.5px + &:hover, &:active, &:focus + &, & * + text-decoration: none + color: white + + +// Show-hide animation \\ +.off-canvas-wrap .inner-wrap, .off-canvas-wrap .inner-wrap .fixed, nav.tab-bar + @include transition(all, 1000ms, ease-in-out) + + &.move-down + margin-top: 55px + @include transition(all, 1000ms, ease-in-out) + + +.off-canvas-wrap .inner-wrap .page-alert.fixed + top: -55px + z-index: 1 + // TODO: Compass to disable transition + -moz-transition: none + -webkit-transition: none + -o-transition: color 0 ease-in + transition: none + +.off-canvas-wrap.move-right .inner-wrap.move-down + .page-alert + top: -55px * 2 + .left-off-canvas-menu + top: -55px diff --git a/app/assets/stylesheets/darkswarm/producers.css.sass b/app/assets/stylesheets/darkswarm/producers.css.sass index 8f0e7edfcb..2055a45035 100644 --- a/app/assets/stylesheets/darkswarm/producers.css.sass +++ b/app/assets/stylesheets/darkswarm/producers.css.sass @@ -2,9 +2,13 @@ @import mixins .producers - @include fullwidthbg - background-image: url("/assets/producers/producers-pg-bg.jpg") + background-color: lighten($clr-turquoise, 68%) + background-image: url("/assets/producers.svg") + background-position: center 50px background-repeat: no-repeat + background-size: 922px 763px + @include sidepaddingSm + @include panepadding a color: $clr-turquoise &:hover, &:active, &:focus diff --git a/app/assets/stylesheets/darkswarm/registration.css.sass b/app/assets/stylesheets/darkswarm/registration.css.sass index f63eb30421..ba56701600 100644 --- a/app/assets/stylesheets/darkswarm/registration.css.sass +++ b/app/assets/stylesheets/darkswarm/registration.css.sass @@ -4,7 +4,6 @@ #registration-modal header text-align: center - // background-color: #efefef @media all and (max-width: 64em) text-align: left .container diff --git a/app/assets/stylesheets/darkswarm/shop.css.sass b/app/assets/stylesheets/darkswarm/shop.css.sass index 9d38398f36..5422c4463a 100644 --- a/app/assets/stylesheets/darkswarm/shop.css.sass +++ b/app/assets/stylesheets/darkswarm/shop.css.sass @@ -12,7 +12,6 @@ @import shop-popovers .darkswarm - products display: block padding-top: 20px diff --git a/app/assets/stylesheets/darkswarm/shopping-cart.css.sass b/app/assets/stylesheets/darkswarm/shopping-cart.css.sass index cb71913831..74cec02ce0 100644 --- a/app/assets/stylesheets/darkswarm/shopping-cart.css.sass +++ b/app/assets/stylesheets/darkswarm/shopping-cart.css.sass @@ -38,6 +38,7 @@ padding: 4px 12px color: #fff .buttons + margin-bottom: 0.1em .button height: auto top: 0px diff --git a/app/assets/stylesheets/darkswarm/signup.css.sass b/app/assets/stylesheets/darkswarm/signup.css.sass new file mode 100644 index 0000000000..cd6ebf458a --- /dev/null +++ b/app/assets/stylesheets/darkswarm/signup.css.sass @@ -0,0 +1,121 @@ +@import branding +@import mixins +@import typography +@import animations +@import variables + + +#producer-signup.pane, #shops-signup.pane + @include tiledPane + + h2 + margin-bottom: 2rem + font-size: 4.4rem + font-weight: 300 + +#producer-details.pane, #hub-details.pane, .groups-details.pane + background-color: lighten($ofn-grey, 44%) + + +#producer-case-studies, #shops-case-studies + padding-top: 100px + padding-bottom: 100px + background-color: $brand-colour + background-image: url("/assets/hubs-bg.jpg") + background-position: center center + -webkit-filter: brightness(1.1) + filter: brightness(1.1) + h2 + color: $brand-colour + font-size: 3rem + .case-study + background-color: rgba(255, 255, 255, 0.5) + padding: 1rem + margin-top: 2rem + text-align: center + .case-study-img + background-color: white + margin-bottom: 1rem + @media all and (min-width: 768px) + float: right + margin-left: 2rem + @media all and (min-width: 640px) + text-align: left + h4, a + color: $brand-colour + a + &, & * + @include csstrans + opacity: 1 + &:hover, &:focus, &:active + &, & * + opacity: 0.75 + + +// Signup tables \\ +table.signup-table + width: 100% + border: 0 + +table.signup-table.hubs-table, table.signup-table.producers-table + tr + td + background-color: white + border-bottom: 1px solid rgba($ofn-grey, 0.3) + td:nth-child(2) + background-color: lighten($ofn-grey, 46%) + td:nth-child(3) + background-color: lighten($ofn-grey, 41%) + td:last-child + &, & i + color: $brand-colour + border-bottom: 1px solid rgba($brand-colour, 0.3) + background-color: lighten($brand-colour, 48%) + thead + background-color: transparent + tr + td + border-bottom: 1px solid transparent + td:nth-child(1) + background-color: transparent + td:nth-child(2) + background: lighten($ofn-grey, 44%) + td:nth-child(3) + background: lighten($ofn-grey, 38%) + td:last-child + &, & * + color: white + background: $brand-colour + h5 + text-transform: uppercase + color: $ofn-grey + font-weight: 400 + font-size: 0.875rem + margin-bottom: 0.25em + + tfoot + background-color: transparent + tr + td + border-bottom: 1px solid transparent + td:nth-child(1) + background-color: transparent + td:nth-child(2) + background: lighten($ofn-grey, 44%) + td:nth-child(3) + background: lighten($ofn-grey, 38%) + td:last-child + &, & * + color: white + background: $brand-colour + h2 + .text-small + text-transform: uppercase + display: inline-block + font-weight: 400 + line-height: 1.5 + @include headingFont + +// Detail \\ +.enterprise-type-flowchart + float: right diff --git a/app/assets/stylesheets/darkswarm/style.css b/app/assets/stylesheets/darkswarm/style.css.scss old mode 100755 new mode 100644 similarity index 100% rename from app/assets/stylesheets/darkswarm/style.css rename to app/assets/stylesheets/darkswarm/style.css.scss diff --git a/app/assets/stylesheets/darkswarm/tabs.css.sass b/app/assets/stylesheets/darkswarm/tabs.css.sass index 0c911eb66a..12134a19e1 100644 --- a/app/assets/stylesheets/darkswarm/tabs.css.sass +++ b/app/assets/stylesheets/darkswarm/tabs.css.sass @@ -37,7 +37,7 @@ text-align: left a - @include avenir + @include headingFont background: transparent text-transform: uppercase line-height: 1 diff --git a/app/assets/stylesheets/darkswarm/typography.css.sass b/app/assets/stylesheets/darkswarm/typography.css.sass index f549420d83..97230d5b0a 100644 --- a/app/assets/stylesheets/darkswarm/typography.css.sass +++ b/app/assets/stylesheets/darkswarm/typography.css.sass @@ -1,30 +1,27 @@ @import branding -//@import mixins -@font-face - font-family: 'AvenirBla_IE' - src: url("/AveniBla.eot") format("opentype") +@mixin headingFont + font-family: 'Oswald', sans-serif -@font-face - font-family: 'AvenirBla' - src: url("/AvenirLTStd-Black.otf") format("opentype") +@mixin bodyFont + font-family: 'Roboto', Arial, sans-serif -@font-face - font-family: 'AvenirMed_IE' - src: url("/AveniMed.eot") format("opentype") +$headingFont: 'Oswald' +$bodyFont: 'Roboto' -@font-face - font-family: 'AvenirMed' - src: url("/AvenirLTStd-Medium.otf") format("opentype") - -$font-helvetica: "Helvetica Neue", "HelveticaNeue", "Helvetica", Helvetica, Arial, sans-serif - +body + @include bodyFont + font-weight: 400 a color: $clr-brick &:hover, &:focus, &:active text-decoration: none color: $clr-brick-bright +.text-vbig + font-size: 2rem + font-weight: 300 + .text-big font-size: 1.5rem font-weight: 300 @@ -35,17 +32,17 @@ small, .small .text-small font-size: 0.875rem margin-bottom: 0.5rem - font-family: $font-helvetica + font-family: $bodyFont &, & * font-size: 0.875rem .text-normal font-weight: 400 - font-family: $font-helvetica + font-family: $bodyFont .text-skinny font-weight: 300 - font-family: $font-helvetica + font-family: $bodyFont .word-wrap word-wrap: break-word @@ -69,11 +66,11 @@ small, .small .brick color: $clr-brick -@mixin avenir - font-family: "AvenirBla_IE", "AvenirBla" +.hr-light + border-color: rgba(#ddd, 0.25) -h1, h2, h3, h4, h5, h6, .avenir - @include avenir +h1, h2, h3, h4, h5, h6 + @include headingFont padding: 0px ul.bullet-list, ul.check-list @@ -108,8 +105,8 @@ ul.check-list .not-bold font-weight: normal -strong.avenir - font-weight: normal // Avenir is basically bold anyway +.footer-pad + padding-bottom: 100px // These selectors match the default Foundation selectors diff --git a/app/assets/stylesheets/darkswarm/ui.css.sass b/app/assets/stylesheets/darkswarm/ui.css.sass index d18adae1b4..5c049a2bba 100644 --- a/app/assets/stylesheets/darkswarm/ui.css.sass +++ b/app/assets/stylesheets/darkswarm/ui.css.sass @@ -1,18 +1,19 @@ @import foundation/components/buttons @import branding @import mixins +@import typography // Button class extensions .neutral-btn @include button @include border-radius(0.5em) - font-family: 'Open Sans', Calibri, Candara, Segoe, "Segoe UI", Optima, Arial, sans-serif + font-family: $bodyFont background-color: transparent border: 2px solid rgba(200, 200, 200, 1) color: #999 -.neutral-btn:hover, .neutral-btn:active, .neutral-btn:focus +.neutral-btn:hover, .neutral-btn:active, .neutral-btn:focus background-color: rgba(200, 200, 200, 0.2) border: 2px solid rgba(200, 200, 200, 0.8) @@ -20,7 +21,7 @@ border-color: #000 color: #000 -.neutral-btn.dark:hover, .neutral-btn.dark:active, .neutral-btn.dark:focus +.neutral-btn.dark:hover, .neutral-btn.dark:active, .neutral-btn.dark:focus background-color: rgba(0, 0, 0, 0.1) border: 2px solid rgba(0, 0, 0, 0.8) text-shadow: 0 1px 0 #fff @@ -29,18 +30,18 @@ border-color: #fff color: #fff -.neutral-btn.light:hover, .neutral-btn.light:active, .neutral-btn.light:focus +.neutral-btn.light:hover, .neutral-btn.light:active, .neutral-btn.light:focus background-color: rgba(255, 255, 255, 0.2) border: 2px solid rgba(255, 255, 255, 0.8) - text-shadow: 0 1px 0 $clr-brick + text-shadow: 0 1px 0 rgba(0,0,0,0.2) .neutral-btn.turquoise border-color: $clr-turquoise color: $clr-turquoise -.neutral-btn.turquoise:hover, .neutral-btn.turquoise:active, .neutral-btn.turquoise:focus +.neutral-btn.turquoise:hover, .neutral-btn.turquoise:active, .neutral-btn.turquoise:focus background-color: rgba(0, 0, 0, 0.1) - text-shadow: 0 1px 0 #fff + // text-shadow: 0 1px 0 #fff // Rewrite foundation's .primary button style @@ -49,7 +50,7 @@ outline: none // Turn off blue highlight on chrome .button.primary, button.primary - font-family: 'Open Sans', Calibri, Candara, Segoe, "Segoe UI", Optima, Arial, sans-serif + font-family: $bodyFont background: $clr-brick color: white @@ -58,7 +59,7 @@ text-shadow: 0 1px 0 $clr-brick button.success, .button.success - background: #0096ad + background: #0096ad .button.success:hover, .button.success:active, .button.success:focus, button.success:hover, button.success:active, button.success:focus background: #14b6cc @@ -77,11 +78,20 @@ button.success, .button.success label margin: 0 0.2rem float: right - -// Responsive +// Transparent button +.button.transparent + @include headingFont + font-size: 20px + text-transform: uppercase + background: rgba(0, 0, 0, 0.3) + border: 3px solid #fff + text-shadow: none + &:hover + background: rgba(0, 0, 0, 0.6) + color: #fff + +// Responsive @media screen and (min-width: 768px) [role="main"] padding: 0 - - diff --git a/app/assets/stylesheets/darkswarm/variables.css.sass b/app/assets/stylesheets/darkswarm/variables.css.sass index 1020f7cbaa..ea87d11fc4 100644 --- a/app/assets/stylesheets/darkswarm/variables.css.sass +++ b/app/assets/stylesheets/darkswarm/variables.css.sass @@ -1 +1,32 @@ -// necessary; user to overwrite Foundation variables css \ No newline at end of file +@import "foundation/functions" +@import "foundation/components/global" + +// Brand guide colours: +// International: #81c26e +// Australia: #f35746 +// Africa: #f35e32 +// South Africa: #f9a72b +// Norway: #4b83cc +// Scandanavia: #0c8bbc +// UK: #e6373f + +$brand-colour: #f27052 + + +// Topbar +$topbar-height: rem-calc(75) +$topbar-link-padding: $topbar-height / 3 + +$topbar-bg: $white +$topbar-bg-color: $topbar-bg + +$topbar-link-color: $black +$topbar-link-color-hover: $brand-colour +$topbar-link-color-active: $black +$topbar-link-color-active-hover: $white +$topbar-link-bg-hover: $white + +$topbar-dropdown-link-color: $black +$topbar-dropdown-bg: $white +$topbar-dropdown-link-bg: $white +$topbar-dropdown-link-bg-hover: $white diff --git a/app/controllers/admin/account_controller.rb b/app/controllers/admin/account_controller.rb new file mode 100644 index 0000000000..a752b2cfdd --- /dev/null +++ b/app/controllers/admin/account_controller.rb @@ -0,0 +1,9 @@ +class Admin::AccountController < Spree::Admin::BaseController + + def show + @invoices = spree_current_user.account_invoices + # @enterprises = Enterprise.where(id: BillablePeriod.where(owner_id: spree_current_user).map(&:enterprise_id)) + # .group_by('enterprise.id').joins(:billable_periods) + # .select('SUM(billable_periods.turnover) AS turnover').order('turnover DESC') + end +end diff --git a/app/controllers/admin/accounts_and_billing_settings_controller.rb b/app/controllers/admin/accounts_and_billing_settings_controller.rb new file mode 100644 index 0000000000..0f3b986cef --- /dev/null +++ b/app/controllers/admin/accounts_and_billing_settings_controller.rb @@ -0,0 +1,74 @@ +require 'open_food_network/accounts_and_billing_settings_validator' + +class Admin::AccountsAndBillingSettingsController < Spree::Admin::BaseController + before_filter :load_distributors, only: [:edit, :update, :start_job] + before_filter :load_jobs, only: [:edit, :update, :start_job] + before_filter :load_settings, only: [:edit, :update, :start_job] + before_filter :require_valid_settings, only: [:update, :start_job] + before_filter :require_known_job, only: [:start_job] + + def update + Spree::Config.set(params[:settings]) + flash[:success] = t(:successfully_updated, :resource => t(:billing_and_account_settings)) + redirect_to_edit + end + + def start_job + if @update_account_invoices_job || @finalize_account_invoices_job + flash[:error] = "A task is already running, please wait until it has finished" + else + new_job = "#{params[:job][:name]}".camelize.constantize.new + Delayed::Job.enqueue new_job + flash[:success] = "Task Queued" + end + + redirect_to_edit + end + + def show_methods + @enterprise = Enterprise.find_by_id(params[:enterprise_id]) + @shipping_methods = @enterprise.shipping_methods + @payment_methods = @enterprise.payment_methods + render partial: 'method_settings' + end + + private + + def redirect_to_edit + redirect_to main_app.edit_admin_accounts_and_billing_settings_path + end + + def require_valid_settings + render :edit unless @settings.valid? + end + + def known_jobs + ['update_account_invoices', 'finalize_account_invoices'] + end + + def require_known_job + unless known_jobs.include?(params[:job][:name]) + flash[:error] = "Unknown Task: #{params[:job][:name].to_s}" + redirect_to_edit + end + end + + def load_settings + @settings = OpenFoodNetwork::AccountsAndBillingSettingsValidator.new(params[:settings] || { + accounts_distributor_id: Spree::Config[:accounts_distributor_id], + default_accounts_payment_method_id: Spree::Config[:default_accounts_payment_method_id], + default_accounts_shipping_method_id: Spree::Config[:default_accounts_shipping_method_id], + auto_update_invoices: Spree::Config[:auto_update_invoices], + auto_finalize_invoices: Spree::Config[:auto_finalize_invoices] + }) + end + + def load_distributors + @distributors = Enterprise.is_distributor.select([:id, :name]) + end + + def load_jobs + @update_account_invoices_job = Delayed::Job.where("handler LIKE (?)", "%UpdateAccountInvoices%").last + @finalize_account_invoices_job = Delayed::Job.where("handler LIKE (?)", "%FinalizeAccountInvoices%").last + end +end diff --git a/app/controllers/admin/contents_controller.rb b/app/controllers/admin/contents_controller.rb new file mode 100644 index 0000000000..87c86997d9 --- /dev/null +++ b/app/controllers/admin/contents_controller.rb @@ -0,0 +1,29 @@ +module Admin + class ContentsController < Spree::Admin::BaseController + def edit + @preference_sections = [{name: 'Header', preferences: [:logo, :logo_mobile, :logo_mobile_svg]}, + {name: 'Home page', preferences: [:home_hero, :home_show_stats]}, + {name: 'Producer signup page', preferences: [:producer_signup_pricing_table_html, :producer_signup_case_studies_html, :producer_signup_detail_html]}, + {name: 'Hub signup page', preferences: [:hub_signup_pricing_table_html, :hub_signup_case_studies_html, :hub_signup_detail_html]}, + {name: 'Group signup page', preferences: [:group_signup_pricing_table_html, :group_signup_case_studies_html, :group_signup_detail_html]}, + {name: 'Footer', preferences: [:footer_logo, + :footer_facebook_url, :footer_twitter_url, :footer_instagram_url, :footer_linkedin_url, :footer_googleplus_url, :footer_pinterest_url, + :footer_email, :footer_links_md, :footer_about_url, :footer_tos_url]}] + end + + def update + params.each do |name, value| + if ContentConfig.has_preference?(name) || ContentConfig.has_attachment?(name) + ContentConfig.send("#{name}=", value) + end + end + + # Save any uploaded images + ContentConfig.save + + flash[:success] = t(:successfully_updated, :resource => "Your content") + + redirect_to main_app.edit_admin_content_path + end + end +end diff --git a/app/controllers/admin/customers_controller.rb b/app/controllers/admin/customers_controller.rb index 27b5b1380d..b1ceb88c2f 100644 --- a/app/controllers/admin/customers_controller.rb +++ b/app/controllers/admin/customers_controller.rb @@ -17,7 +17,7 @@ module Admin private def collection - return Customer.where("1=0") if html_request? || params[:enterprise_id].nil? + return Customer.where("1=0") unless json_request? && params[:enterprise_id].present? enterprise = Enterprise.managed_by(spree_current_user).find_by_id(params[:enterprise_id]) Customer.of(enterprise) end diff --git a/app/controllers/admin/enterprises_controller.rb b/app/controllers/admin/enterprises_controller.rb index e50779c3ff..5ccc9b9c92 100644 --- a/app/controllers/admin/enterprises_controller.rb +++ b/app/controllers/admin/enterprises_controller.rb @@ -3,7 +3,7 @@ require 'open_food_network/referer_parser' module Admin class EnterprisesController < ResourceController before_filter :load_enterprise_set, :only => :index - before_filter :load_countries, :except => [:index, :set_sells, :check_permalink] + before_filter :load_countries, :except => [:index, :register, :check_permalink] before_filter :load_methods_and_fees, :only => [:new, :edit, :update, :create] before_filter :load_groups, :only => [:new, :edit, :update, :create] before_filter :load_taxons, :only => [:new, :edit, :update, :create] @@ -18,34 +18,61 @@ module Admin before_filter :load_properties, only: [:edit, :update] before_filter :setup_property, only: [:edit] - helper 'spree/products' include ActionView::Helpers::TextHelper include OrderCyclesHelper - def set_sells - enterprise = Enterprise.find_by_permalink(params[:id]) || Enterprise.find(params[:id]) - attributes = { sells: params[:sells] } - attributes[:producer_profile_only] = params[:sells] == "none" && !!params[:producer_profile_only] - attributes[:shop_trial_start_date] = Time.now if params[:sells] == "own" + def index + respond_to do |format| + format.html + format.json do + render json: @collection, each_serializer: Api::Admin::IndexEnterpriseSerializer, spree_current_user: spree_current_user + end + end + end - if %w(none own).include?(params[:sells]) - if params[:sells] == 'own' && enterprise.shop_trial_start_date - expiry = enterprise.shop_trial_start_date + Enterprise::SHOP_TRIAL_LENGTH.days - if Time.now > expiry - flash[:error] = "Sorry, but you've already had a trial. Expired on: #{expiry.strftime('%Y-%m-%d')}" - else - attributes.delete :shop_trial_start_date - enterprise.update_attributes(attributes) - flash[:notice] = "Welcome back! Your trial expires on: #{expiry.strftime('%Y-%m-%d')}" - end - elsif enterprise.update_attributes(attributes) - flash[:success] = "Congratulations! Registration for #{enterprise.name} is complete!" + def welcome + render layout: "spree/layouts/bare_admin" + end + + + def update + invoke_callbacks(:update, :before) + if @object.update_attributes(params[object_name]) + invoke_callbacks(:update, :after) + flash[:success] = flash_message_for(@object, :successfully_updated) + respond_with(@object) do |format| + format.html { redirect_to location_after_save } + format.js { render :layout => false } + format.json { render json: @object, serializer: Api::Admin::IndexEnterpriseSerializer, spree_current_user: spree_current_user } end else - flash[:error] = "Unauthorised" + invoke_callbacks(:update, :fails) + respond_with(@object) do |format| + format.json { render json: { errors: @object.errors.messages }, status: :unprocessable_entity } + end + end + end + + def register + if params[:sells] == 'unspecified' + flash[:error] = "Please select a package" + return render :welcome, layout: "spree/layouts/bare_admin" + end + + attributes = { sells: params[:sells], visible: true } + + if ['own', 'any'].include? params[:sells] + attributes[:shop_trial_start_date] = @enterprise.shop_trial_start_date || Time.now + end + + if @enterprise.update_attributes(attributes) + flash[:success] = "Congratulations! Registration for #{@enterprise.name} is complete!" + redirect_to admin_path + else + flash[:error] = "Could not complete registration for #{@enterprise.name}" + render :welcome, layout: "spree/layouts/bare_admin" end - redirect_to admin_path end def bulk_update @@ -98,7 +125,7 @@ module Admin private def load_enterprise_set - @enterprise_set = EnterpriseSet.new collection + @enterprise_set = EnterpriseSet.new(collection) if spree_current_user.admin? end def load_countries @@ -112,6 +139,16 @@ module Admin coordinator = Enterprise.find_by_id(params[:coordinator_id]) if params[:coordinator_id] order_cycle = OrderCycle.new(coordinator: coordinator) if order_cycle.nil? && coordinator.present? return OpenFoodNetwork::OrderCyclePermissions.new(spree_current_user, order_cycle).visible_enterprises + when :index + if spree_current_user.admin? + OpenFoodNetwork::Permissions.new(spree_current_user). + editable_enterprises. + order('is_primary_producer ASC, name') + elsif json_request? + OpenFoodNetwork::Permissions.new(spree_current_user).editable_enterprises + else + Enterprise.where("1=0") unless json_request? + end else # TODO was ordered with is_distributor DESC as well, not sure why or how we want to sort this now OpenFoodNetwork::Permissions.new(spree_current_user). @@ -141,13 +178,15 @@ module Admin def check_can_change_bulk_sells unless spree_current_user.admin? params[:enterprise_set][:collection_attributes].each do |i, enterprise_params| - enterprise_params.delete :sells + enterprise_params.delete :sells unless spree_current_user == Enterprise.find_by_id(enterprise_params[:id]).owner end end end def check_can_change_sells - params[:enterprise].delete :sells unless spree_current_user.admin? + unless spree_current_user.admin? || spree_current_user == @enterprise.owner + params[:enterprise].delete :sells + end end def override_owner @@ -202,7 +241,7 @@ module Admin # Overriding method on Spree's resource controller def location_after_save referer_path = OpenFoodNetwork::RefererParser::path(request.referer) - refered_from_edit = referer_path == main_app.edit_admin_enterprise_path(@enterprise) + refered_from_edit = referer_path =~ /\/edit$/ if params[:enterprise].key?(:producer_properties_attributes) && !refered_from_edit main_app.admin_enterprises_path else diff --git a/app/controllers/admin/order_cycles_controller.rb b/app/controllers/admin/order_cycles_controller.rb index 8cc0aef9b4..9e1155d6b7 100644 --- a/app/controllers/admin/order_cycles_controller.rb +++ b/app/controllers/admin/order_cycles_controller.rb @@ -64,8 +64,8 @@ module Admin end def bulk_update - @order_cycle_set = OrderCycleSet.new(params[:order_cycle_set]) - if @order_cycle_set.save + @order_cycle_set = params[:order_cycle_set] && OrderCycleSet.new(params[:order_cycle_set]) + if @order_cycle_set.andand.save redirect_to main_app.admin_order_cycles_path, :notice => 'Order cycles have been updated.' else render :index @@ -138,10 +138,12 @@ module Admin end def remove_unauthorized_bulk_attrs - params[:order_cycle_set][:collection_attributes].each do |i, hash| - order_cycle = OrderCycle.find(hash[:id]) - unless Enterprise.managed_by(spree_current_user).include?(order_cycle.andand.coordinator) - params[:order_cycle_set][:collection_attributes].delete i + if params.key? :order_cycle_set + params[:order_cycle_set][:collection_attributes].each do |i, hash| + order_cycle = OrderCycle.find(hash[:id]) + unless Enterprise.managed_by(spree_current_user).include?(order_cycle.andand.coordinator) + params[:order_cycle_set][:collection_attributes].delete i + end end end end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index aaa7d0bb06..7dfd404b69 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -4,6 +4,7 @@ class ApplicationController < ActionController::Base protect_from_forgery include EnterprisesHelper + helper CssSplitter::ApplicationHelper def redirect_to(options = {}, response_status = {}) ::Rails.logger.error("Redirected by #{caller(1).first rescue "unknown"}") diff --git a/app/controllers/checkout_controller.rb b/app/controllers/checkout_controller.rb index 11be28ef04..b23a57ebde 100644 --- a/app/controllers/checkout_controller.rb +++ b/app/controllers/checkout_controller.rb @@ -23,7 +23,7 @@ class CheckoutController < Spree::CheckoutController return if redirect_to_paypal_express_form_if_needed end - if @order.next + if advance_order_state(@order) state_callback(:after) else if @order.errors.present? @@ -83,6 +83,16 @@ class CheckoutController < Spree::CheckoutController params[:order] end + # Perform order.next, guarding against StaleObjectErrors + def advance_order_state(order) + tries ||= 3 + order.next + + rescue ActiveRecord::StaleObjectError + retry unless (tries -= 1).zero? + false + end + def update_failed clear_ship_address diff --git a/app/controllers/enterprise_confirmations_controller.rb b/app/controllers/enterprise_confirmations_controller.rb index d275260599..0375a04d49 100644 --- a/app/controllers/enterprise_confirmations_controller.rb +++ b/app/controllers/enterprise_confirmations_controller.rb @@ -40,7 +40,7 @@ class EnterpriseConfirmationsController < DeviseController def new_user_reset_path(resource) password = Devise.friendly_token.first(8) user = Spree::User.create(email: resource.email, password: password, password_confirmation: password) - user.send_reset_password_instructions + user.send_reset_password_instructions_without_delay resource.users << user spree.edit_spree_user_password_path(user, :reset_password_token => user.reset_password_token, return_to: spree.admin_path) end diff --git a/app/controllers/enterprises_controller.rb b/app/controllers/enterprises_controller.rb index 75ad5c475b..8a875c76bf 100644 --- a/app/controllers/enterprises_controller.rb +++ b/app/controllers/enterprises_controller.rb @@ -9,55 +9,6 @@ class EnterprisesController < BaseController respond_to :js, only: :permalink_checker - def index - @enterprises = Enterprise.all - end - - def suppliers - @suppliers = Enterprise.is_primary_producer - end - - def distributors - @distributors = Enterprise.is_distributor - - respond_to do |format| - format.js do - @distributor_details = Hash[@distributors.map { |d| [d.id, render_to_string(:partial => 'enterprises/distributor_details', :locals => {:distributor => d})] }] - end - format.html do - @distributors - end - end - end - - def show - @enterprise = Enterprise.find_by_permalink(params[:id]) || Enterprise.find(params[:id]) - - # User can view this page if they've already chosen their distributor, or if this page - # is for a supplier, they may use it to select a distributor that sells this supplier's - # products. - unless current_distributor || @enterprise.is_primary_producer - redirect_to spree.root_path and return - end - - - options = {:enterprise_id => params[:id]} - options.merge(params.reject { |k,v| k == :id }) - - @products = [] - - if @enterprise.is_primary_producer - @distributors = Enterprise.distributing_any_product_of(@enterprise.supplied_products).by_name.all - end - - if current_order_cycle - @searcher = Spree::Config.searcher_class.new(options) - @products = @searcher.retrieve_products - - order_cycle_products = current_order_cycle.products_distributed_by(current_distributor) - @products = @products & order_cycle_products - end - end def check_permalink return render text: params[:permalink], status: 409 if Enterprise.find_by_permalink params[:permalink] diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index 3bb7a68538..07ba097f85 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -2,8 +2,11 @@ class HomeController < BaseController layout 'darkswarm' def index - end - - def about_us + if ContentConfig.home_show_stats + @num_distributors = Enterprise.is_distributor.activated.visible.count + @num_producers = Enterprise.is_primary_producer.activated.visible.count + @num_users = Spree::User.joins(:orders).merge(Spree::Order.complete).count('DISTINCT spree_users.*') + @num_orders = Spree::Order.complete.count + end end end diff --git a/app/controllers/shop_controller.rb b/app/controllers/shop_controller.rb index 0655624a0b..ef78605b41 100644 --- a/app/controllers/shop_controller.rb +++ b/app/controllers/shop_controller.rb @@ -12,13 +12,17 @@ class ShopController < BaseController def products if @products = products_for_shop + enterprise_fee_calculator = OpenFoodNetwork::EnterpriseFeeCalculator.new current_distributor, current_order_cycle + render status: 200, json: ActiveModel::ArraySerializer.new(@products, each_serializer: Api::ProductSerializer, current_order_cycle: current_order_cycle, current_distributor: current_distributor, variants: variants_for_shop_by_id, - master_variants: master_variants_for_shop_by_id).to_json + master_variants: master_variants_for_shop_by_id, + enterprise_fee_calculator: enterprise_fee_calculator, + ).to_json else render json: "", status: 404 @@ -42,10 +46,12 @@ class ShopController < BaseController def products_for_shop if current_order_cycle + scoper = OpenFoodNetwork::ScopeProductToHub.new(current_distributor) + current_order_cycle. valid_products_distributed_by(current_distributor). order(taxon_order). - each { |p| p.scope_to_hub current_distributor }. + each { |p| scoper.scope(p) }. select { |p| !p.deleted? && p.has_stock_for_distribution?(current_order_cycle, current_distributor) } end end @@ -65,9 +71,10 @@ class ShopController < BaseController # We use the in_stock? method here instead of the in_stock scope because we need to # look up the stock as overridden by VariantOverrides, and the scope method is not affected # by them. + scoper = OpenFoodNetwork::ScopeVariantToHub.new(current_distributor) Spree::Variant. for_distribution(current_order_cycle, current_distributor). - each { |v| v.scope_to_hub current_distributor }. + each { |v| scoper.scope(v) }. select(&:in_stock?) end diff --git a/app/controllers/shops_controller.rb b/app/controllers/shops_controller.rb new file mode 100644 index 0000000000..3305811e07 --- /dev/null +++ b/app/controllers/shops_controller.rb @@ -0,0 +1,6 @@ +class ShopsController < BaseController + layout 'darkswarm' + + def index + end +end diff --git a/app/controllers/spree/admin/base_controller_decorator.rb b/app/controllers/spree/admin/base_controller_decorator.rb index 3fa6a5c5e1..4579e905e7 100644 --- a/app/controllers/spree/admin/base_controller_decorator.rb +++ b/app/controllers/spree/admin/base_controller_decorator.rb @@ -62,4 +62,8 @@ Spree::Admin::BaseController.class_eval do def html_request? request.format.html? end + + def json_request? + request.format.json? + end end diff --git a/app/controllers/spree/admin/overview_controller_decorator.rb b/app/controllers/spree/admin/overview_controller_decorator.rb index 6eea9c6ea4..ffd2569c1f 100644 --- a/app/controllers/spree/admin/overview_controller_decorator.rb +++ b/app/controllers/spree/admin/overview_controller_decorator.rb @@ -5,16 +5,22 @@ Spree::Admin::OverviewController.class_eval do @product_count = Spree::Product.active.managed_by(spree_current_user).count @order_cycle_count = OrderCycle.active.managed_by(spree_current_user).count + unspecified = spree_current_user.owned_enterprises.where(sells: 'unspecified') + outside_referral = !URI(request.referer.to_s).path.match(/^\/admin/) + if OpenFoodNetwork::Permissions.new(spree_current_user).manages_one_enterprise? && !spree_current_user.admin? @enterprise = @enterprises.first - if @enterprise.sells == "unspecified" - render "welcome", layout: "spree/layouts/bare_admin" + if outside_referral && unspecified.any? + redirect_to main_app.welcome_admin_enterprise_path(@enterprise) else render "single_enterprise_dashboard" end else - render "multi_enterprise_dashboard" + if outside_referral && unspecified.any? + redirect_to main_app.admin_enterprises_path + else + render "multi_enterprise_dashboard" + end end end end - diff --git a/app/controllers/spree/admin/reports_controller_decorator.rb b/app/controllers/spree/admin/reports_controller_decorator.rb index 81975bb250..50762a97e1 100644 --- a/app/controllers/spree/admin/reports_controller_decorator.rb +++ b/app/controllers/spree/admin/reports_controller_decorator.rb @@ -1,13 +1,18 @@ require 'csv' require 'open_food_network/order_and_distributor_report' require 'open_food_network/products_and_inventory_report' +require 'open_food_network/lettuce_share_report' require 'open_food_network/group_buy_report' require 'open_food_network/order_grouper' require 'open_food_network/customers_report' require 'open_food_network/users_and_enterprises_report' require 'open_food_network/order_cycle_management_report' +require 'open_food_network/packing_report' require 'open_food_network/sales_tax_report' require 'open_food_network/xero_invoices_report' +require 'open_food_network/bulk_coop_report' +require 'open_food_network/payments_report' +require 'open_food_network/orders_and_fulfillments_report' Spree::Admin::ReportsController.class_eval do @@ -22,7 +27,8 @@ Spree::Admin::ReportsController.class_eval do ], products_and_inventory: [ ['All products', :all_products], - ['Inventory (on hand)', :inventory] + ['Inventory (on hand)', :inventory], + ['LettuceShare', :lettuce_share] ], customers: [ ["Mailing List", :mailing_list], @@ -31,11 +37,15 @@ Spree::Admin::ReportsController.class_eval do order_cycle_management: [ ["Payment Methods Report", :payment_methods], ["Delivery Report", :delivery] + ], + packing: [ + ["Pack By Customer", :pack_by_customer], + ["Pack By Supplier", :pack_by_supplier] ] } # Fetches user's distributors, suppliers and order_cycles - before_filter :load_data, only: [:customers, :products_and_inventory, :order_cycle_management] + before_filter :load_data, only: [:customers, :products_and_inventory, :order_cycle_management, :packing] # Render a partial for orders and fulfillment description respond_override :index => { :html => { :success => lambda { @@ -47,7 +57,9 @@ Spree::Admin::ReportsController.class_eval do render_to_string(partial: 'customers_description', layout: false, locals: {report_types: REPORT_TYPES[:customers]}).html_safe @reports[:order_cycle_management][:description] = render_to_string(partial: 'order_cycle_management_description', layout: false, locals: {report_types: REPORT_TYPES[:order_cycle_management]}).html_safe - } } } + @reports[:packing][:description] = + render_to_string(partial: 'packing_description', layout: false, locals: {report_types: REPORT_TYPES[:packing]}).html_safe +} } } # Overide spree reports list. @@ -75,19 +87,33 @@ Spree::Admin::ReportsController.class_eval do render_report(@report.header, @report.table, params[:csv], "order_cycle_management_#{timestamp}.csv") end + def packing + # -- Prepare date parameters + prepare_date_params params + + # -- Prepare form options + my_distributors = Enterprise.is_distributor.managed_by(spree_current_user) + my_suppliers = Enterprise.is_primary_producer.managed_by(spree_current_user) + + # My distributors and any distributors distributing products I supply + @distributors = my_distributors | Enterprise.with_distributed_products_outer.merge(Spree::Product.in_any_supplier(my_suppliers)) + # My suppliers and any suppliers supplying products I distribute + @suppliers = my_suppliers | my_distributors.map { |d| Spree::Product.in_distributor(d) }.flatten.map(&:supplier).uniq + @order_cycles = OrderCycle.active_or_complete.accessible_by(spree_current_user).order('orders_close_at DESC') + @report_types = REPORT_TYPES[:packing] + @report_type = params[:report_type] + + # -- Build Report with Order Grouper + @report = OpenFoodNetwork::PackingReport.new spree_current_user, params + order_grouper = OpenFoodNetwork::OrderGrouper.new @report.rules, @report.columns + @table = order_grouper.table(@report.table_items) + csv_file_name = "#{params[:report_type]}_#{timestamp}.csv" + + render_report(@report.header, @table, params[:csv], "packing_#{timestamp}.csv") + end + def orders_and_distributors - params[:q] ||= {} - - if params[:q][:completed_at_gt].blank? - params[:q][:completed_at_gt] = Time.zone.now.beginning_of_month - else - params[:q][:completed_at_gt] = Time.zone.parse(params[:q][:completed_at_gt]).beginning_of_day rescue Time.zone.now.beginning_of_month - end - - if params[:q] && !params[:q][:completed_at_lt].blank? - params[:q][:completed_at_lt] = Time.zone.parse(params[:q][:completed_at_lt]).end_of_day rescue "" - end - params[:q][:meta_sort] ||= "completed_at.desc" + prepare_date_params params @search = Spree::Order.complete.not_state(:canceled).managed_by(spree_current_user).search(params[:q]) orders = @search.result @@ -105,18 +131,7 @@ Spree::Admin::ReportsController.class_eval do end def sales_tax - params[:q] ||= {} - - if params[:q][:completed_at_gt].blank? - params[:q][:completed_at_gt] = Time.zone.now.beginning_of_month - else - params[:q][:completed_at_gt] = Time.zone.parse(params[:q][:completed_at_gt]).beginning_of_day rescue Time.zone.now.beginning_of_month - end - - if params[:q] && !params[:q][:completed_at_lt].blank? - params[:q][:completed_at_lt] = Time.zone.parse(params[:q][:completed_at_lt]).end_of_day rescue "" - end - params[:q][:meta_sort] ||= "completed_at.desc" + prepare_date_params params @search = Spree::Order.complete.not_state(:canceled).managed_by(spree_current_user).search(params[:q]) orders = @search.result @@ -135,300 +150,47 @@ Spree::Admin::ReportsController.class_eval do end def bulk_coop - params[:q] ||= {} - - if params[:q][:completed_at_gt].blank? - params[:q][:completed_at_gt] = Time.zone.now.beginning_of_month - else - params[:q][:completed_at_gt] = Time.zone.parse(params[:q][:completed_at_gt]).beginning_of_day rescue Time.zone.now.beginning_of_month - end - - if params[:q] && !params[:q][:completed_at_lt].blank? - params[:q][:completed_at_lt] = Time.zone.parse(params[:q][:completed_at_lt]).end_of_day rescue "" - end - params[:q][:meta_sort] ||= "completed_at.desc" - - @search = Spree::Order.complete.not_state(:canceled).managed_by(spree_current_user).search(params[:q]) - - orders = @search.result - @line_items = orders.map { |o| o.line_items.managed_by(spree_current_user) }.flatten + # -- Prepare date parameters + prepare_date_params params + # -- Prepare form options @distributors = Enterprise.is_distributor.managed_by(spree_current_user) @report_type = params[:report_type] - case params[:report_type] - when "bulk_coop_supplier_report" + # -- Build Report with Order Grouper + @report = OpenFoodNetwork::BulkCoopReport.new spree_current_user, params + order_grouper = OpenFoodNetwork::OrderGrouper.new @report.rules, @report.columns + @table = order_grouper.table(@report.table_items) + csv_file_name = "bulk_coop_#{params[:report_type]}_#{timestamp}.csv" - header = ["Supplier", "Product", "Unit Size", "Variant", "Weight", "Sum Total", "Sum Max Total", "Units Required", "Remainder"] - - columns = [ proc { |lis| lis.first.variant.product.supplier.name }, - proc { |lis| lis.first.variant.product.name }, - proc { |lis| lis.first.variant.product.group_buy ? (lis.first.variant.product.group_buy_unit_size || 0.0) : "" }, - proc { |lis| lis.first.variant.full_name }, - proc { |lis| lis.first.variant.weight || 0 }, - proc { |lis| lis.sum { |li| li.quantity } }, - proc { |lis| lis.sum { |li| li.max_quantity || 0 } }, - proc { |lis| "" }, - proc { |lis| "" } ] - - rules = [ { group_by: proc { |li| li.variant.product.supplier }, - sort_by: proc { |supplier| supplier.name } }, - { group_by: proc { |li| li.variant.product }, - sort_by: proc { |product| product.name }, - summary_columns: [ proc { |lis| lis.first.variant.product.supplier.name }, - proc { |lis| lis.first.variant.product.name }, - proc { |lis| lis.first.variant.product.group_buy ? (lis.first.variant.product.group_buy_unit_size || 0.0) : "" }, - proc { |lis| "" }, - proc { |lis| "" }, - proc { |lis| lis.sum { |li| (li.quantity || 0) * (li.variant.weight || 0) } }, - proc { |lis| lis.sum { |li| (li.max_quantity || 0) * (li.variant.weight || 0) } }, - proc { |lis| ( (lis.first.variant.product.group_buy_unit_size || 0).zero? ? 0 : ( lis.sum { |li| ( [li.max_quantity || 0, li.quantity || 0].max ) * (li.variant.weight || 0) } / lis.first.variant.product.group_buy_unit_size ) ).floor }, - proc { |lis| lis.sum { |li| ( [li.max_quantity || 0, li.quantity || 0].max) * (li.variant.weight || 0) } - ( ( (lis.first.variant.product.group_buy_unit_size || 0).zero? ? 0 : ( lis.sum { |li| ( [li.max_quantity || 0, li.quantity || 0].max) * (li.variant.weight || 0) } / lis.first.variant.product.group_buy_unit_size ) ).floor * (lis.first.variant.product.group_buy_unit_size || 0) ) } ] }, - { group_by: proc { |li| li.variant }, - sort_by: proc { |variant| variant.full_name } } ] - - when "bulk_coop_allocation" - - header = ["Customer", "Product", "Unit Size", "Variant", "Weight", "Sum Total", "Sum Max Total", "Total Allocated", "Remainder"] - - columns = [ proc { |lis| lis.first.order.bill_address.firstname + " " + lis.first.order.bill_address.lastname }, - proc { |lis| lis.first.variant.product.name }, - proc { |lis| lis.first.variant.product.group_buy ? (lis.first.variant.product.group_buy_unit_size || 0.0) : "" }, - proc { |lis| lis.first.variant.full_name }, - proc { |lis| lis.first.variant.weight || 0 }, - proc { |lis| lis.sum { |li| li.quantity } }, - proc { |lis| lis.sum { |li| li.max_quantity || 0 } }, - proc { |lis| "" }, - proc { |lis| "" } ] - - rules = [ { group_by: proc { |li| li.variant.product }, - sort_by: proc { |product| product.name }, - summary_columns: [ proc { |lis| "TOTAL" }, - proc { |lis| lis.first.variant.product.name }, - proc { |lis| lis.first.variant.product.group_buy ? (lis.first.variant.product.group_buy_unit_size || 0.0) : "" }, - proc { |lis| "" }, - proc { |lis| "" }, - proc { |lis| lis.sum { |li| li.quantity * (li.variant.weight || 0) } }, - proc { |lis| lis.sum { |li| (li.max_quantity || 0) * (li.variant.weight || 0) } }, - proc { |lis| ( (lis.first.variant.product.group_buy_unit_size || 0).zero? ? 0 : ( lis.sum { |li| ( [li.max_quantity || 0, li.quantity || 0].max ) * (li.variant.weight || 0) } / lis.first.variant.product.group_buy_unit_size ) ).floor * (lis.first.variant.product.group_buy_unit_size || 0) }, - proc { |lis| lis.sum { |li| ( [li.max_quantity || 0, li.quantity || 0].max ) * (li.variant.weight || 0) } - ( ( (lis.first.variant.product.group_buy_unit_size || 0).zero? ? 0 : ( lis.sum { |li| ( [li.max_quantity || 0, li.quantity || 0].max ) * (li.variant.weight || 0) } / lis.first.variant.product.group_buy_unit_size ) ).floor * (lis.first.variant.product.group_buy_unit_size || 0) ) } ] }, - { group_by: proc { |li| li.variant }, - sort_by: proc { |variant| variant.full_name } }, - { group_by: proc { |li| li.order }, - sort_by: proc { |order| order.to_s } } ] - - when "bulk_coop_packing_sheets" - - header = ["Customer", "Product", "Variant", "Sum Total"] - - columns = [ proc { |lis| lis.first.order.bill_address.firstname + " " + lis.first.order.bill_address.lastname }, - proc { |lis| lis.first.variant.product.name }, - proc { |lis| lis.first.variant.full_name }, - proc { |lis| lis.sum { |li| li.quantity } } ] - - rules = [ { group_by: proc { |li| li.variant.product }, - sort_by: proc { |product| product.name } }, - { group_by: proc { |li| li.variant }, - sort_by: proc { |variant| variant.full_name } }, - { group_by: proc { |li| li.order }, - sort_by: proc { |order| order.to_s } } ] - - when "bulk_coop_customer_payments" - - header = ["Customer", "Date of Order", "Total Cost", "Amount Owing", "Amount Paid"] - - columns = [ proc { |lis| lis.first.order.bill_address.firstname + " " + lis.first.order.bill_address.lastname }, - proc { |lis| lis.first.order.completed_at.to_s }, - proc { |lis| lis.map { |li| li.order }.uniq.sum { |o| o.total } }, - proc { |lis| lis.map { |li| li.order }.uniq.sum { |o| o.outstanding_balance } }, - proc { |lis| lis.map { |li| li.order }.uniq.sum { |o| o.payment_total } } ] - - rules = [ { group_by: proc { |li| li.order }, - sort_by: proc { |order| order.completed_at } } ] - - else # List all line items - - header = ["Supplier", "Product", "Unit Size", "Variant", "Weight", "Sum Total", "Sum Max Total", "Units Required", "Remainder"] - - columns = [ proc { |lis| lis.first.variant.product.supplier.name }, - proc { |lis| lis.first.variant.product.name }, - proc { |lis| lis.first.variant.product.group_buy ? (lis.first.variant.product.group_buy_unit_size || 0.0) : "" }, - proc { |lis| lis.first.variant.full_name }, - proc { |lis| lis.first.variant.weight || 0 }, - proc { |lis| lis.sum { |li| li.quantity } }, - proc { |lis| lis.sum { |li| li.max_quantity || 0 } }, - proc { |lis| "" }, - proc { |lis| "" } ] - - rules = [ { group_by: proc { |li| li.variant.product.supplier }, - sort_by: proc { |supplier| supplier.name } }, - { group_by: proc { |li| li.variant.product }, - sort_by: proc { |product| product.name }, - summary_columns: [ proc { |lis| lis.first.variant.product.supplier.name }, - proc { |lis| lis.first.variant.product.name }, - proc { |lis| lis.first.variant.product.group_buy ? (lis.first.variant.product.group_buy_unit_size || 0.0) : "" }, - proc { |lis| "" }, - proc { |lis| "" }, - proc { |lis| lis.sum { |li| li.quantity * (li.variant.weight || 0) } }, - proc { |lis| lis.sum { |li| (li.max_quantity || 0) * (li.variant.weight || 0) } }, - proc { |lis| ( (lis.first.variant.product.group_buy_unit_size || 0).zero? ? 0 : ( lis.sum { |li| ( [li.max_quantity || 0, li.quantity || 0].max ) * (li.variant.weight || 0) } / lis.first.variant.product.group_buy_unit_size ) ).floor }, - proc { |lis| lis.sum { |li| ( [li.max_quantity || 0, li.quantity || 0].max ) * (li.variant.weight || 0) } - ( ( (lis.first.variant.product.group_buy_unit_size || 0).zero? ? 0 : ( lis.sum { |li| ( [li.max_quantity || 0, li.quantity || 0].max ) * (li.variant.weight || 0) } / lis.first.variant.product.group_buy_unit_size ) ).floor * (lis.first.variant.product.group_buy_unit_size || 0) ) } ] }, - { group_by: proc { |li| li.variant }, - sort_by: proc { |variant| variant.full_name } } ] - - end - - order_grouper = OpenFoodNetwork::OrderGrouper.new rules, columns - - @header = header - @table = order_grouper.table(@line_items) - csv_file_name = "bulk_coop_#{timestamp}.csv" - - render_report(@header, @table, params[:csv], csv_file_name) + render_report(@report.header, @table, params[:csv], csv_file_name) end def payments - params[:q] ||= {} - - if params[:q][:completed_at_gt].blank? - params[:q][:completed_at_gt] = Time.zone.now.beginning_of_month - else - params[:q][:completed_at_gt] = Time.zone.parse(params[:q][:completed_at_gt]).beginning_of_day rescue Time.zone.now.beginning_of_month - end - - if params[:q] && !params[:q][:completed_at_lt].blank? - params[:q][:completed_at_lt] = Time.zone.parse(params[:q][:completed_at_lt]).end_of_day rescue "" - end - params[:q][:meta_sort] ||= "completed_at.desc" - - @search = Spree::Order.complete.not_state(:canceled).managed_by(spree_current_user).search(params[:q]) - - orders = @search.result - payments = orders.map { |o| o.payments.select { |payment| payment.completed? } }.flatten # Only select completed payments + # -- Prepare Date Params + prepare_date_params params + # -- Prepare Form Options @distributors = Enterprise.is_distributor.managed_by(spree_current_user) @report_type = params[:report_type] - case params[:report_type] - when "payments_by_payment_type" - table_items = payments - - header = ["Payment State", "Distributor", "Payment Type", "Total (#{currency_symbol})"] - - columns = [ proc { |payments| payments.first.order.payment_state }, - proc { |payments| payments.first.order.distributor.name }, - proc { |payments| payments.first.payment_method.name }, - proc { |payments| payments.sum { |payment| payment.amount } } ] - - rules = [ { group_by: proc { |payment| payment.order.payment_state }, - sort_by: proc { |payment_state| payment_state } }, - { group_by: proc { |payment| payment.order.distributor }, - sort_by: proc { |distributor| distributor.name } }, - { group_by: proc { |payment| Spree::PaymentMethod.unscoped { payment.payment_method } }, - sort_by: proc { |method| method.name } } ] - - when "itemised_payment_totals" - table_items = orders - - header = ["Payment State", "Distributor", "Product Total (#{currency_symbol})", "Shipping Total (#{currency_symbol})", "Outstanding Balance (#{currency_symbol})", "Total (#{currency_symbol})"] - - columns = [ proc { |orders| orders.first.payment_state }, - proc { |orders| orders.first.distributor.name }, - proc { |orders| orders.sum { |o| o.item_total } }, - proc { |orders| orders.sum { |o| o.ship_total } }, - proc { |orders| orders.sum { |o| o.outstanding_balance } }, - proc { |orders| orders.sum { |o| o.total } } ] - - rules = [ { group_by: proc { |order| order.payment_state }, - sort_by: proc { |payment_state| payment_state } }, - { group_by: proc { |order| order.distributor }, - sort_by: proc { |distributor| distributor.name } } ] - - when "payment_totals" - table_items = orders - - header = ["Payment State", "Distributor", "Product Total (#{currency_symbol})", "Shipping Total (#{currency_symbol})", "Total (#{currency_symbol})", "EFT (#{currency_symbol})", "PayPal (#{currency_symbol})", "Outstanding Balance (#{currency_symbol})"] - - columns = [ proc { |orders| orders.first.payment_state }, - proc { |orders| orders.first.distributor.name }, - proc { |orders| orders.sum { |o| o.item_total } }, - proc { |orders| orders.sum { |o| o.ship_total } }, - proc { |orders| orders.sum { |o| o.total } }, - proc { |orders| orders.sum { |o| o.payments.select { |payment| payment.completed? && (payment.payment_method.name.to_s.include? "EFT") }.sum { |payment| payment.amount } } }, - proc { |orders| orders.sum { |o| o.payments.select { |payment| payment.completed? && (payment.payment_method.name.to_s.include? "PayPal") }.sum{ |payment| payment.amount } } }, - proc { |orders| orders.sum { |o| o.outstanding_balance } } ] - - rules = [ { group_by: proc { |order| order.payment_state }, - sort_by: proc { |payment_state| payment_state } }, - { group_by: proc { |order| order.distributor }, - sort_by: proc { |distributor| distributor.name } } ] - - else - table_items = payments - - header = ["Payment State", "Distributor", "Payment Type", "Total (#{currency_symbol})"] - - columns = [ proc { |payments| payments.first.order.payment_state }, - proc { |payments| payments.first.order.distributor.name }, - proc { |payments| payments.first.payment_method.name }, - proc { |payments| payments.sum { |payment| payment.amount } } ] - - rules = [ { group_by: proc { |payment| payment.order.payment_state }, - sort_by: proc { |payment_state| payment_state } }, - { group_by: proc { |payment| payment.order.distributor }, - sort_by: proc { |distributor| distributor.name } }, - { group_by: proc { |payment| payment.payment_method }, - sort_by: proc { |method| method.name } } ] - - end - - order_grouper = OpenFoodNetwork::OrderGrouper.new rules, columns - - @header = header - @table = order_grouper.table(table_items) + # -- Build Report with Order Grouper + @report = OpenFoodNetwork::PaymentsReport.new spree_current_user, params + order_grouper = OpenFoodNetwork::OrderGrouper.new @report.rules, @report.columns + @table = order_grouper.table(@report.table_items) csv_file_name = "payments_#{timestamp}.csv" - render_report(@header, @table, params[:csv], csv_file_name) - + render_report(@report.header, @table, params[:csv], csv_file_name) end def orders_and_fulfillment - # -- Prepare parameters - params[:q] ||= {} - - if params[:q][:completed_at_gt].blank? - params[:q][:completed_at_gt] = Time.zone.now.beginning_of_month - else - params[:q][:completed_at_gt] = Time.zone.parse(params[:q][:completed_at_gt]) rescue Time.zone.now.beginning_of_month - end - - if params[:q] && !params[:q][:completed_at_lt].blank? - params[:q][:completed_at_lt] = Time.zone.parse(params[:q][:completed_at_lt]) rescue "" - end - params[:q][:meta_sort] ||= "completed_at.desc" + # -- Prepare Date Params + prepare_date_params params + # -- Prepare Form Options permissions = OpenFoodNetwork::Permissions.new(spree_current_user) - - # -- Search - - @search = Spree::Order.complete.not_state(:canceled).search(params[:q]) - orders = permissions.visible_orders.merge(@search.result) - - @line_items = permissions.visible_line_items.merge(Spree::LineItem.where(order_id: orders)) - @line_items = @line_items.supplied_by_any(params[:supplier_id_in]) if params[:supplier_id_in].present? - - line_items_with_hidden_details = @line_items.where('"spree_line_items"."id" NOT IN (?)', permissions.editable_line_items) - @line_items.select{ |li| line_items_with_hidden_details.include? li }.each do |line_item| - # TODO We should really be hiding customer code here too, but until we - # have an actual association between order and customer, it's a bit tricky - line_item.order.bill_address.assign_attributes(firstname: "HIDDEN", lastname: "", phone: "", address1: "", address2: "", city: "", zipcode: "", state: nil) - line_item.order.ship_address.assign_attributes(firstname: "HIDDEN", lastname: "", phone: "", address1: "", address2: "", city: "", zipcode: "", state: nil) - line_item.order.assign_attributes(email: "HIDDEN") - end - # My distributors and any distributors distributing products I supply @distributors = permissions.visible_enterprises_for_order_reports.is_distributor - # My suppliers and any suppliers supplying products I distribute @suppliers = permissions.visible_enterprises_for_order_reports.is_primary_producer @@ -438,239 +200,25 @@ Spree::Admin::ReportsController.class_eval do @report_types = REPORT_TYPES[:orders_and_fulfillment] @report_type = params[:report_type] - # -- Format according to report type - case params[:report_type] - when "order_cycle_supplier_totals" - table_items = @line_items - @include_blank = 'All' + @include_blank = 'All' - header = ["Producer", "Product", "Variant", "Amount", "Total Units", "Curr. Cost per Unit", "Total Cost", "Status", "Incoming Transport"] - - columns = [ proc { |line_items| line_items.first.variant.product.supplier.name }, - proc { |line_items| line_items.first.variant.product.name }, - proc { |line_items| line_items.first.variant.full_name }, - proc { |line_items| line_items.sum { |li| li.quantity } }, - proc { |line_items| total_units(line_items) }, - proc { |line_items| line_items.first.price }, - proc { |line_items| line_items.sum { |li| li.amount } }, - proc { |line_items| "" }, - proc { |line_items| "incoming transport" } ] - - rules = [ { group_by: proc { |line_item| line_item.variant.product.supplier }, - sort_by: proc { |supplier| supplier.name } }, - { group_by: proc { |line_item| line_item.variant.product }, - sort_by: proc { |product| product.name } }, - { group_by: proc { |line_item| line_item.variant }, - sort_by: proc { |variant| variant.full_name } } ] - - when "order_cycle_supplier_totals_by_distributor" - table_items = @line_items - @include_blank = 'All' - - header = ["Producer", "Product", "Variant", "To Hub", "Amount", "Curr. Cost per Unit", "Total Cost", "Shipping Method"] - - columns = [ proc { |line_items| line_items.first.variant.product.supplier.name }, - proc { |line_items| line_items.first.variant.product.name }, - proc { |line_items| line_items.first.variant.full_name }, - proc { |line_items| line_items.first.order.distributor.name }, - proc { |line_items| line_items.sum { |li| li.quantity } }, - proc { |line_items| line_items.first.price }, - proc { |line_items| line_items.sum { |li| li.amount } }, - proc { |line_items| "shipping method" } ] - - rules = [ { group_by: proc { |line_item| line_item.variant.product.supplier }, - sort_by: proc { |supplier| supplier.name } }, - { group_by: proc { |line_item| line_item.variant.product }, - sort_by: proc { |product| product.name } }, - { group_by: proc { |line_item| line_item.variant }, - sort_by: proc { |variant| variant.full_name }, - summary_columns: [ proc { |line_items| "" }, - proc { |line_items| "" }, - proc { |line_items| "" }, - proc { |line_items| "TOTAL" }, - proc { |line_items| "" }, - proc { |line_items| "" }, - proc { |line_items| line_items.sum { |li| li.amount } }, - proc { |line_items| "" } ] }, - { group_by: proc { |line_item| line_item.order.distributor }, - sort_by: proc { |distributor| distributor.name } } ] - - when "order_cycle_distributor_totals_by_supplier" - table_items = @line_items - @include_blank = 'All' - - header = ["Hub", "Producer", "Product", "Variant", "Amount", "Curr. Cost per Unit", "Total Cost", "Total Shipping Cost", "Shipping Method"] - - columns = [ proc { |line_items| line_items.first.order.distributor.name }, - proc { |line_items| line_items.first.variant.product.supplier.name }, - proc { |line_items| line_items.first.variant.product.name }, - proc { |line_items| line_items.first.variant.full_name }, - proc { |line_items| line_items.sum { |li| li.quantity } }, - proc { |line_items| line_items.first.price }, - proc { |line_items| line_items.sum { |li| li.amount } }, - proc { |line_items| "" }, - proc { |line_items| "shipping method" } ] - - rules = [ { group_by: proc { |line_item| line_item.order.distributor }, - sort_by: proc { |distributor| distributor.name }, - summary_columns: [ proc { |line_items| "" }, - proc { |line_items| "TOTAL" }, - proc { |line_items| "" }, - proc { |line_items| "" }, - proc { |line_items| "" }, - proc { |line_items| "" }, - proc { |line_items| line_items.sum { |li| li.amount } }, - proc { |line_items| line_items.map { |li| li.order }.uniq.sum { |o| o.ship_total } }, - proc { |line_items| "" } ] }, - { group_by: proc { |line_item| line_item.variant.product.supplier }, - sort_by: proc { |supplier| supplier.name } }, - { group_by: proc { |line_item| line_item.variant.product }, - sort_by: proc { |product| product.name } }, - { group_by: proc { |line_item| line_item.variant }, - sort_by: proc { |variant| variant.full_name } } ] - - when "order_cycle_customer_totals" - table_items = @line_items - @include_blank = 'All' - - header = ["Hub", "Customer", "Email", "Phone", "Producer", "Product", "Variant", - "Amount", "Item (#{currency_symbol})", "Item + Fees (#{currency_symbol})", "Admin & Handling (#{currency_symbol})", "Ship (#{currency_symbol})", "Total (#{currency_symbol})", "Paid?", - "Shipping", "Delivery?", - "Ship Street", "Ship Street 2", "Ship City", "Ship Postcode", "Ship State", - "Comments", "SKU", - "Order Cycle", "Payment Method", "Customer Code", "Tags", - "Billing Street 1", "Billing Street 2", "Billing City", "Billing Postcode", "Billing State" - ] - - rsa = proc { |line_items| line_items.first.order.shipping_method.andand.require_ship_address } - - columns = [ - proc { |line_items| line_items.first.order.distributor.name }, - proc { |line_items| line_items.first.order.bill_address.firstname + " " + line_items.first.order.bill_address.lastname }, - proc { |line_items| line_items.first.order.email }, - proc { |line_items| line_items.first.order.bill_address.phone }, - proc { |line_items| line_items.first.variant.product.supplier.name }, - proc { |line_items| line_items.first.variant.product.name }, - proc { |line_items| line_items.first.variant.full_name }, - - proc { |line_items| line_items.sum { |li| li.quantity } }, - proc { |line_items| line_items.sum { |li| li.amount } }, - proc { |line_items| line_items.sum { |li| li.amount_with_adjustments } }, - proc { |line_items| "" }, - proc { |line_items| "" }, - proc { |line_items| "" }, - proc { |line_items| "" }, - - proc { |line_items| line_items.first.order.shipping_method.andand.name }, - proc { |line_items| rsa.call(line_items) ? 'Y' : 'N' }, - - proc { |line_items| line_items.first.order.ship_address.andand.address1 if rsa.call(line_items) }, - proc { |line_items| line_items.first.order.ship_address.andand.address2 if rsa.call(line_items) }, - proc { |line_items| line_items.first.order.ship_address.andand.city if rsa.call(line_items) }, - proc { |line_items| line_items.first.order.ship_address.andand.zipcode if rsa.call(line_items) }, - proc { |line_items| line_items.first.order.ship_address.andand.state if rsa.call(line_items) }, - - proc { |line_items| "" }, - proc { |line_items| line_items.first.variant.product.sku }, - - proc { |line_items| line_items.first.order.order_cycle.andand.name }, - proc { |line_items| line_items.first.order.payments.first.andand.payment_method.andand.name }, - proc { |line_items| line_items.first.order.user.andand.customer_of(line_items.first.order.distributor).andand.code }, - proc { |line_items| "" }, - - proc { |line_items| line_items.first.order.bill_address.andand.address1 }, - proc { |line_items| line_items.first.order.bill_address.andand.address2 }, - proc { |line_items| line_items.first.order.bill_address.andand.city }, - proc { |line_items| line_items.first.order.bill_address.andand.zipcode }, - proc { |line_items| line_items.first.order.bill_address.andand.state } ] - - rules = [ { group_by: proc { |line_item| line_item.order.distributor }, - sort_by: proc { |distributor| distributor.name } }, - { group_by: proc { |line_item| line_item.order }, - sort_by: proc { |order| order.bill_address.lastname + " " + order.bill_address.firstname }, - summary_columns: [ - proc { |line_items| line_items.first.order.distributor.name }, - proc { |line_items| line_items.first.order.bill_address.firstname + " " + line_items.first.order.bill_address.lastname }, - proc { |line_items| "" }, - proc { |line_items| "" }, - proc { |line_items| "" }, - proc { |line_items| "TOTAL" }, - proc { |line_items| "" }, - - proc { |line_items| "" }, - proc { |line_items| line_items.sum { |li| li.amount } }, - proc { |line_items| line_items.sum { |li| li.amount_with_adjustments } }, - proc { |line_items| line_items.map { |li| li.order }.uniq.sum { |o| o.admin_and_handling_total } }, - proc { |line_items| line_items.map { |li| li.order }.uniq.sum { |o| o.ship_total } }, - proc { |line_items| line_items.map { |li| li.order }.uniq.sum { |o| o.total } }, - proc { |line_items| line_items.all? { |li| li.order.paid? } ? "Yes" : "No" }, - - proc { |line_items| "" }, - proc { |line_items| "" }, - - proc { |line_items| "" }, - proc { |line_items| "" }, - proc { |line_items| "" }, - proc { |line_items| "" }, - proc { |line_items| "" }, - - proc { |line_items| line_items.first.order.special_instructions } , - proc { |line_items| "" }, - - proc { |line_items| line_items.first.order.order_cycle.andand.name }, - proc { |line_items| line_items.first.order.payments.first.andand.payment_method.andand.name }, - proc { |line_items| "" }, - proc { |line_items| "" }, - - proc { |line_items| "" }, - proc { |line_items| "" }, - proc { |line_items| "" }, - proc { |line_items| "" }, - proc { |line_items| "" } - ] }, - - { group_by: proc { |line_item| line_item.variant.product }, - sort_by: proc { |product| product.name } }, - { group_by: proc { |line_item| line_item.variant }, - sort_by: proc { |variant| variant.full_name } } ] - - else - table_items = @line_items - @include_blank = 'All' - - header = ["Producer", "Product", "Variant", "Amount", "Curr. Cost per Unit", "Total Cost", "Status", "Incoming Transport"] - - columns = [ proc { |line_items| line_items.first.variant.product.supplier.name }, - proc { |line_items| line_items.first.variant.product.name }, - proc { |line_items| line_items.first.variant.full_name }, - proc { |line_items| line_items.sum { |li| li.quantity } }, - proc { |line_items| line_items.first.price }, - proc { |line_items| line_items.sum { |li| li.quantity * li.price } }, - proc { |line_items| "" }, - proc { |line_items| "incoming transport" } ] - - rules = [ { group_by: proc { |line_item| line_item.variant.product.supplier }, - sort_by: proc { |supplier| supplier.name } }, - { group_by: proc { |line_item| line_item.variant.product }, - sort_by: proc { |product| product.name } }, - { group_by: proc { |line_item| line_item.variant }, - sort_by: proc { |variant| variant.full_name } } ] - - end - - order_grouper = OpenFoodNetwork::OrderGrouper.new rules, columns - - @header = header - @table = order_grouper.table(table_items) + # -- Build Report with Order Grouper + @report = OpenFoodNetwork::OrdersAndFulfillmentsReport.new spree_current_user, params + order_grouper = OpenFoodNetwork::OrderGrouper.new @report.rules, @report.columns + @table = order_grouper.table(@report.table_items) csv_file_name = "#{params[:report_type]}_#{timestamp}.csv" - render_report(@header, @table, params[:csv], csv_file_name) + render_report(@report.header, @table, params[:csv], csv_file_name) end def products_and_inventory @report_types = REPORT_TYPES[:products_and_inventory] - @report = OpenFoodNetwork::ProductsAndInventoryReport.new spree_current_user, params + if params[:report_type] != 'lettuce_share' + @report = OpenFoodNetwork::ProductsAndInventoryReport.new spree_current_user, params + else + @report = OpenFoodNetwork::LettuceShareReport.new spree_current_user, params + end render_report(@report.header, @report.table, params[:csv], "products_and_inventory_#{timestamp}.csv") end @@ -709,6 +257,20 @@ Spree::Admin::ReportsController.class_eval do private + def prepare_date_params(params) + # -- Prepare parameters + params[:q] ||= {} + if params[:q][:completed_at_gt].blank? + params[:q][:completed_at_gt] = Time.zone.now.beginning_of_month + else + params[:q][:completed_at_gt] = Time.zone.parse(params[:q][:completed_at_gt]) rescue Time.zone.now.beginning_of_month + end + if params[:q] && !params[:q][:completed_at_lt].blank? + params[:q][:completed_at_lt] = Time.zone.parse(params[:q][:completed_at_lt]) rescue "" + end + params[:q][:meta_sort] ||= "completed_at.desc" + end + def load_data # Load distributors either owned by the user or selling their enterprises products. my_distributors = Enterprise.is_distributor.managed_by(spree_current_user) @@ -732,23 +294,14 @@ Spree::Admin::ReportsController.class_eval do :sales_total => { :name => "Sales Total", :description => "Sales Total For All Orders" }, :users_and_enterprises => { :name => "Users & Enterprises", :description => "Enterprise Ownership & Status" }, :order_cycle_management => {:name => "Order Cycle Management", :description => ''}, + :packing => {:name => "Packing Reports", :description => ''}, :sales_tax => { :name => "Sales Tax", :description => "Sales Tax For Orders" }, :xero_invoices => { :name => "Xero Invoices", :description => 'Invoices for import into Xero' } - } # Return only reports the user is authorized to view. reports.select { |action| can? action, :report } end - def total_units(line_items) - return " " if line_items.map{ |li| li.variant.unit_value.nil? }.any? - total_units = line_items.sum do |li| - scale_factor = ( li.product.variant_unit == 'weight' ? 1000 : 1 ) - li.quantity * li.variant.unit_value / scale_factor - end - total_units.round(3) - end - def timestamp Time.now.strftime("%Y%m%d") end diff --git a/app/controllers/spree/admin/variants_controller_decorator.rb b/app/controllers/spree/admin/variants_controller_decorator.rb index 2308d2a923..814cfb12f8 100644 --- a/app/controllers/spree/admin/variants_controller_decorator.rb +++ b/app/controllers/spree/admin/variants_controller_decorator.rb @@ -14,9 +14,10 @@ Spree::Admin::VariantsController.class_eval do if params[:distributor_id].present? distributor = Enterprise.find params[:distributor_id] @variants = @variants.in_distributor(distributor) + scoper = OpenFoodNetwork::ScopeVariantToHub.new(distributor) # Perform scoping after all filtering is done. # Filtering could be a problem on scoped variants. - @variants.each { |v| v.scope_to_hub(distributor) } + @variants.each { |v| scoper.scope(v) } end end diff --git a/app/controllers/spree/api/line_items_controller_decorator.rb b/app/controllers/spree/api/line_items_controller_decorator.rb index 35fca864f4..5493c74043 100644 --- a/app/controllers/spree/api/line_items_controller_decorator.rb +++ b/app/controllers/spree/api/line_items_controller_decorator.rb @@ -1,5 +1,8 @@ Spree::Api::LineItemsController.class_eval do - after_filter :apply_enterprise_fees, :only => :update + after_filter :apply_enterprise_fees, only: :update + + + private def apply_enterprise_fees authorize! :read, order diff --git a/app/helpers/markdown_helper.rb b/app/helpers/markdown_helper.rb new file mode 100644 index 0000000000..bd45e31124 --- /dev/null +++ b/app/helpers/markdown_helper.rb @@ -0,0 +1,6 @@ +module MarkdownHelper + def render_markdown(markdown) + md ||= Redcarpet::Markdown.new(Redcarpet::Render::HTML, no_intra_emphasis: true, tables: true, autolink: true, superscript: true) + md.render markdown + end +end diff --git a/app/helpers/spree/admin/base_helper_decorator.rb b/app/helpers/spree/admin/base_helper_decorator.rb index e278626c5c..82bf794073 100644 --- a/app/helpers/spree/admin/base_helper_decorator.rb +++ b/app/helpers/spree/admin/base_helper_decorator.rb @@ -15,6 +15,16 @@ module Spree link_to_with_icon('icon-trash', name, url, :class => "remove_fields #{options[:class]}", :data => {:action => 'remove'}, :title => t(:remove)) + f.hidden_field(:_destroy) end + + + def preference_field_tag_with_files(name, value, options) + if options[:type] == :file + file_field_tag name, preference_field_options(options) + else + preference_field_tag_without_files name, value, options + end + end + alias_method_chain :preference_field_tag, :files end end end diff --git a/app/helpers/spree/reports_helper.rb b/app/helpers/spree/reports_helper.rb index f4ecdcea6f..bbc184d800 100644 --- a/app/helpers/spree/reports_helper.rb +++ b/app/helpers/spree/reports_helper.rb @@ -18,6 +18,11 @@ module Spree orders.map { |o| o.shipping_method.andand.name }.uniq end + def xero_report_types + [['Summary', 'summary'], + ['Detailed', 'detailed']] + end + def currency_symbol Spree::Money.currency_symbol end diff --git a/app/jobs/finalize_account_invoices.rb b/app/jobs/finalize_account_invoices.rb new file mode 100644 index 0000000000..fcf872e1c3 --- /dev/null +++ b/app/jobs/finalize_account_invoices.rb @@ -0,0 +1,96 @@ +class FinalizeAccountInvoices + attr_reader :year, :month, :start_date, :end_date + + def initialize(year = nil, month = nil) + ref_point = Time.now - 1.month + @year = year || ref_point.year + @month = month || ref_point.month + @start_date = Time.new(@year, @month) + @end_date = Time.new(@year, @month) + 1.month + end + + def before(job) + UpdateBillablePeriods.new(year, month).perform + UpdateAccountInvoices.new(year, month).perform + end + + def perform + return unless settings_are_valid? + + + invoice_orders = AccountInvoice.where(year: year, month: month).map(&:order) + invoice_orders.select{ |order| order.present? && order.completed_at.nil? }.each{ |order| finalize(order) } + end + + def finalize(invoice_order) + # TODO: When we implement per-customer and/or per-user preferences around shipping and payment methods + # we can update these to read from those preferences + invoice_order.payments.create(payment_method_id: Spree::Config.default_accounts_payment_method_id, amount: invoice_order.total) + invoice_order.update_attribute(:shipping_method_id, Spree::Config.default_accounts_shipping_method_id) + while invoice_order.state != "complete" + if invoice_order.errors.any? + Bugsnag.notify(RuntimeError.new("FinalizeInvoiceError"), { + job: "FinalizeAccountInvoices", + error: "Cannot finalize invoice due to errors", + data: { + errors: invoice_order.errors.full_messages + } + }) + break + else + invoice_order.next + end + end + end + + private + + def settings_are_valid? + unless end_date <= Time.now + Bugsnag.notify(RuntimeError.new("InvalidJobSettings"), { + job: "FinalizeAccountInvoices", + error: "end_date is in the future", + data: { + end_date: end_date.localtime.strftime("%F %T"), + now: Time.now.strftime("%F %T") + } + }) + return false + end + + unless @accounts_distributor = Enterprise.find_by_id(Spree::Config.accounts_distributor_id) + Bugsnag.notify(RuntimeError.new("InvalidJobSettings"), { + job: "FinalizeAccountInvoices", + error: "accounts_distributor_id is invalid", + data: { + accounts_distributor_id: Spree::Config.accounts_distributor_id + } + }) + return false + end + + unless @accounts_distributor.payment_methods.find_by_id(Spree::Config.default_accounts_payment_method_id) + Bugsnag.notify(RuntimeError.new("InvalidJobSettings"), { + job: "FinalizeAccountInvoices", + error: "default_accounts_payment_method_id is invalid", + data: { + default_accounts_payment_method_id: Spree::Config.default_accounts_payment_method_id + } + }) + return false + end + + unless @accounts_distributor.shipping_methods.find_by_id(Spree::Config.default_accounts_shipping_method_id) + Bugsnag.notify(RuntimeError.new("InvalidJobSettings"), { + job: "FinalizeAccountInvoices", + error: "default_accounts_shipping_method_id is invalid", + data: { + default_accounts_shipping_method_id: Spree::Config.default_accounts_shipping_method_id + } + }) + return false + end + + true + end +end diff --git a/app/jobs/update_account_invoices.rb b/app/jobs/update_account_invoices.rb new file mode 100644 index 0000000000..6f43a10e31 --- /dev/null +++ b/app/jobs/update_account_invoices.rb @@ -0,0 +1,104 @@ +class UpdateAccountInvoices + attr_reader :year, :month, :start_date, :end_date + + def initialize(year = nil, month = nil) + ref_point = Time.now - 1.day + @year = year || ref_point.year + @month = month || ref_point.month + @start_date = Time.new(@year, @month) + @end_date = Time.new(@year, @month) + 1.month + @end_date = Time.now.beginning_of_day if start_date == Time.now.beginning_of_month + end + + def before(job) + UpdateBillablePeriods.new(year, month).perform + end + + def perform + return unless settings_are_valid? + + account_invoices = AccountInvoice.where(year: year, month: month) + account_invoices.each { |account_invoice| update(account_invoice) } + end + + def update(account_invoice) + current_adjustments = [] + unless account_invoice.order + account_invoice.order = account_invoice.user.orders.new(distributor_id: Spree::Config[:accounts_distributor_id]) + end + + if account_invoice.order.complete? + Bugsnag.notify(RuntimeError.new("InvoiceAlreadyFinalized"), { + invoice_order: account_invoice.order.as_json + }) + else + billable_periods = account_invoice.billable_periods.order(:enterprise_id, :begins_at).reject{ |bp| bp.turnover == 0 } + + if billable_periods.any? + address = billable_periods.first.enterprise.address + account_invoice.order.update_attributes(bill_address: address, ship_address: address) + end + + billable_periods.each do |billable_period| + current_adjustments << billable_period.ensure_correct_adjustment_for(account_invoice.order) + end + + account_invoice.save if current_adjustments.any? + + clean_up(account_invoice.order, current_adjustments) + end + end + + def clean_up(invoice_order, current_adjustments) + # Snag and then delete any obsolete adjustments + obsolete_adjustments = invoice_order.adjustments.where('source_type = (?) AND id NOT IN (?)', "BillablePeriod", current_adjustments) + + if obsolete_adjustments.any? + Bugsnag.notify(RuntimeError.new("Obsolete Adjustments"), { + current: current_adjustments.map(&:as_json), + obsolete: obsolete_adjustments.map(&:as_json) + }) + + obsolete_adjustments.destroy_all + end + + if current_adjustments.empty? + if invoice_order.persisted? + Bugsnag.notify(RuntimeError.new("Empty Persisted Invoice"), { + invoice_order: invoice_order.as_json + }) + else + invoice_order.destroy + end + end + end + + private + + def settings_are_valid? + unless end_date <= Time.now + Bugsnag.notify(RuntimeError.new("InvalidJobSettings"), { + job: "UpdateAccountInvoices", + error: "end_date is in the future", + data: { + end_date: end_date.localtime.strftime("%F %T"), + now: Time.now.strftime("%F %T") + } + }) + return false + end + + unless Enterprise.find_by_id(Spree::Config.accounts_distributor_id) + Bugsnag.notify(RuntimeError.new("InvalidJobSettings"), { + job: "UpdateAccountInvoices", + error: "accounts_distributor_id is invalid", + data: { + accounts_distributor_id: Spree::Config.accounts_distributor_id + } + }) + return false + end + + true + end +end diff --git a/app/jobs/update_billable_periods.rb b/app/jobs/update_billable_periods.rb new file mode 100644 index 0000000000..35daf48dbc --- /dev/null +++ b/app/jobs/update_billable_periods.rb @@ -0,0 +1,128 @@ +class UpdateBillablePeriods + attr_reader :year, :month, :start_date, :end_date + + def initialize(year = nil, month = nil) + ref_point = Time.now - 1.day + @year = year || ref_point.year + @month = month || ref_point.month + @start_date = Time.new(@year, @month) + @end_date = Time.new(@year, @month) + 1.month + @end_date = Time.now.beginning_of_day if start_date == Time.now.beginning_of_month + end + + def perform + return unless settings_are_valid? + + job_start_time = Time.now + + enterprises = Enterprise.where('created_at < (?)', end_date).select([:id, :name, :owner_id, :sells, :shop_trial_start_date, :created_at]) + + # Cycle through enterprises + enterprises.each do |enterprise| + start_for_enterprise = [start_date, enterprise.created_at].max + end_for_enterprise = [end_date].min # [end_date, enterprise.deleted_at].min + + # Cycle through previous versions of this enterprise + versions = enterprise.versions.where('created_at >= (?) AND created_at < (?)', start_for_enterprise, end_for_enterprise).order(:created_at) + + trial_start = enterprise.shop_trial_start_date + trial_expiry = enterprise.shop_trial_expiry + + versions.each do |version| + begins_at = version.previous.andand.created_at || start_for_enterprise + ends_at = version.created_at + + split_for_trial(version.reify, begins_at, ends_at, trial_start, trial_expiry) + end + + # Update / create billable_period for current start + begins_at = versions.last.andand.created_at || start_for_enterprise + ends_at = end_date + + split_for_trial(enterprise, begins_at, ends_at, trial_start, trial_expiry) + + clean_up_untouched_billable_periods_for(enterprise, job_start_time) + end + end + + def split_for_trial(enterprise, begins_at, ends_at, trial_start, trial_expiry) + trial_start = trial_expiry = begins_at-1.day if trial_start.nil? || trial_expiry.nil? + + # If the trial begins after ends_at, create a bill for the entire period + # Otherwise, create a normal billable_period from the begins_at until the start of the trial + if trial_start > begins_at + update_billable_period(enterprise, begins_at, [ends_at, trial_start].min, false) + end + + # If all or some of the trial occurs between begins_at and ends_at + # Create a trial billable_period from the from begins_at or trial_start, whichever occurs last, until ends_at, or trial_expiry whichever occurs first + if trial_expiry >= begins_at && trial_start <= ends_at + update_billable_period(enterprise, [trial_start, begins_at].max, [ends_at, trial_expiry].min, true) + end + + # If the trial finishes before begins_at, or trial has not been set, create a bill for the entire period + # Otherwise, create a normal billable_period from the end of the trial until ends_at + if trial_expiry < ends_at + update_billable_period(enterprise, [trial_expiry, begins_at].max, ends_at, false) + end + end + + def update_billable_period(enterprise, begins_at, ends_at, trial) + owner_id = enterprise.owner_id + sells = enterprise.sells + orders = Spree::Order.where('distributor_id = (?) AND completed_at >= (?) AND completed_at < (?)', enterprise.id, begins_at, ends_at) + account_invoice = AccountInvoice.find_or_create_by_user_id_and_year_and_month(owner_id, begins_at.year, begins_at.month) + + billable_period = BillablePeriod.where(account_invoice_id: account_invoice.id, begins_at: begins_at, enterprise_id: enterprise.id).first + + unless account_invoice.order.andand.complete? + billable_period ||= BillablePeriod.new(account_invoice_id: account_invoice.id, begins_at: begins_at, enterprise_id: enterprise.id) + billable_period.update_attributes({ + ends_at: ends_at, + sells: sells, + trial: trial, + owner_id: owner_id, + turnover: orders.sum(&:total) + }) + end + + billable_period.touch + end + + def clean_up_untouched_billable_periods_for(enterprise, job_start_time) + # Snag and then delete any BillablePeriods which overlap + obsolete_billable_periods = enterprise.billable_periods.where('ends_at > (?) AND begins_at < (?) AND updated_at < (?)', start_date, end_date, job_start_time) + + if obsolete_billable_periods.any? + current_billable_periods = enterprise.billable_periods.where('ends_at >= (?) AND begins_at <= (?) AND updated_at > (?)', start_date, end_date, job_start_time) + + Delayed::Worker.logger.info "#{enterprise.name} #{start_date.strftime("%F %T")} #{job_start_time.strftime("%F %T")}" + Delayed::Worker.logger.info "#{obsolete_billable_periods.first.updated_at.strftime("%F %T")}" + + Bugsnag.notify(RuntimeError.new("Obsolete BillablePeriods"), { + current: current_billable_periods.map(&:as_json), + obsolete: obsolete_billable_periods.map(&:as_json) + }) + end + + obsolete_billable_periods.each(&:delete) + end + + private + + def settings_are_valid? + unless end_date <= Time.now + Bugsnag.notify(RuntimeError.new("InvalidJobSettings"), { + job: "UpdateBillablePeriods", + error: "end_date is in the future", + data: { + end_date: end_date.localtime.strftime("%F %T"), + now: Time.now.strftime("%F %T") + } + }) + return false + end + + true + end +end diff --git a/app/models/account_invoice.rb b/app/models/account_invoice.rb new file mode 100644 index 0000000000..6d381565bb --- /dev/null +++ b/app/models/account_invoice.rb @@ -0,0 +1,6 @@ +class AccountInvoice < ActiveRecord::Base + belongs_to :user, class_name: "Spree::User" + belongs_to :order, class_name: "Spree::Order" + attr_accessible :user_id, :order_id, :issued_at, :month, :year + has_many :billable_periods +end diff --git a/app/models/billable_period.rb b/app/models/billable_period.rb new file mode 100644 index 0000000000..8ea3829440 --- /dev/null +++ b/app/models/billable_period.rb @@ -0,0 +1,79 @@ +class BillablePeriod < ActiveRecord::Base + belongs_to :enterprise + belongs_to :owner, class_name: 'Spree::User' + belongs_to :account_invoice + has_one :adjustment, :as => :source, class_name: "Spree::Adjustment" #, :dependent => :destroy + + default_scope where(deleted_at: nil) + + def display_turnover + Spree::Money.new(turnover, {currency: Spree::Config[:currency]}) + end + + def display_bill + Spree::Money.new(bill, {currency: Spree::Config[:currency]}) + end + + def bill + # Will make this more sophisicated in the future in that it will use global config variables to calculate + return 0 if trial? + if ['own', 'any'].include? sells + bill = (turnover * 0.02).round(2) + bill > 50 ? 50 : bill + else + 0 + end + end + + def label + enterprise_version = enterprise.version_at(begins_at) + category = enterprise_version.category.to_s.titleize + category += (trial ? " Trial" : "") + + "#{enterprise_version.name} (#{category})" + end + + def adjustment_label + begins = begins_at.localtime.strftime("%d/%m/%y") + ends = ends_at.localtime.strftime("%d/%m/%y") + + "#{label} [#{begins} - #{ends}]" + end + + def delete + self.update_column(:deleted_at, Time.now) + end + + def ensure_correct_adjustment_for(invoice) + if adjustment + # adjustment.originator = enterprise.package + adjustment.update_attributes( label: adjustment_label, amount: bill ) + else + self.adjustment = invoice.adjustments.new( adjustment_attrs, :without_protection => true ) + end + + if Spree::Config.account_bill_inc_tax + adjustment.set_included_tax! Spree::Config.account_bill_tax_rate + else + adjustment.set_included_tax! 0 + end + + adjustment + end + + private + + def adjustment_attrs + # We should ultimately have an EnterprisePackage model, which holds all info about shop type, producer, trials, etc. + # It should also implement a calculator that we can use here by specifying the package as the originator of the + # adjustment, meaning that adjustments are created and updated using Spree's existing architecture. + + { label: adjustment_label, + amount: bill, + source: self, + originator: nil, # enterprise.package + mandatory: true, + locked: false + } + end +end diff --git a/app/models/content_configuration.rb b/app/models/content_configuration.rb new file mode 100644 index 0000000000..2357f8dfa3 --- /dev/null +++ b/app/models/content_configuration.rb @@ -0,0 +1,54 @@ +require 'open_food_network/paperclippable' + +class ContentConfiguration < Spree::Preferences::FileConfiguration + include OpenFoodNetwork::Paperclippable + + # Header + preference :logo, :file + preference :logo_mobile, :file + preference :logo_mobile_svg, :file + has_attached_file :logo + has_attached_file :logo_mobile + has_attached_file :logo_mobile_svg + + # Home page + preference :home_hero, :file + preference :home_show_stats, :boolean, default: true + has_attached_file :home_hero + + # Producer sign-up page + preference :producer_signup_pricing_table_html, :text, default: "(TODO: Pricing table)" + preference :producer_signup_case_studies_html, :text, default: "(TODO: Case studies)" + preference :producer_signup_detail_html, :text, default: "(TODO: Detail)" + + # Hubs sign-up page + preference :hub_signup_pricing_table_html, :text, default: "(TODO: Pricing table)" + preference :hub_signup_case_studies_html, :text, default: "(TODO: Case studies)" + preference :hub_signup_detail_html, :text, default: "(TODO: Detail)" + + # Groups sign-up page + preference :group_signup_pricing_table_html, :text, default: "(TODO: Pricing table)" + preference :group_signup_case_studies_html, :text, default: "(TODO: Case studies)" + preference :group_signup_detail_html, :text, default: "(TODO: Detail)" + + # Footer + preference :footer_logo, :file + has_attached_file :footer_logo + preference :footer_facebook_url, :string, default: "https://www.facebook.com/OpenFoodNet" + preference :footer_twitter_url, :string, default: "https://twitter.com/OpenFoodNet" + preference :footer_instagram_url, :string, default: "" + preference :footer_linkedin_url, :string, default: "http://www.linkedin.com/groups/Open-Food-Foundation-4743336" + preference :footer_googleplus_url, :string, default: "" + preference :footer_pinterest_url, :string, default: "" + preference :footer_email, :string, default: "hello@openfoodnetwork.org" + preference :footer_links_md, :text, default: <<-EOS +[Newsletter sign-up](/) + +[News](/) + +[Calendar](/) +EOS + + preference :footer_about_url, :string, default: "http://www.openfoodnetwork.org/ofn-local/open-food-network-australia/" + preference :footer_tos_url, :string, default: "/Terms-of-service.pdf" +end diff --git a/app/models/enterprise.rb b/app/models/enterprise.rb index 4adfb05309..a0b8e91c40 100644 --- a/app/models/enterprise.rb +++ b/app/models/enterprise.rb @@ -10,11 +10,14 @@ class Enterprise < ActiveRecord::Base devise :confirmable, reconfirmable: true, confirmation_keys: [ :id, :email ] handle_asynchronously :send_confirmation_instructions handle_asynchronously :send_on_create_confirmation_instructions + has_paper_trail only: [:owner_id, :sells], on: [:update] self.inheritance_column = nil acts_as_gmappable :process_geocoding => false + has_many :relationships_as_parent, class_name: 'EnterpriseRelationship', foreign_key: 'parent_id', dependent: :destroy + has_many :relationships_as_child, class_name: 'EnterpriseRelationship', foreign_key: 'child_id', dependent: :destroy has_and_belongs_to_many :groups, class_name: 'EnterpriseGroup' has_many :producer_properties, foreign_key: 'producer_id' has_many :properties, through: :producer_properties @@ -31,6 +34,7 @@ class Enterprise < ActiveRecord::Base has_many :distributor_shipping_methods, foreign_key: :distributor_id has_many :shipping_methods, through: :distributor_shipping_methods has_many :customers + has_many :billable_periods delegate :latitude, :longitude, :city, :state_name, :to => :address @@ -56,6 +60,7 @@ class Enterprise < ActiveRecord::Base validates :name, presence: true + validate :name_is_unique validates :sells, presence: true, inclusion: {in: SELLS} validates :address, presence: true, associated: true validates :email, presence: true @@ -325,6 +330,10 @@ class Enterprise < ActiveRecord::Base !confirmed? || pending_reconfirmation? end + def shop_trial_expiry + shop_trial_start_date.andand + Enterprise::SHOP_TRIAL_LENGTH.days + end + protected def devise_mailer @@ -333,6 +342,15 @@ class Enterprise < ActiveRecord::Base private + def name_is_unique + dups = Enterprise.where(name: name) + dups = dups.where('id != ?', id) unless new_record? + + if dups.any? + errors.add :name, "has already been taken. If this is your enterprise and you would like to claim ownership, please contact the current manager of this profile at #{dups.first.owner.email}." + end + end + def email_is_known? owner.enterprises.confirmed.map(&:email).include?(email) end diff --git a/app/models/enterprise_group.rb b/app/models/enterprise_group.rb index 986dc07d22..63eb63f0b3 100644 --- a/app/models/enterprise_group.rb +++ b/app/models/enterprise_group.rb @@ -1,6 +1,8 @@ require 'open_food_network/locking' +require 'open_food_network/permalink_generator' class EnterpriseGroup < ActiveRecord::Base + include PermalinkGenerator acts_as_list has_and_belongs_to_many :enterprises @@ -81,25 +83,10 @@ class EnterpriseGroup < ActiveRecord::Base private - def self.find_available_value(existing, requested) - return requested unless existing.include?(requested) - used_indices = existing.map do |p| - p.slice!(/^#{requested}/) - p.match(/^\d+$/).to_s.to_i - end - options = (1..used_indices.length + 1).to_a - used_indices - requested + options.first.to_s - end - - def find_available_permalink(requested) - existing = self.class.where(id: !id).where("permalink LIKE ?", "#{requested}%").pluck(:permalink) - self.class.find_available_value(existing, requested) - end - def sanitize_permalink if permalink.blank? || permalink_changed? requested = permalink.presence || permalink_was.presence || name.presence || 'group' - self.permalink = find_available_permalink(requested.parameterize) + self.permalink = create_unique_permalink(requested.parameterize) end end end diff --git a/app/models/enterprise_relationship.rb b/app/models/enterprise_relationship.rb index de06a0578e..8d279b2a20 100644 --- a/app/models/enterprise_relationship.rb +++ b/app/models/enterprise_relationship.rb @@ -32,6 +32,15 @@ class EnterpriseRelationship < ActiveRecord::Base relationships = EnterpriseRelationship.includes(:child, :parent) relatives = {} + Enterprise.is_primary_producer.pluck(:id).each do |enterprise_id| + relatives[enterprise_id] ||= { distributors: Set.new, producers: Set.new } + relatives[enterprise_id][:producers] << enterprise_id + end + Enterprise.is_distributor.pluck(:id).each do |enterprise_id| + relatives[enterprise_id] ||= { distributors: Set.new, producers: Set.new } + relatives[enterprise_id][:distributors] << enterprise_id + end + relationships.each do |r| relatives[r.parent_id] ||= {distributors: Set.new, producers: Set.new} relatives[r.child_id] ||= {distributors: Set.new, producers: Set.new} diff --git a/app/models/order_cycle.rb b/app/models/order_cycle.rb index 515f26621d..44b482c1c4 100644 --- a/app/models/order_cycle.rb +++ b/app/models/order_cycle.rb @@ -132,7 +132,12 @@ class OrderCycle < ActiveRecord::Base end def variants - self.exchanges.map(&:variants).flatten.uniq.reject(&:deleted?) + Spree::Variant. + joins(:exchanges). + merge(Exchange.in_order_cycle(self)). + not_deleted. + select('DISTINCT spree_variants.*'). + to_a # http://stackoverflow.com/q/15110166 end def distributed_variants diff --git a/app/models/spree/ability_decorator.rb b/app/models/spree/ability_decorator.rb index 56478f99c7..95b509ee33 100644 --- a/app/models/spree/ability_decorator.rb +++ b/app/models/spree/ability_decorator.rb @@ -73,9 +73,12 @@ class AbilityDecorator can [:admin, :index, :read, :create, :edit, :update_positions, :destroy], ProducerProperty can [:admin, :index, :create], Enterprise - can [:read, :edit, :update, :bulk_update, :set_sells, :resend_confirmation], Enterprise do |enterprise| + can [:read, :edit, :update, :bulk_update, :resend_confirmation], Enterprise do |enterprise| OpenFoodNetwork::Permissions.new(user).editable_enterprises.include? enterprise end + can [:welcome, :register], Enterprise do |enterprise| + enterprise.owner == user + end can [:manage_payment_methods, :manage_shipping_methods, :manage_enterprise_fees], Enterprise do |enterprise| user.enterprises.include? enterprise end @@ -87,6 +90,8 @@ class AbilityDecorator end can [:admin, :known_users], :search + + can [:admin, :show], :account end def add_product_management_abilities(user) @@ -120,7 +125,7 @@ class AbilityDecorator can [:admin, :index, :read, :create, :edit], Spree::Classification # Reports page - can [:admin, :index, :customers, :orders_and_distributors, :group_buys, :bulk_coop, :payments, :orders_and_fulfillment, :products_and_inventory, :order_cycle_management], :report + can [:admin, :index, :customers, :orders_and_distributors, :group_buys, :bulk_coop, :payments, :orders_and_fulfillment, :products_and_inventory, :order_cycle_management, :packing], :report end def add_order_cycle_management_abilities(user) @@ -183,7 +188,7 @@ class AbilityDecorator end # Reports page - can [:admin, :index, :customers, :group_buys, :bulk_coop, :sales_tax, :payments, :orders_and_distributors, :orders_and_fulfillment, :products_and_inventory, :order_cycle_management], :report + can [:admin, :index, :customers, :group_buys, :bulk_coop, :sales_tax, :payments, :orders_and_distributors, :orders_and_fulfillment, :products_and_inventory, :order_cycle_management, :xero_invoices], :report can [:admin, :index, :update], Customer, enterprise_id: Enterprise.managed_by(user).pluck(:id) end diff --git a/app/models/spree/adjustment_decorator.rb b/app/models/spree/adjustment_decorator.rb index 8447d24046..d27b5d760b 100644 --- a/app/models/spree/adjustment_decorator.rb +++ b/app/models/spree/adjustment_decorator.rb @@ -6,6 +6,7 @@ module Spree has_one :metadata, class_name: 'AdjustmentMetadata' scope :enterprise_fee, where(originator_type: 'EnterpriseFee') + scope :billable_period, where(source_type: 'BillablePeriod') scope :included_tax, where(originator_type: 'Spree::TaxRate', adjustable_type: 'Spree::LineItem') scope :with_tax, where('spree_adjustments.included_tax > 0') scope :without_tax, where('spree_adjustments.included_tax = 0') @@ -20,5 +21,9 @@ module Spree def set_absolute_included_tax!(tax) update_attributes! included_tax: tax.round(2) end + + def has_tax? + included_tax > 0 + end end end diff --git a/app/models/spree/app_configuration_decorator.rb b/app/models/spree/app_configuration_decorator.rb index 5ad4a9a1c5..2e1c838e95 100644 --- a/app/models/spree/app_configuration_decorator.rb +++ b/app/models/spree/app_configuration_decorator.rb @@ -7,4 +7,13 @@ Spree::AppConfiguration.class_eval do # Tax Preferences preference :products_require_tax_category, :boolean, default: false preference :shipping_tax_rate, :decimal, default: 0 + preference :account_bill_inc_tax, :boolean, default: false + preference :account_bill_tax_rate, :decimal, default: 0 + + # Accounts & Billing Preferences + preference :accounts_distributor_id, :integer, default: nil + preference :default_accounts_payment_method_id, :integer, default: nil + preference :default_accounts_shipping_method_id, :integer, default: nil + preference :auto_update_invoices, :boolean, default: false + preference :auto_finalize_invoices, :boolean, default: false end diff --git a/app/models/spree/inventory_unit_decorator.rb b/app/models/spree/inventory_unit_decorator.rb index 939a97996f..9868596b41 100644 --- a/app/models/spree/inventory_unit_decorator.rb +++ b/app/models/spree/inventory_unit_decorator.rb @@ -4,9 +4,10 @@ module Spree return [] unless order.completed? #increase inventory to meet initial requirements + scoper = OpenFoodNetwork::ScopeVariantToHub.new(order.distributor) order.line_items.each do |line_item| # Scope variant to hub so that stock levels may be subtracted from VariantOverride. - line_item.variant.scope_to_hub order.distributor + scoper.scope(line_item.variant) increase(order, line_item.variant, line_item.quantity) end diff --git a/app/models/spree/line_item_decorator.rb b/app/models/spree/line_item_decorator.rb index f64f197d56..689cabcc6c 100644 --- a/app/models/spree/line_item_decorator.rb +++ b/app/models/spree/line_item_decorator.rb @@ -1,6 +1,6 @@ Spree::LineItem.class_eval do - attr_accessible :max_quantity, :unit_value - attr_accessible :unit_value, :price, :as => :api + attr_accessible :max_quantity, :final_weight_volume + attr_accessible :final_weight_volume, :price, :as => :api # -- Scopes scope :managed_by, lambda { |user| @@ -33,6 +33,10 @@ Spree::LineItem.class_eval do where('spree_adjustments.id IS NULL') + def has_tax? + adjustments.included_tax.any? + end + def price_with_adjustments # EnterpriseFee#create_locked_adjustment applies adjustments on line items to their parent order, # so line_item.adjustments returns an empty array diff --git a/app/models/spree/order_decorator.rb b/app/models/spree/order_decorator.rb index 3c0f212945..4ca328a5b1 100644 --- a/app/models/spree/order_decorator.rb +++ b/app/models/spree/order_decorator.rb @@ -130,7 +130,11 @@ Spree::Order.class_eval do else current_item = Spree::LineItem.new(:quantity => quantity, max_quantity: max_quantity) current_item.variant = variant - current_item.unit_value = variant.unit_value + if variant.unit_value + current_item.final_weight_volume = variant.unit_value * quantity + else + current_item.final_weight_volume = 0 + end if currency current_item.currency = currency unless currency.nil? current_item.price = variant.price_in(currency).amount @@ -157,20 +161,22 @@ Spree::Order.class_eval do end def update_distribution_charge! - EnterpriseFee.clear_all_adjustments_on_order self + with_lock do + EnterpriseFee.clear_all_adjustments_on_order self - line_items.each do |line_item| - if provided_by_order_cycle? line_item - OpenFoodNetwork::EnterpriseFeeCalculator.new.create_line_item_adjustments_for line_item + line_items.each do |line_item| + if provided_by_order_cycle? line_item + OpenFoodNetwork::EnterpriseFeeCalculator.new.create_line_item_adjustments_for line_item - else - pd = product_distribution_for line_item - pd.create_adjustment_for line_item if pd + else + pd = product_distribution_for line_item + pd.create_adjustment_for line_item if pd + end end - end - if order_cycle - OpenFoodNetwork::EnterpriseFeeCalculator.new.create_order_adjustments_for self + if order_cycle + OpenFoodNetwork::EnterpriseFeeCalculator.new.create_order_adjustments_for self + end end end @@ -230,9 +236,16 @@ Spree::Order.class_eval do (adjustments + price_adjustments).sum &:included_tax end + def account_invoice? + distributor_id == Spree::Config.accounts_distributor_id + end + # Overrride of Spree method, that allows us to send separate confirmation emails to user and shop owners + # And separately, to skip sending confirmation email completely for user invoice orders def deliver_order_confirmation_email - Delayed::Job.enqueue ConfirmOrderJob.new(id) + unless account_invoice? + Delayed::Job.enqueue ConfirmOrderJob.new(id) + end end diff --git a/app/models/spree/order_populator_decorator.rb b/app/models/spree/order_populator_decorator.rb index 3759866236..cd80d17b97 100644 --- a/app/models/spree/order_populator_decorator.rb +++ b/app/models/spree/order_populator_decorator.rb @@ -9,7 +9,7 @@ Spree::OrderPopulator.class_eval do errors.add(:base, "That distributor or order cycle can't supply all the products in your cart. Please choose another.") end - if valid? + if valid? @order.with_lock do @order.empty! if overwrite @@ -33,7 +33,7 @@ Spree::OrderPopulator.class_eval do def attempt_cart_add(variant_id, quantity, max_quantity = nil) quantity = quantity.to_i variant = Spree::Variant.find(variant_id) - variant.scope_to_hub @distributor + OpenFoodNetwork::ScopeVariantToHub.new(@distributor).scope(variant) if quantity > 0 if check_stock_levels(variant, quantity) && check_order_cycle_provided_for(variant) && diff --git a/app/models/spree/preferences/file_configuration.rb b/app/models/spree/preferences/file_configuration.rb new file mode 100644 index 0000000000..fa1838a778 --- /dev/null +++ b/app/models/spree/preferences/file_configuration.rb @@ -0,0 +1,50 @@ +module Spree::Preferences + class FileConfiguration < Configuration + + def self.preference(name, type, *args) + if type == :file + super "#{name}_file_name", :string, *args + super "#{name}_content_type", :string, *args + super "#{name}_file_size", :integer, *args + super "#{name}_updated_at", :string, *args + + else + super name, type, *args + end + end + + + def get_preference(key) + if !has_preference?(key) && has_attachment?(key) + send key + else + super key + end + end + alias :[] :get_preference + + + def preference_type(name) + if has_attachment? name + :file + else + super name + end + end + + + # Spree's Configuration responds to preference methods via method_missing, but doesn't + # override respond_to?, which consequently reports those methods as unavailable. Paperclip + # errors if respond_to? isn't correct, so we override it here. + def respond_to?(method, include_all=false) + name = method.to_s.gsub('=', '') + super(self.class.preference_getter_method(name), include_all) || super(method, include_all) + end + + + def has_attachment?(name) + self.class.respond_to?(:attachment_definitions) && + self.class.attachment_definitions.keys.include?(name.to_sym) + end + end +end diff --git a/app/models/spree/product_decorator.rb b/app/models/spree/product_decorator.rb index c789d81963..53f960ba63 100644 --- a/app/models/spree/product_decorator.rb +++ b/app/models/spree/product_decorator.rb @@ -1,8 +1,7 @@ -require 'open_food_network/scope_product_to_hub' +require 'open_food_network/permalink_generator' Spree::Product.class_eval do - include OpenFoodNetwork::ProductScopableToHub - + include PermalinkGenerator # We have an after_destroy callback on Spree::ProductOptionType. However, if we # don't specify dependent => destroy on this association, it is not called. See: # https://github.com/rails/rails/issues/7618 @@ -23,6 +22,8 @@ Spree::Product.class_eval do attr_accessible :variant_unit, :variant_unit_scale, :variant_unit_name, :unit_value attr_accessible :inherits_properties, :sku + before_validation :sanitize_permalink + # validates_presence_of :variants, unless: :new_record?, message: "Product must have at least one variant" validates_presence_of :supplier validates :primary_taxon, presence: { message: "^Product Category can't be blank" } @@ -243,4 +244,11 @@ Spree::Product.class_eval do raise end end + + def sanitize_permalink + if permalink.blank? || permalink_changed? + requested = permalink.presence || permalink_was.presence || name.presence || 'product' + self.permalink = create_unique_permalink(requested.parameterize) + end + end end diff --git a/app/models/spree/shipment_decorator.rb b/app/models/spree/shipment_decorator.rb index 460fae4985..f544d95517 100644 --- a/app/models/spree/shipment_decorator.rb +++ b/app/models/spree/shipment_decorator.rb @@ -11,5 +11,14 @@ module Spree end alias_method_chain :ensure_correct_adjustment, :included_tax + + private + + # NOTE: This is an override of spree's method, needed to allow orders + # without line items (ie. user invoices) to not have inventory units + def require_inventory + return false unless Spree::Config[:track_inventory_levels] && line_items.count > 0 # This line altered + order.completed? && !order.canceled? + end end end diff --git a/app/models/spree/user_decorator.rb b/app/models/spree/user_decorator.rb index 13ab56c129..b724a41d1b 100644 --- a/app/models/spree/user_decorator.rb +++ b/app/models/spree/user_decorator.rb @@ -7,6 +7,8 @@ Spree.user_class.class_eval do has_many :enterprises, through: :enterprise_roles has_many :owned_enterprises, class_name: 'Enterprise', foreign_key: :owner_id, inverse_of: :owner has_many :owned_groups, class_name: 'EnterpriseGroup', foreign_key: :owner_id, inverse_of: :owner + has_many :account_invoices + has_many :billable_periods, foreign_key: :owner_id, inverse_of: :owner has_one :cart has_many :customers @@ -17,7 +19,6 @@ Spree.user_class.class_eval do validate :limit_owned_enterprises - def known_users if admin? Spree::User.scoped @@ -48,7 +49,6 @@ Spree.user_class.class_eval do owned_enterprises(:reload).size < enterprise_limit end - private def limit_owned_enterprises diff --git a/app/models/spree/variant_decorator.rb b/app/models/spree/variant_decorator.rb index 72a7fb4dec..5ac89c1430 100644 --- a/app/models/spree/variant_decorator.rb +++ b/app/models/spree/variant_decorator.rb @@ -1,10 +1,7 @@ -require 'open_food_network/scope_variant_to_hub' require 'open_food_network/enterprise_fee_calculator' require 'open_food_network/option_value_namer' Spree::Variant.class_eval do - include OpenFoodNetwork::VariantScopableToHub - has_many :exchange_variants, dependent: :destroy has_many :exchanges, through: :exchange_variants has_many :variant_overrides @@ -46,6 +43,13 @@ Spree::Variant.class_eval do } + def self.indexed + Hash[ + scoped.map { |v| [v.id, v] } + ] + end + + def price_with_fees(distributor, order_cycle) price + fees_for(distributor, order_cycle) end @@ -96,6 +100,14 @@ Spree::Variant.class_eval do display_as end + def product_and_variant_name + name = product.name + + name += " - #{name_to_display}" if name_to_display != product.name + name += " (#{options_text})" if options_text + + name + end def update_units delete_unit_option_values diff --git a/app/models/variant_override.rb b/app/models/variant_override.rb index 51bb1468f7..55afd99321 100644 --- a/app/models/variant_override.rb +++ b/app/models/variant_override.rb @@ -8,6 +8,12 @@ class VariantOverride < ActiveRecord::Base where(hub_id: hubs) } + def self.indexed(hub) + Hash[ + for_hubs(hub).map { |vo| [vo.variant, vo] } + ] + end + def self.price_for(hub, variant) self.for(hub, variant).andand.price end @@ -25,12 +31,21 @@ class VariantOverride < ActiveRecord::Base if vo.nil? Bugsnag.notify RuntimeError.new "Attempting to decrement stock level for a variant without a VariantOverride." - - elsif vo.count_on_hand.blank? - Bugsnag.notify RuntimeError.new "Attempting to decrement stock level on a VariantOverride without a count_on_hand specified." - else - vo.decrement! :count_on_hand, quantity + vo.decrement_stock! quantity + end + end + + + def stock_overridden? + count_on_hand.present? + end + + def decrement_stock!(quantity) + if stock_overridden? + decrement! :count_on_hand, quantity + else + Bugsnag.notify RuntimeError.new "Attempting to decrement stock level on a VariantOverride without a count_on_hand specified." end end diff --git a/app/overrides/spree/admin/shared/_configuration_menu/add_accounts_and_billing.html.haml.deface b/app/overrides/spree/admin/shared/_configuration_menu/add_accounts_and_billing.html.haml.deface new file mode 100644 index 0000000000..14f4925206 --- /dev/null +++ b/app/overrides/spree/admin/shared/_configuration_menu/add_accounts_and_billing.html.haml.deface @@ -0,0 +1,4 @@ +// insert_bottom "[data-hook='admin_configurations_sidebar_menu']" + +%li + = link_to 'Accounts & Billing', main_app.edit_admin_accounts_and_billing_settings_path diff --git a/app/overrides/spree/admin/shared/_configuration_menu/add_content.html.haml.deface b/app/overrides/spree/admin/shared/_configuration_menu/add_content.html.haml.deface new file mode 100644 index 0000000000..0709cb9451 --- /dev/null +++ b/app/overrides/spree/admin/shared/_configuration_menu/add_content.html.haml.deface @@ -0,0 +1,3 @@ +/ insert_bottom "[data-hook='admin_configurations_sidebar_menu']" + +%li= link_to 'Content', main_app.edit_admin_content_path diff --git a/app/overrides/spree/admin/tax_settings/edit/add_products_require_tax_category.html.haml.deface b/app/overrides/spree/admin/tax_settings/edit/add_products_require_tax_category.html.haml.deface deleted file mode 100644 index 588669f005..0000000000 --- a/app/overrides/spree/admin/tax_settings/edit/add_products_require_tax_category.html.haml.deface +++ /dev/null @@ -1,6 +0,0 @@ -/ insert_before "[data-hook='shipment_vat']" - -%div.field.align-center{ "data-hook" => "products_require_tax_category" } - = hidden_field_tag 'preferences[products_require_tax_category]', '0' - = check_box_tag 'preferences[products_require_tax_category]', '1', Spree::Config[:products_require_tax_category] - = label_tag nil, t(:products_require_tax_category) \ No newline at end of file diff --git a/app/overrides/spree/admin/tax_settings/edit/shipping_tax_rate.html.haml.deface b/app/overrides/spree/admin/tax_settings/edit/shipping_tax_rate.html.haml.deface deleted file mode 100644 index b378ba84a6..0000000000 --- a/app/overrides/spree/admin/tax_settings/edit/shipping_tax_rate.html.haml.deface +++ /dev/null @@ -1,5 +0,0 @@ -/ insert_after "[data-hook='shipment_vat']" - -.field.align-center{ "data-hook" => "shipping_tax_rate" } - = number_field_tag "preferences[shipping_tax_rate]", Spree::Config[:shipping_tax_rate].to_f, in: 0.0..1.0, step: 0.01 - = label_tag nil, t(:shipping_tax_rate) \ No newline at end of file diff --git a/app/overrides/spree/layouts/admin/add_customers_admin_tab.html.haml.deface b/app/overrides/spree/layouts/admin/add_customers_admin_tab.html.haml.deface new file mode 100644 index 0000000000..7b7534b23f --- /dev/null +++ b/app/overrides/spree/layouts/admin/add_customers_admin_tab.html.haml.deface @@ -0,0 +1,2 @@ +/ insert_bottom "[data-hook='admin_tabs'], #admin_tabs[data-hook]" += tab :customers, :url => main_app.admin_customers_path diff --git a/app/serializers/api/admin/index_enterprise_serializer.rb b/app/serializers/api/admin/index_enterprise_serializer.rb new file mode 100644 index 0000000000..0f9166f4f8 --- /dev/null +++ b/app/serializers/api/admin/index_enterprise_serializer.rb @@ -0,0 +1,56 @@ +class Api::Admin::IndexEnterpriseSerializer < ActiveModel::Serializer + attributes :name, :id, :permalink, :is_primary_producer, :sells, :producer_profile_only, :owned, :edit_path + + attributes :issues, :warnings + + def owned + return true if options[:spree_current_user].admin? + object.owner == options[:spree_current_user] + end + + def edit_path + edit_admin_enterprise_path(object) + end + + def shipping_methods_ok? + return true unless object.is_distributor + object.shipping_methods.any? + end + + def payment_methods_ok? + return true unless object.is_distributor + object.payment_methods.any? + end + + def issues + issues = [] + + issues << { + description: "#{object.name} currently has no shipping methods.", + link: "Create New" + } unless shipping_methods_ok? + + issues << { + description: "#{object.name} currently has no payment methods.", + link: "Create New" + } unless payment_methods_ok? + + issues << { + description: "Email confirmation is pending. We've sent a confirmation email to #{object.email}.", + link: "Resend Email" + } unless object.confirmed? + + issues + end + + def warnings + warnings = [] + + warnings << { + description: "#{object.name} is not visible and so cannot be found on the map or in searches", + link: "Edit" + } unless object.visible + + warnings + end +end diff --git a/app/serializers/api/admin/line_item_serializer.rb b/app/serializers/api/admin/line_item_serializer.rb index 21fde91145..d12e3b291a 100644 --- a/app/serializers/api/admin/line_item_serializer.rb +++ b/app/serializers/api/admin/line_item_serializer.rb @@ -1,5 +1,5 @@ class Api::Admin::LineItemSerializer < ActiveModel::Serializer - attributes :id, :quantity, :max_quantity, :supplier, :price, :unit_value, :units_product, :units_variant + attributes :id, :quantity, :max_quantity, :supplier, :price, :final_weight_volume, :units_product, :units_variant def supplier Api::Admin::IdNameSerializer.new(object.product.supplier).serializable_hash @@ -13,7 +13,7 @@ class Api::Admin::LineItemSerializer < ActiveModel::Serializer Api::Admin::UnitsVariantSerializer.new(object.variant).serializable_hash end - def unit_value - object.unit_value.to_f + def final_weight_volume + object.final_weight_volume.to_f end end diff --git a/app/serializers/api/enterprise_serializer.rb b/app/serializers/api/enterprise_serializer.rb index 44364c4dbc..201757314f 100644 --- a/app/serializers/api/enterprise_serializer.rb +++ b/app/serializers/api/enterprise_serializer.rb @@ -89,12 +89,12 @@ class Api::CachedEnterpriseSerializer < ActiveModel::Serializer def producers relatives = options[:data].relatives[object.id] - relatives ? ids_to_objs(relatives[:producers]) : [] + ids_to_objs(relatives.andand[:producers]) end def hubs relatives = options[:data].relatives[object.id] - relatives ? ids_to_objs(relatives[:distributors]) : [] + ids_to_objs(relatives.andand[:distributors]) end # Map svg icons. @@ -139,6 +139,7 @@ class Api::CachedEnterpriseSerializer < ActiveModel::Serializer private def ids_to_objs(ids) - ids.andand.map { |id| {id: id} } + return [] if ids.blank? + ids.map { |id| {id: id} } end end diff --git a/app/serializers/api/product_serializer.rb b/app/serializers/api/product_serializer.rb index 4c4aec2310..5a1d1b5c86 100644 --- a/app/serializers/api/product_serializer.rb +++ b/app/serializers/api/product_serializer.rb @@ -22,7 +22,12 @@ class Api::UncachedProductSerializer < ActiveModel::Serializer attributes :price def price - object.master.price_with_fees(options[:current_distributor], options[:current_order_cycle]) + if options[:enterprise_fee_calculator] + object.master.price + options[:enterprise_fee_calculator].indexed_fees_for(object.master) + else + object.master.price_with_fees(options[:current_distributor], options[:current_order_cycle]) + end + end end @@ -35,13 +40,14 @@ class Api::CachedProductSerializer < ActiveModel::Serializer attributes :properties_with_values has_many :variants, serializer: Api::VariantSerializer - has_many :taxons, serializer: Api::IdSerializer - has_many :images, serializer: Api::ImageSerializer - - has_one :supplier, serializer: Api::IdSerializer - has_one :primary_taxon, serializer: Api::TaxonSerializer has_one :master, serializer: Api::VariantSerializer + has_one :primary_taxon, serializer: Api::TaxonSerializer + has_many :taxons, serializer: Api::IdSerializer + + has_many :images, serializer: Api::ImageSerializer + has_one :supplier, serializer: Api::IdSerializer + def properties_with_values object.properties_including_inherited end diff --git a/app/serializers/api/variant_serializer.rb b/app/serializers/api/variant_serializer.rb index f995fa4345..6908eaf84f 100644 --- a/app/serializers/api/variant_serializer.rb +++ b/app/serializers/api/variant_serializer.rb @@ -1,21 +1,25 @@ class Api::VariantSerializer < ActiveModel::Serializer - attributes :id, :is_master, :count_on_hand, :name_to_display, :unit_to_display, + attributes :id, :is_master, :count_on_hand, :name_to_display, :unit_to_display, :options_text, :on_demand, :price, :fees, :price_with_fees, :product_name - def price_with_fees - object.price_with_fees(options[:current_distributor], options[:current_order_cycle]) - end - def price object.price end def fees - object.fees_by_type_for(options[:current_distributor], options[:current_order_cycle]) + options[:enterprise_fee_calculator].andand.indexed_fees_by_type_for(object) || + object.fees_by_type_for(options[:current_distributor], options[:current_order_cycle]) + end + + def price_with_fees + if options[:enterprise_fee_calculator] + object.price + options[:enterprise_fee_calculator].indexed_fees_for(object) + else + object.price_with_fees(options[:current_distributor], options[:current_order_cycle]) + end end def product_name object.product.name end - end diff --git a/app/views/admin/account/show.html.haml b/app/views/admin/account/show.html.haml new file mode 100644 index 0000000000..809d52eb2f --- /dev/null +++ b/app/views/admin/account/show.html.haml @@ -0,0 +1,59 @@ + +- content_for :page_title do + = t(:account) + + +- if @invoices.empty? + %h4= t(:no_invoices_to_display) + +- @invoices.order('year DESC, month DESC').each do |invoice| + - order = invoice.order + .row.invoice_title + .eight.columns.alpha + %h4= "#{t(:abbr_month_names, :scope => :date)[invoice.month]} #{invoice.year}#{invoice.order.completed? ? "" : "*"}" + .eight.columns.omega.text-right + %h4.balance= invoice.order.display_total + %table.invoice_summary + %col{ width: '20%' } + %col{ width: '60%' } + %col{ width: '20%' } + %thead + %th Date + %th= t(:description) + %th= t(:charge) + - invoice.billable_periods.select{ |bp| bp.bill > 0}.each do |billable_period| + %tr + %td.text-center= "#{billable_period.begins_at.strftime("%d/%m/%Y")}" + %td= billable_period.label + %td.text-right= billable_period.display_bill + - order.adjustments.where('source_type <> (?)', "BillablePeriod").each do |adjustment| + %tr + %td.text-center + %td= adjustment.label + %td.text-right= adjustment.display_amount + %tr.total + %td.text-center + %td= t(:total).upcase + %td.text-right= order.display_total + +-# - if @enterprises.empty? +-# %h4 No enterprises to display +-# +-# - @enterprises.each do |enterprise| +-# %h2= enterprise.name +-# %table +-# %thead +-# %th Begins +-# %th Ends +-# %th Sells +-# %th Trial? +-# %th Turnover +-# %th Bill +-# - enterprise.billable_periods.each do |billable_period| +-# %tr +-# %td= billable_period.begins_at.localtime.strftime("%F %T") +-# %td= billable_period.ends_at.localtime.strftime("%F %T") +-# %td= billable_period.sells +-# %td= billable_period.trial? +-# %td= billable_period.display_turnover +-# %td= billable_period.display_bill diff --git a/app/views/admin/accounts_and_billing_settings/_method_settings.html.haml b/app/views/admin/accounts_and_billing_settings/_method_settings.html.haml new file mode 100644 index 0000000000..17037d47e9 --- /dev/null +++ b/app/views/admin/accounts_and_billing_settings/_method_settings.html.haml @@ -0,0 +1,10 @@ +.row + .six.columns.alpha + .field + = label :settings, :default_accounts_payment_method_id, t(:default_accounts_payment_method) + = collection_select(:settings, :default_accounts_payment_method_id, @payment_methods, :id, :name, { include_blank: true, selected: Spree::Config.default_accounts_payment_method_id}, { class: "select2 fullwidth" }) + + .six.columns.omega + .field + = label :settings, :default_accounts_shipping_method_id, t(:default_accounts_shipping_method) + = collection_select(:settings, :default_accounts_shipping_method_id, @shipping_methods, :id, :name, { include_blank: true, selected: Spree::Config.default_accounts_shipping_method_id}, { class: "select2 fullwidth" }) diff --git a/app/views/admin/accounts_and_billing_settings/edit.html.haml b/app/views/admin/accounts_and_billing_settings/edit.html.haml new file mode 100644 index 0000000000..d13c81dbed --- /dev/null +++ b/app/views/admin/accounts_and_billing_settings/edit.html.haml @@ -0,0 +1,88 @@ += render :partial => 'spree/admin/shared/configuration_menu' + +- content_for :page_title do + = t(:accounts_and_billing_settings) + += render 'spree/shared/error_messages', target: @settings + +-# - month_options = (0...12).map { |i| Time.now.beginning_of_month - i.months }.map{ |t| [t.strftime("%b %Y"), t.strftime("%b %Y %z")]} + +%fieldset.no-border-bottom + %legend Settings + = form_for @settings, as: :settings, url: main_app.admin_accounts_and_billing_settings_path, :method => :put do |f| + .row{ ng: { app: 'admin.accounts_and_billing_settings' } } + .twelve.columns.alpha.omega + .field + = f.label :accounts_distributor_id, t(:accounts_administration_distributor) + = f.collection_select(:accounts_distributor_id, @distributors, :id, :name, { include_blank: true }, { class: "select2 fullwidth", 'watch-value-as' => "enterprise_id"}) + + = f.hidden_field :default_accounts_payment_method_id, value: '' + = f.hidden_field :default_accounts_shipping_method_id, value: '' + %div{ 'method-settings-for' => 'enterprise_id' } + + .row + .six.columns.alpha + %fieldset.no-border-bottom + %legend Update Invoices + = f.check_box :auto_update_invoices + = f.label :auto_update_invoices, "Auto-update invoices nightly at 1:00am" + + .six.columns.omega + %fieldset.no-border-bottom + %legend Finalise Invoices + = f.check_box :auto_finalize_invoices + = f.label :auto_finalize_invoices, "Auto-finalise invoices monthly on the 2nd at 1:30am" + + .row + .twelve.columns.alpha.omega.form-buttons{"data-hook" => "buttons"} + = button t(:update), 'icon-refresh', value: "update" + +%fieldset.no-border-bottom + %legend Manually Run Tasks + .row + .six.columns.alpha.step.text-center + .form-buttons{"data-hook" => "buttons"} + =link_to_with_icon "icon-undo", "Update User Invoices", + main_app.start_job_admin_accounts_and_billing_settings_path(job: { name: "update_account_invoices" }), + class: "button fullwidth" + + %br + + - if @update_account_invoices_job + %p.text-center + - if @update_account_invoices_job.run_at < Time.now + %strong In Progress + %br + Started at: + - else + %strong Queued + %br + Scheduled for: + = @update_account_invoices_job.run_at + - else + %p.explanation + Use this button to immediately update invoices for the month to date for each enterprise user in the system. This task can be set up to run automatically every night. + + + .six.columns.omega.step.text-center + .form-buttons{"data-hook" => "buttons"} + =link_to_with_icon "icon-ok-sign", "Finalise User Invoices", + main_app.start_job_admin_accounts_and_billing_settings_path(job: { name: "finalize_account_invoices" }), + class: "button fullwidth" + + %br + + - if @finalize_account_invoices_job + %p.text-center + - if @finalize_account_invoices_job.run_at < Time.now + %strong In Progress + %br + Started at: + - else + %strong Queued + %br + Scheduled for: + = @finalize_account_invoices_job.run_at + - else + %p.explanation + Use this button to finalize all invoices in the system for the previous calendar month. This task can be set up to run automatically once a month. diff --git a/app/views/admin/contents/_fieldset.html.haml b/app/views/admin/contents/_fieldset.html.haml new file mode 100644 index 0000000000..709f3d5ec8 --- /dev/null +++ b/app/views/admin/contents/_fieldset.html.haml @@ -0,0 +1,8 @@ +%fieldset.no-border-bottom + %legend{align: "center"}= name + - preferences.each do |key| + - type = ContentConfig.preference_type(key) + .field + = label_tag(key, t(key) + ': ') + tag(:br) if type != :boolean + = preference_field_tag(key, ContentConfig[key], :type => type) + = label_tag(key, t(key)) + tag(:br) if type == :boolean diff --git a/app/views/admin/contents/edit.html.haml b/app/views/admin/contents/edit.html.haml new file mode 100644 index 0000000000..82751abebc --- /dev/null +++ b/app/views/admin/contents/edit.html.haml @@ -0,0 +1,15 @@ += render 'spree/admin/shared/configuration_menu' + +- content_for :page_title do + Content + + += form_tag main_app.admin_content_path, method: :put, multipart: true do + #preferences + - @preference_sections.each do |preference_section| + = render 'fieldset', name: preference_section[:name], preferences: preference_section[:preferences] + + .form-buttons.filter-actions.actions{"data-hook" => "buttons"} + = button t(:update), 'icon-refresh' + %span.or= t(:or) + = link_to_with_icon 'icon-remove', t(:cancel), main_app.edit_admin_content_path, class: 'button' diff --git a/app/views/admin/enterprise_relationships/_enterprise_relationship.html.haml b/app/views/admin/enterprise_relationships/_enterprise_relationship.html.haml index 45e79b1877..996bc487d8 100644 --- a/app/views/admin/enterprise_relationships/_enterprise_relationship.html.haml +++ b/app/views/admin/enterprise_relationships/_enterprise_relationship.html.haml @@ -1,10 +1,10 @@ -%tr{"ng-repeat" => "enterprise_relationship in EnterpriseRelationships.enterprise_relationships | filter:query"} +%tr{"ng-repeat" => "enterprise_relationship in EnterpriseRelationships.enterprise_relationships | keywords:query"} %td {{ enterprise_relationship.parent_name }} %td permits %td {{ enterprise_relationship.child_name }} %td %ul %li{"ng-repeat" => "permission in enterprise_relationship.permissions"} - {{ EnterpriseRelationships.permission_presentation(permission.name) }} + to {{ EnterpriseRelationships.permission_presentation(permission.name) }} %td.actions %a.delete-enterprise-relationship.icon-trash.no-text{'ng-click' => 'delete(enterprise_relationship)'} diff --git a/app/views/admin/enterprise_relationships/_form.html.haml b/app/views/admin/enterprise_relationships/_form.html.haml index 9d031a57c5..433f9f4ca0 100644 --- a/app/views/admin/enterprise_relationships/_form.html.haml +++ b/app/views/admin/enterprise_relationships/_form.html.haml @@ -7,10 +7,13 @@ %td %select{name: "enterprise_relationship_child_id", "ng-model" => "child_id", "ng-options" => "e.id as e.name for e in Enterprises.all_enterprises"} %td + %label + %input{type: "checkbox", ng: {checked: "allPermissionsChecked()", click: "checkAllPermissions()"}} + Everything %div{"ng-repeat" => "permission in EnterpriseRelationships.all_permissions"} %label %input{type: "checkbox", "ng-model" => "permissions[permission]"} - {{ EnterpriseRelationships.permission_presentation(permission) }} + to {{ EnterpriseRelationships.permission_presentation(permission) }} %td.actions %input{type: "button", value: "Create", "ng-click" => "create()"} .errors {{ EnterpriseRelationships.create_errors }} diff --git a/app/views/admin/enterprise_relationships/_search_input.html.haml b/app/views/admin/enterprise_relationships/_search_input.html.haml new file mode 100644 index 0000000000..b8bcbc62c6 --- /dev/null +++ b/app/views/admin/enterprise_relationships/_search_input.html.haml @@ -0,0 +1,5 @@ +%input.search{"ng-model" => "query", "placeholder" => "Search"} + +%label{ng: {repeat: "permission in EnterpriseRelationships.all_permissions"}} + %input{type: "checkbox", ng: {click: "$parent.query = toggleKeyword($parent.query, permission)"}} + {{ EnterpriseRelationships.permission_presentation(permission) }} diff --git a/app/views/admin/enterprise_relationships/index.html.haml b/app/views/admin/enterprise_relationships/index.html.haml index 0807a37825..40fbdbc415 100644 --- a/app/views/admin/enterprise_relationships/index.html.haml +++ b/app/views/admin/enterprise_relationships/index.html.haml @@ -6,7 +6,7 @@ %div{"ng-app" => "ofn.admin", "ng-controller" => "AdminEnterpriseRelationshipsCtrl"} = render 'data' - %input.search{"ng-model" => "query", "placeholder" => "Search"} + = render 'search_input' %table#enterprise-relationships %tbody diff --git a/app/views/admin/enterprises/_admin_index.html.haml b/app/views/admin/enterprises/_admin_index.html.haml new file mode 100644 index 0000000000..68c74a783c --- /dev/null +++ b/app/views/admin/enterprises/_admin_index.html.haml @@ -0,0 +1,44 @@ +-# For purposes of debugging bulk_update. See Admin/Enterprises#bulk_update. +- if flash[:action] + %p= flash[:action] + += form_for @enterprise_set, url: main_app.bulk_update_admin_enterprises_path do |f| + %table#listing_enterprises.index + %colgroup + %col{style: "width: 25%;"}/ + %col{style: "width: 15%;"}/ + %col{style: "width: 5%;"}/ + - if spree_current_user.admin? + %col{style: "width: 12%;"}/ + - if spree_current_user.admin? + %col{style: "width: 18%;"}/ + %col{style: "width: 25%;"}/ + %thead + %tr{"data-hook" => "enterprises_header"} + %th Name + %th Role + - if spree_current_user.admin? + %th Sells + %th Visible? + - if spree_current_user.admin? + %th Owner + %th + %tbody + = f.fields_for :collection do |enterprise_form| + - enterprise = enterprise_form.object + %tr{class: "enterprise-#{enterprise.id}"} + %td= link_to enterprise.name, main_app.edit_admin_enterprise_path(enterprise) + %td + = enterprise_form.check_box :is_primary_producer + Producer + - if spree_current_user.admin? + %td= enterprise_form.select :sells, Enterprise::SELLS, {}, class: 'select2 fullwidth' + %td= enterprise_form.check_box :visible + - if spree_current_user.admin? + %td= enterprise_form.select :owner_id, enterprise.users.map{ |e| [ e.email, e.id ] }, {}, class: "select2 fullwidth" + %td{"data-hook" => "admin_users_index_row_actions"} + = render 'actions', enterprise: enterprise + - if @enterprises.empty? + %tr + %td{colspan: "4"}= t(:none) + = f.submit 'Update' diff --git a/app/views/admin/enterprises/_change_type_form.html.haml b/app/views/admin/enterprises/_change_type_form.html.haml new file mode 100644 index 0000000000..740857e381 --- /dev/null +++ b/app/views/admin/enterprises/_change_type_form.html.haml @@ -0,0 +1,104 @@ += admin_inject_enterprise + += form_for @enterprise, url: main_app.register_admin_enterprise_path(@enterprise), + html: { name: "change_type", id: "change_type", novalidate: true, "ng-app" => "admin.enterprises", "ng-controller"=> 'changeTypeFormCtrl' } do |change_type_form| + -# Have to use hidden:'true' on this input rather than type:'hidden' as the latter seems to break ngPattern and therefore validation + %input{ hidden: "true", name: "sells", ng: { required: true, pattern: "/^(none|own|any)$/", model: 'sells', value: "sells"} } + + .row + .options.sixteen.columns.alpha + - if @enterprise.is_primary_producer + .basic_producer.option.one-third.column.alpha + %a.full-width.button.selector{ ng: { click: "sells='none'", class: "{selected: sells=='none'}" } } + .top + %h3 Producer Profile + %p Connect through OFN + .bottom ALWAYS FREE + %p.description + Add your products to Open Food Network, allowing hubs to stock your products in their stores. + %br + %br + Having a profile, and making connections within your local food system through the Open Food Network will always be free. + + .producer_shop.option.one-third.column + %a.full-width.button.selector{ ng: { click: "sells='own'", class: "{selected: sells=='own'}" } } + .top + %h3 Producer Shop + %p Sell your own produce + .bottom + \%2 OF SALES + %br + CAPPED AT $50 PER MONTH + %p.description + Sell your products directly to customers through your very own Open Food Network shopfront. + %br + %br + A Producer Shop is for your produce only, if you want to sell produce grown/produced off site, select 'Producer Hub'. + %br + %br + You will be billed for 2% of your actual transactions, capped at $50 a month (so if you don’t sell anything you don’t pay anything, but you never pay more than $50 a month). + + .full_hub.option.one-third.column.omega + %a.full-width.button.selector{ ng: { click: "sells='any'", class: "{selected: sells=='any'}" } } + .top + %h3 Producer Hub + %p Sell produce from self and others + .bottom + \%2 OF SALES + %br + CAPPED AT $50 PER MONTH + %p.description + Your enterprise is the backbone of your local food system. You can sell your own produce as well as produce aggregated from other enterprises through your shopfront on the Open Food Network. + %br + %br + You will be billed for 2% of your actual transactions, capped at $50 a month (so if you don’t sell anything you don’t pay anything, but you never pay more than $50 a month). + + + -# %p.description + -# Test out having your own shopfront with full access to all Shopfront features for 30 days. + -# %br + -# %br + -# At the end of your trial, there is a one-off $200 fee to fully activate your account. Then you will be billed for 2% of your actual transactions, capped at $50 a month (so if you don’t sell anything you don’t pay anything, but you never pay more than $50 a month). + + - else + .shop_profile.option.one-third.column.alpha + %a.full-width.button.selector{ ng: { click: "sells='none'", class: "{selected: sells=='none'}" } } + .top + %h3 Profile Only + %p Get a listing + .bottom ALWAYS FREE + %p.description + People can find and contact you on the Open Food Network. Your enterprise will be visible on the map, and will be searchable in listings. + %br + %br + Having a profile, and making connections within your local food system through the Open Food Network will always be free. + + .full_hub.option.one-third.column + %a.full-width.button.selector{ ng: { click: "sells='any'", class: "{selected: sells=='any'}" } } + .top + %h3 Hub Shop + %p Sell produce from others + .bottom + \%2 OF SALES + %br + CAPPED AT $50 PER MONTH + %p.description + Your enterprise is the backbone of your local food system. You aggregate produce from other enterprises and can sell it through your shop on the Open Food Network. + %br + %br + You will be billed for 2% of your actual transactions, capped at $50 a month (so if you don’t sell anything you don’t pay anything, but you never pay more than $50 a month). + + + .row + .sixteen.columns.alpha + %span.error{ ng: { show: "(change_type.sells.$error.required || change_type.sells.$error.pattern) && submitted" } } + Please choose one of the options above. + - if @enterprise.sells == 'unspecified' && @enterprise.shop_trial_start_date.nil? + %input.button.big{ type: 'submit', value: 'Start 30 Day Trial', ng: { click: "submit(change_type)", show: "sells=='own' || sells=='any'" } } + %input.button.big{ type: 'submit', value: 'Select and continue', ng: { click: "submit(change_type)", hide: "sells=='own' || sells=='any'" } } + - elsif @enterprise.sells == 'unspecified' + %input.button.big{ type: 'submit', value: 'Select and continue', ng: { click: "submit(change_type)" } } + - else + %input.button.big{ type: 'submit', value: 'Change now', ng: { click: "submit(change_type)" } } + %br + %hr diff --git a/app/views/admin/enterprises/_enterprise_user_index.html.haml b/app/views/admin/enterprises/_enterprise_user_index.html.haml new file mode 100644 index 0000000000..5f4ad6cca9 --- /dev/null +++ b/app/views/admin/enterprises/_enterprise_user_index.html.haml @@ -0,0 +1,60 @@ +%div{ ng: { app: 'admin.enterprises', controller: 'enterprisesCtrl' } } + .row{ 'ng-hide' => '!loaded()' } + .controls{ :class => "sixteen columns alpha", :style => "margin-bottom: 15px;" } + .four.columns.alpha + %input{ :class => "fullwidth", :type => "text", :id => 'quick_search', 'ng-model' => 'quickSearch', :placeholder => 'Search By Name' } + .six.columns + -# %div.ofn_drop_down{ 'ng-controller' => "DropDownCtrl", :id => "bulk_actions_dropdown", 'ofn-drop-down' => true } + -# %span{ :class => 'icon-check' } Actions + -# %span{ 'ng-class' => "expanded && 'icon-caret-up' || !expanded && 'icon-caret-down'" } + -# %div.menu{ 'ng-show' => "expanded" } + -# %div.menu_item{ :class => "three columns alpha", 'ng-repeat' => "action in bulkActions", 'ng-click' => "selectedBulkAction.callback(filteredEnterprises)", 'ofn-close-on-click' => true } + -# %span{ :class => 'three columns omega' } {{action.name }} + .three.columns + .three.columns.omega + %div.ofn_drop_down{ 'ng-controller' => "DropDownCtrl", :id => "columns_dropdown", 'ofn-drop-down' => true, :style => 'float:right;' } + %span{ :class => 'icon-reorder' } Columns + %span{ 'ng-class' => "expanded && 'icon-caret-up' || !expanded && 'icon-caret-down'" } + %div.menu{ 'ng-show' => "expanded" } + %div.menu_item{ :class => "three columns alpha", 'ng-repeat' => "column in columns", 'ofn-toggle-column' => true } + %span{ :class => 'one column alpha', :style => 'text-align: center'} {{ column.visible && "✓" || !column.visible && " " }} + %span{ :class => 'two columns omega' } {{column.name }} + .row{ 'ng-if' => '!loaded()' } + .sixteen.columns.alpha#loading + %img.spinner{ src: "/assets/spinning-circles.svg" } + %h1 LOADING ENTERPRISES + .row{ :class => "sixteen columns alpha", 'ng-show' => 'loaded() && filteredEnterprises.length == 0'} + %h1#no_results No enterprises found. + + + .row{ ng: { show: "loaded() && filteredEnterprises.length > 0" }, bindonce: true } + %table.index#enterprises + %col.name{ width: "28%", ng: { show: 'columns.name.visible' } } + %col.producer{ width: "18%", ng: { show: 'columns.producer.visible' }} + %col.package{ width: "18%", ng: { show: 'columns.package.visible' }} + %col.status{ width: "18%", ng: { show: 'columns.status.visible' }} + %col.manage{ width: "18%", ng: { show: 'columns.manage.visible' }} + %thead + %tr{ ng: { controller: "ColumnsCtrl" } } + %th.name{ ng: { show: 'columns.name.visible' } } + Name + %th.producer{ ng: { show: 'columns.producer.visible' } } Producer? + %th.package{ ng: { show: 'columns.package.visible' } } Package + %th.status{ ng: { show: 'columns.status.visible' } } Status + %th.manage{ ng: { show: 'columns.manage.visible' } } Manage + %tbody{ :id => "e_{{enterprise.id}}", ng: { repeat: "enterprise in filteredEnterprises = ( allEnterprises | filter:{ name: quickSearch } )", controller: 'EnterpriseIndexRowCtrl' } } + %tr.enterprise.panel-toggle-row{ object: "enterprise", ng: { class: { even: "'even'", odd: "'odd'"} } } + %td.name{ ng: { show: 'columns.name.visible' } } + %span{ bo: { bind: "enterprise.name" } } + %td.producer.panel-toggle.text-center{ ng: { show: 'columns.producer.visible', class: "{error: producerError}" }, name: "producer" } + %h5{ ng: { bind: "producer" } } + %td.package.panel-toggle.text-center{ ng: { show: 'columns.package.visible', class: "{error: packageError}" }, name: "package" } + %h5{ ng: { bind: "package" } } + %td.status.panel-toggle.text-center{ ng: { show: 'columns.status.visible' }, name: "status" } + %i.icon-status{ bo: { class: "status" } } + %td.manage{ ng: { show: 'columns.manage.visible' } } + %a.button.fullwidth{ bo: { href: 'enterprise.edit_path' } } + Manage + %i.icon-arrow-right + + %tr.panel-row{ object: "enterprise", panels: "{producer: 'enterprise_producer', package: 'enterprise_package', status: 'enterprise_status'}" } diff --git a/app/views/admin/enterprises/index.html.haml b/app/views/admin/enterprises/index.html.haml index 8269cd3699..826d09336c 100644 --- a/app/views/admin/enterprises/index.html.haml +++ b/app/views/admin/enterprises/index.html.haml @@ -2,6 +2,8 @@ Enterprises - content_for :page_actions do + = render 'admin/shared/user_guide_link' + - if spree_current_user.can_own_more_enterprises? %li#new_product_link = button_link_to "New Enterprise", main_app.new_admin_enterprise_path, :icon => 'icon-plus', :id => 'admin_new_enterprise_link' @@ -10,47 +12,7 @@ = render :partial => 'spree/shared/error_messages', :locals => { :target => @enterprise_set } --# For purposes of debugging bulk_update. See Admin/Enterprises#bulk_update. -- if flash[:action] - %p= flash[:action] - -= form_for @enterprise_set, url: main_app.bulk_update_admin_enterprises_path do |f| - %table#listing_enterprises.index - %colgroup - %col{style: "width: 25%;"}/ - %col{style: "width: 15%;"}/ - %col{style: "width: 5%;"}/ - - if spree_current_user.admin? - %col{style: "width: 12%;"}/ - - if spree_current_user.admin? - %col{style: "width: 18%;"}/ - %col{style: "width: 25%;"}/ - %thead - %tr{"data-hook" => "enterprises_header"} - %th Name - %th Role - - if spree_current_user.admin? - %th Sells - %th Visible? - - if spree_current_user.admin? - %th Owner - %th - %tbody - = f.fields_for :collection do |enterprise_form| - - enterprise = enterprise_form.object - %tr{class: "enterprise-#{enterprise.id}"} - %td= link_to enterprise.name, main_app.edit_admin_enterprise_path(enterprise) - %td - = enterprise_form.check_box :is_primary_producer - Producer - - if spree_current_user.admin? - %td= enterprise_form.select :sells, Enterprise::SELLS, {}, class: 'select2 fullwidth' - %td= enterprise_form.check_box :visible - - if spree_current_user.admin? - %td= enterprise_form.select :owner_id, enterprise.users.map{ |e| [ e.email, e.id ] }, {}, class: "select2 fullwidth" - %td{"data-hook" => "admin_users_index_row_actions"} - = render 'actions', enterprise: enterprise - - if @enterprises.empty? - %tr - %td{colspan: "4"}= t(:none) - = f.submit 'Update' +- if spree_current_user.admin? + = render 'admin_index' +- else + = render 'enterprise_user_index' diff --git a/app/views/spree/admin/overview/welcome.html.haml b/app/views/admin/enterprises/welcome.html.haml similarity index 100% rename from app/views/spree/admin/overview/welcome.html.haml rename to app/views/admin/enterprises/welcome.html.haml diff --git a/app/views/admin/shared/_enterprises_sub_menu.html.haml b/app/views/admin/shared/_enterprises_sub_menu.html.haml index 31366091ef..3088b7e73e 100644 --- a/app/views/admin/shared/_enterprises_sub_menu.html.haml +++ b/app/views/admin/shared/_enterprises_sub_menu.html.haml @@ -1,4 +1,4 @@ = content_for :sub_menu do %ul#sub_nav.inline-menu{"data-hook" => "admin_enterprise_sub_tabs"} = tab :enterprises, url: main_app.admin_enterprises_path - = tab :relationships, url: main_app.admin_enterprise_relationships_path, match_path: '/enterprise_relationships' + = tab :enterprise_relationships, url: main_app.admin_enterprise_relationships_path diff --git a/app/views/admin/shared/_user_guide_link.html.haml b/app/views/admin/shared/_user_guide_link.html.haml new file mode 100644 index 0000000000..bfae6002e7 --- /dev/null +++ b/app/views/admin/shared/_user_guide_link.html.haml @@ -0,0 +1 @@ += button_link_to "User Guide", "http://www.openfoodnetwork.org/platform/user-guide/", :icon => 'icon-external-link', :id => 'user_guide_link', target: '_blank' diff --git a/app/views/checkout/edit.html.haml b/app/views/checkout/edit.html.haml index 2f30c93ba5..c2cc0aaa9d 100644 --- a/app/views/checkout/edit.html.haml +++ b/app/views/checkout/edit.html.haml @@ -1,6 +1,9 @@ +- content_for(:title) do + Checkout + = inject_enterprises -.darkswarm +.darkswarm.footer-pad - content_for :order_cycle_form do %closing Checkout now @@ -23,4 +26,3 @@ = render partial: "shared/footer" - diff --git a/app/views/enterprise_mailer/confirmation_instructions.html.haml b/app/views/enterprise_mailer/confirmation_instructions.html.haml index 3fe7ba09e9..3ee6e8fd12 100644 --- a/app/views/enterprise_mailer/confirmation_instructions.html.haml +++ b/app/views/enterprise_mailer/confirmation_instructions.html.haml @@ -15,7 +15,7 @@ %p After confirming your email you can access your administration account for this enterprise. See the - = link_to 'User Guide', 'http://global.openfoodnetwork.org/platform/user-guide/' + = link_to 'User Guide', 'http://www.openfoodnetwork.org/platform/user-guide/' = "to find out more about #{ Spree::Config[:site_name] }'s features and to start using your profile or online store." = render 'shared/mailers/signoff' diff --git a/app/views/enterprise_mailer/welcome.html.haml b/app/views/enterprise_mailer/welcome.html.haml index 3cd9d14034..ea939dbc6f 100644 --- a/app/views/enterprise_mailer/welcome.html.haml +++ b/app/views/enterprise_mailer/welcome.html.haml @@ -8,7 +8,7 @@ %p The User Guide with detailed support for setting up your Producer or Hub is here: - = link_to 'Open Food Network User Guide', 'http://global.openfoodnetwork.org/platform/user-guide/' + = link_to 'Open Food Network User Guide', 'http://www.openfoodnetwork.org/platform/user-guide/' %p You can manage your account by logging into the diff --git a/app/views/enterprises/_distributor_details.html.haml b/app/views/enterprises/_distributor_details.html.haml deleted file mode 100644 index a3f070b451..0000000000 --- a/app/views/enterprises/_distributor_details.html.haml +++ /dev/null @@ -1,24 +0,0 @@ -.distributor-details{'data-hook' => 'distributor-details'} - %h2= distributor.name - %p - %strong Address: - %br/ - = render 'spree/shared/address', :address => distributor.address - %p - %strong Next collection time: - %br/ - = distributor.next_collection_at - %p - %strong Regular collection times: - %br/ - = distributor.pickup_times - %p - %strong Contact: - %br/ - = distributor.contact - %br/ - = "Phone: #{distributor.phone}" - %br/ - = "Email: #{distributor.email}" - %p= distributor.description - %p= link_to distributor.website, distributor.website if distributor.website diff --git a/app/views/enterprises/distributors.html.haml b/app/views/enterprises/distributors.html.haml deleted file mode 100644 index 941b65237b..0000000000 --- a/app/views/enterprises/distributors.html.haml +++ /dev/null @@ -1,12 +0,0 @@ -- content_for :sidebar do - %div{'data-hook' => "homepage_sidebar_navigation"} - = render 'spree/sidebar' - - -%h1 Distributors - -= cms_page_content(:content, Cms::Page.find_by_full_path('/enterprises/distributors')) - -%ul.enterprises - - @distributors.each do |distributor| - %li= link_to distributor.name, distributor diff --git a/app/views/enterprises/distributors.js.erb b/app/views/enterprises/distributors.js.erb deleted file mode 100644 index aed01164da..0000000000 --- a/app/views/enterprises/distributors.js.erb +++ /dev/null @@ -1 +0,0 @@ -distributors = <%= @distributor_details.to_json.html_safe %>; \ No newline at end of file diff --git a/app/views/enterprises/index.html.haml b/app/views/enterprises/index.html.haml deleted file mode 100644 index a1e07dfc04..0000000000 --- a/app/views/enterprises/index.html.haml +++ /dev/null @@ -1,12 +0,0 @@ -- content_for :sidebar do - %div{'data-hook' => "homepage_sidebar_navigation"} - = render 'spree/sidebar' - - -%h1 Enterprises - -= cms_page_content(:content, Cms::Page.find_by_full_path('/enterprises')) - -%ul.enterprises - - @enterprises.each do |enterprise| - %li= link_to enterprise.name, enterprise diff --git a/app/views/enterprises/shop.html.haml b/app/views/enterprises/shop.html.haml index 08e25417cb..7ad617a4a1 100644 --- a/app/views/enterprises/shop.html.haml +++ b/app/views/enterprises/shop.html.haml @@ -1,3 +1,6 @@ +- content_for(:title) do + = current_distributor.name + = inject_enterprises %shop.darkswarm @@ -13,7 +16,7 @@ / Will this label should be a variable to reflect 'Ready for pickup / delivery' as appropriate %select.avenir#order_cycle_id{"ng-model" => "order_cycle.order_cycle_id", - "ng-change" => "changeOrderCycle()", + "ofn-change-order-cycle" => true, "ng-options" => "oc.id as oc.time for oc in #{@order_cycles.map {|oc| {time: pickup_time(oc), id: oc.id}}.to_json}", "popover-placement" => "left", "popover" => "Choose when you want your order:", "popover-trigger" => "openTrigger"} diff --git a/app/views/enterprises/shop_front.html.haml b/app/views/enterprises/shop_front.html.haml deleted file mode 100644 index 79364d368a..0000000000 --- a/app/views/enterprises/shop_front.html.haml +++ /dev/null @@ -1,15 +0,0 @@ -.row - .large-12.columns - %h2= @enterprise.name -.row - .large-12.columns= @enterprise.long_description.andand.html_safe - -.row - .large-12.columns - .products - - @products.each_slice(4).to_a.each do |products_row| - .row - - products_row.each do |product| - .large-4.columns.centered - .clearfix= link_to small_image(product), product - = link_to product.name, product diff --git a/app/views/enterprises/show.html.haml b/app/views/enterprises/show.html.haml deleted file mode 100644 index 8e5035dbcc..0000000000 --- a/app/views/enterprises/show.html.haml +++ /dev/null @@ -1,18 +0,0 @@ -- if @enterprise != current_distributor - %h2= @enterprise.name - -.enterprise-description= @enterprise.long_description.andand.html_safe - -- if current_distributor - = render :template => 'spree/products/index' - -- else - %h3 Hubs that distribute our products - %p.hint Select a hub to start shopping: - - %ul#supplier-distributors - - if @distributors.delete @enterprise - %li= link_to "Buy direct from the farm", enterprise_shop_path(@enterprise), {class: distributor_link_class(@enterprise)} - - - @distributors.each do |distributor| - %li= render partial: "shared/distributor", object: distributor diff --git a/app/views/enterprises/suppliers.html.haml b/app/views/enterprises/suppliers.html.haml deleted file mode 100644 index c67a7bc987..0000000000 --- a/app/views/enterprises/suppliers.html.haml +++ /dev/null @@ -1,12 +0,0 @@ -- content_for :sidebar do - %div{'data-hook' => "homepage_sidebar_navigation"} - = render 'spree/sidebar' - - -%h1 Suppliers - -= cms_page_content(:content, Cms::Page.find_by_full_path('/enterprises/suppliers')) - -%ul.enterprises - - @suppliers.each do |supplier| - %li= link_to supplier.name, supplier diff --git a/app/views/groups/index.html.haml b/app/views/groups/index.html.haml index 710039c7fb..537a81cc26 100644 --- a/app/views/groups/index.html.haml +++ b/app/views/groups/index.html.haml @@ -1,20 +1,23 @@ -= inject_enterprises +- content_for(:title) do + Groups + += inject_enterprises :javascript angular.module('Darkswarm').value('groups', #{render partial: "json/groups", object: @groups}) -#groups.pad-top{"ng-controller" => "GroupsCtrl"} +#groups.pad-top.footer-pad{"ng-controller" => "GroupsCtrl"} #active-table-search.row.pad-top .small-12.columns %h1 Groups / regions %p - %input.animate-show{type: :text, + %input{type: :text, "ng-model" => "query", placeholder: "Search name or keyword", "ng-debounce" => "150", "ofn-disable-enter" => true} - .group{"ng-repeat" => "group in groups = (Groups.groups | groups:query | orderBy:order)", + .group.animate-repeat{"ng-repeat" => "group in groups = (Groups.groups | groups:query | orderBy:order)", name: "group{{group.id}}", id: "group{{group.id}}"} .row.pad-top{bindonce: true} diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index 80101d9e8c..907926f8b7 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -1,3 +1,6 @@ +- content_for(:title) do + = @group.name + -# inject all enterprises as "enterprises" -# it could be more efficient to inject only the enterprises that are related to the group = inject_enterprises @@ -6,7 +9,7 @@ -# further hubs and producers of these enterprises can't be resolved within this small subset = inject_group_enterprises -#group-page.row.pad-top{"ng-controller" => "GroupPageCtrl"} +#group-page.row.pad-top.footer-pad{"ng-controller" => "GroupPageCtrl"} .small-12.columns.pad-top %header .row diff --git a/app/views/groups/signup.html.haml b/app/views/groups/signup.html.haml new file mode 100644 index 0000000000..c1c885d0f0 --- /dev/null +++ b/app/views/groups/signup.html.haml @@ -0,0 +1,61 @@ +- content_for(:title) do + Sign up as a group + +#panes + #shops-signup.pane + .row.header + .small-12.medium-12.columns.text-center + %h2 Groups sign up + .row.content + .small-12.medium-6.medium-offset-3.columns.text-center + %p.text-big We're an amazing platform for collaborative marketing, the easiest way for your members and stakeholders to reach new markets. We're non-profit, affordable, and simple. + %br + %a.button.transparent{href: "hello@openfoodnetwork.org?subject=I'd%20like%20to%20talk%20to%20you%20about%20groups%20on%20the%20Open%20Food%20Network".reverse, target: '_blank', mailto: true} + Email us + + .groups-details.pane + .row + .small-12.medium-8.medium-offset-2.columns + %h3.text-center We transform food systems fairly. + %p.text-big It's why we get out of bed every day. We're a global non-profit, based on open source code. We play fair. You can always trust us. + %p.text-big We know you have big ideas, and we want to help. We'll share our knowledge, networks and resources. We know that isolation doesn't create change, so we'll partner with you. + %br + %h3.text-center We meet you where you are. + %p.text-big You might be an alliance of food hubs, producers, or distributors, and an industry body, or a local government. + %p.text-big Whatever your role in your local food movement, we're ready to help. However you come to wonder what Open Food Network would look like or is doing in your part of the world, let's start the conversation. + %br + %h3.text-center We make food movements make more sense. + %p.text-big You need to activate and enable your networks, we offer a platform for conversation and action. You need real engagement. We’ll help reach all the players, all the stakeholders, all the sectors. + %p.text-big You need resourcing. We’ll bring all our experience to bear. You need cooperation. We’ll better connect you to a global network of peers. + .pane + .row + .small-12.medium-10.medium-offset-1.columns.text-center + %h2 Group Account + -# %p.text-big + -# / If there is a time-sensitive offer you can write it here, e.g. + -# Time-sensitive offer goes here! + %br + = ContentConfig.group_signup_pricing_table_html.html_safe + + #shops-case-studies + .row + .small-12.medium-10.medium-offset-1.columns + %h2.text-center Case studies + %br + = ContentConfig.group_signup_case_studies_html.html_safe + + .pane#cta + .row + .small-12.medium-6.medium-offset-3.columns.text-center + %h2 Ready to discuss? + %p.text-big Get in touch to discover what OFN can do for you: + %a.button.transparent{href: "hello@openfoodnetwork.org?subject=I'd%20like%20to%20talk%20to%20you%20about%20groups%20on%20the%20Open%20Food%20Network".reverse, target: '_blank', mailto: true} + Email us + + #hub-details.pane.footer-pad + .row + .small-12.medium-10.medium-offset-1.columns + %h2.text-center Here's the detail. + = ContentConfig.group_signup_detail_html.html_safe + += render partial: "shared/footer" diff --git a/app/views/home/_beta.en-GB.html.haml b/app/views/home/_beta.en-GB.html.haml deleted file mode 100644 index 2badc7c826..0000000000 --- a/app/views/home/_beta.en-GB.html.haml +++ /dev/null @@ -1,13 +0,0 @@ -#beta.pane - - .row - .small-12.columns.text-center - %h2 S'cuse us - %h5 while we get (more) awesome - %p Open Food Network UK is a new service that’s being piloted right now! - %p Want to help? Or find out when OFN is coming to you? - %strong We’d love to hear from you: - %p - %a{href: "hello@openfoodnetwork.org".reverse, target: '_blank', mailto: true} Food buyers - | - %a{href: "hello@openfoodnetwork.org".reverse, target: '_blank', mailto: true} Food producers & farmers diff --git a/app/views/home/_beta.html.haml b/app/views/home/_beta.html.haml deleted file mode 100644 index af72211862..0000000000 --- a/app/views/home/_beta.html.haml +++ /dev/null @@ -1,13 +0,0 @@ -#beta.pane - - .row - .small-12.columns.text-center - %h2 S'cuse us - %h5 while we get (more) awesome - %p Open Food Network (beta) is a new service that’s being built right now! Our food producers are currently based around Melbourne and Victoria, and we hope to expand OFN nationally very soon. - %p Want to help? Or find out when OFN is coming to you? - %strong We’d love to hear from you: - %p - %a{href: "hello@openfoodnetwork.org".reverse, target: '_blank', mailto: true} Food buyers - | - %a{href: "hello@openfoodnetwork.org".reverse, target: '_blank', mailto: true} Food producers & farmers \ No newline at end of file diff --git a/app/views/home/_brandstory.html.haml b/app/views/home/_brandstory.html.haml new file mode 100644 index 0000000000..40e5b77bf4 --- /dev/null +++ b/app/views/home/_brandstory.html.haml @@ -0,0 +1,18 @@ +#brand-story.pane + .row + .small-12.medium-8.medium-offset-2.columns.text-center + %h2 Food, unincorporated. + %p Sometimes the best way to fix the system is to start a new one… + + #brand-story-text.hide-show.slideable + %p We begin from the ground up. With farmers and growers ready to tell their stories proudly and truly. With distributors ready to connect people with products fairly and honestly. With buyers who believe that better weekly shopping decisions can seriously change the world. + %p Then we need a way to make it real. A way to empower everyone who grows, sells and buys food. A way to tell all the stories, to handle all the logistics. A way to turn transaction into transformation every day. + %p So we build an online marketplace that levels the playing field. It’s transparent, so it creates real relationships. It’s open source, so it’s owned by everyone. It scales to regions and nations, so people start versions across the world. + %p It works everywhere. It changes everything. + %p + %strong We call it Open Food Network. + %p We all love food. Now we can love our food system too. + + %a.text-vbig{"slide-toggle" => "#brand-story-text", "ng-click" => "toggleBrandStory()"} + %i.ofn-i_005-caret-down{"ng-hide" => "brandStoryExpanded"} + %i.ofn-i_006-caret-up{ "ng-show" => "brandStoryExpanded"} diff --git a/app/views/home/_cta.html.haml b/app/views/home/_cta.html.haml new file mode 100644 index 0000000000..ab95bae95d --- /dev/null +++ b/app/views/home/_cta.html.haml @@ -0,0 +1,7 @@ +#cta.pane + .row + .small-12.columns.text-center + %h2 Shopping that makes the world a better place. + %br + %a.button.transparent{href: "/shops"} + I'm Ready diff --git a/app/views/home/_fat.html.haml b/app/views/home/_fat.html.haml index a5543fa2a3..8d857d03f7 100644 --- a/app/views/home/_fat.html.haml +++ b/app/views/home/_fat.html.haml @@ -1,21 +1,21 @@ -.row.active_table_row{"ng-show" => "open()", "ng-click" => "toggle($event)", "ng-class" => "{'open' : !ofn-i_032-closed-sign()}"} +.row.active_table_row{"ng-show" => "open()", "ng-click" => "toggle($event)", "ng-class" => "{'open' : !ofn-i_032-closed-sign()}", bindonce: true} .columns.small-12.medium-6.large-5.fat %div{"bo-if" => "hub.taxons"} %label Shop for .trans-sentence %span.fat-taxons{"ng-repeat" => "taxon in hub.taxons"} %render-svg{path: "{{taxon.icon}}"} - %span{"bo-text" => "taxon.name"} + %span{"bo-text" => "taxon.name"} %div.show-for-medium-up{"bo-if" => "hub.taxons.length==0"} .columns.small-12.medium-3.large-2.fat %div{"bo-if" => "hub.pickup || hub.delivery"} %label Delivery options %ul.small-block-grid-2.medium-block-grid-1.large-block-grid-1 - %li.pickup{"bo-if" => "hub.pickup"} + %li.pickup{"bo-if" => "hub.pickup"} %i.ofn-i_038-takeaway Pickup - %li.delivery{"bo-if" => "hub.delivery"} + %li.delivery{"bo-if" => "hub.delivery"} %i.ofn-i_039-delivery Delivery .columns.small-12.medium-3.large-5.fat diff --git a/app/views/home/_filters.html.haml b/app/views/home/_filters.html.haml index 6b339c13c9..21719b04b7 100644 --- a/app/views/home/_filters.html.haml +++ b/app/views/home/_filters.html.haml @@ -1,22 +1,21 @@ .row - -# = render partial: 'shared/components/filter_controls' - .small-12.medium-6.columns + = render partial: 'shared/components/filter_controls' + -# .small-12.medium-6.columns = render partial: 'shared/components/show_profiles' --# .row.animate-show{"ng-show" => "filtersActive"} --# .small-12.columns --# .row.filter-box --# .small-12.large-9.columns --# %h5.tdhead --# .light Filter by --# Type --# %ul.small-block-grid-2.medium-block-grid-4.large-block-grid-5 --# %filter-selector{objects: "Enterprises.hubs | searchEnterprises:query | taxonsOf", "active-selectors" => "activeTaxons"} --# .small-12.large-3.columns --# %h5.tdhead --# .light Filter by --# Delivery --# %ul.small-block-grid-2.medium-block-grid-4.large-block-grid-2 --# %shipping-type-selector{results: "shippingTypes"} --# --# = render partial: 'shared/components/filter_box' +.row.animate-show{"ng-show" => "filtersActive"} + .small-12.columns + .row.filter-box + .small-12.large-9.columns + %h5.tdhead + .light Filter by + Type + %filter-selector.small-block-grid-2.medium-block-grid-4.large-block-grid-5{ objects: "visibleMatches | visible | taxonsOf", "active-selectors" => "activeTaxons" } + .small-12.large-3.columns + %h5.tdhead + .light Filter by + Delivery + %ul.small-block-grid-2.medium-block-grid-4.large-block-grid-2 + %shipping-type-selector{results: "shippingTypes"} + += render partial: 'shared/components/filter_box' diff --git a/app/views/home/_groups.html.haml b/app/views/home/_groups.html.haml deleted file mode 100644 index 5d76dd1e2d..0000000000 --- a/app/views/home/_groups.html.haml +++ /dev/null @@ -1,10 +0,0 @@ -#groups.pane - - .row - .small-12.columns.text-center - %h2 Groups / Regions - %h5 See all the groups & regions on the Open Food Network - %p - %a.neutral-btn.light{href: "/groups"} - %i.ofn-i_035-groups - View groups & regions \ No newline at end of file diff --git a/app/views/home/_hubs.html.haml b/app/views/home/_hubs.html.haml index 1e5151469e..40074ce0bb 100644 --- a/app/views/home/_hubs.html.haml +++ b/app/views/home/_hubs.html.haml @@ -1,22 +1,26 @@ -= inject_enterprises -#hubs.hubs{"ng-controller" => "EnterprisesCtrl"} += inject_enterprises + +#hubs.hubs{"ng-controller" => "EnterprisesCtrl", "ng-cloak" => true} .row .small-12.columns - %h1 Shop in your local area + %h1{"scroll-after-load" => (spree_current_user ? true : nil)} Shop in your local area - = render partial: "shared/components/enterprise_search" - = render partial: "home/filters" + = render "shared/components/enterprise_search" + = render "home/filters" - .row{bindonce: true} + .row .small-12.columns - .active_table - %hub.active_table_node.row.animate-repeat{"ng-repeat" => "hub in filteredEnterprises = (Enterprises.hubs | visible | searchEnterprises:query | taxons:activeTaxons | shipping:shippingTypes | showHubProfiles:show_profiles | orderBy:['-active', '+orders_close_at'])", - "ng-class" => "{'is_profile' : hub.category == 'hub_profile', 'closed' : !open(), 'open' : open(), 'inactive' : !hub.active, 'current' : current()}", - "scroll-after-load" => true, - "ng-controller" => "HubNodeCtrl", - id: "{{hub.hash}}"} - .small-12.columns - = render partial: 'home/skinny' - = render partial: 'home/fat' + .name-matches{"ng-show" => "nameMatchesFiltered.length > 0"} + %h2 Did you mean? + = render "home/hubs_table", enterprises: "nameMatches" - = render partial: 'shared/components/enterprise_no_results' + .distance-matches{"ng-if" => "nameMatchesFiltered.length == 0 || distanceMatchesShown"} + %h2{"ng-show" => "nameMatchesFiltered.length > 0 || query.length > 0"} + Closest to + %span{"ng-show" => "nameMatchesFiltered.length > 0"} {{ nameMatchesFiltered[0].name }}... + %span{"ng-hide" => "nameMatchesFiltered.length > 0"} {{ query }}... + + = render "home/hubs_table", enterprises: "distanceMatches" + + .show-distance-matches{"ng-show" => "nameMatchesFiltered.length > 0 && !distanceMatchesShown"} + %a{href: "", "ng-click" => "showDistanceMatches()"} Show me shops near {{ nameMatchesFiltered[0].name }} diff --git a/app/views/home/_hubs_table.html.haml b/app/views/home/_hubs_table.html.haml new file mode 100644 index 0000000000..8842079f56 --- /dev/null +++ b/app/views/home/_hubs_table.html.haml @@ -0,0 +1,10 @@ +.active_table + %hub.active_table_node.row{"ng-repeat" => "hub in #{enterprises}Filtered = (#{enterprises} | visible | taxons:activeTaxons | shipping:shippingTypes | showHubProfiles:show_profiles | orderBy:['-active', '+distance', '+orders_close_at'])", + "ng-class" => "{'is_profile' : hub.category == 'hub_profile', 'closed' : !open(), 'open' : open(), 'inactive' : !hub.active, 'current' : current()}", + "ng-controller" => "HubNodeCtrl", + id: "{{hub.hash}}"} + .small-12.columns + = render 'home/skinny' + = render 'home/fat' + + = render 'shared/components/enterprise_no_results', enterprises: "#{enterprises}Filtered" diff --git a/app/views/home/_map.html.haml b/app/views/home/_map.html.haml deleted file mode 100644 index 5a89dd181e..0000000000 --- a/app/views/home/_map.html.haml +++ /dev/null @@ -1,10 +0,0 @@ -#map.pane - - .row - .small-12.columns.text-center - %h2 Map - %h5 of all our food hubs and producers - %p - %a.neutral-btn.light{href: "/map"} - %i.ofn-i_037-map - View map \ No newline at end of file diff --git a/app/views/home/_producer-register.html.haml b/app/views/home/_producer-register.html.haml deleted file mode 100644 index 62845bd9ea..0000000000 --- a/app/views/home/_producer-register.html.haml +++ /dev/null @@ -1,75 +0,0 @@ -#producers.pane - - .row - .small-12.columns.text-center - %h2 - = t :producers - %h5 Want to join the Open Food Network? - %br - %a.neutral-btn.turquoise{href: "/register"} - Register now - %i.ofn-i_007-caret-right - / .row - / .small-12.medium-4.columns.text-center - / %ul.pricing-table - / %li.title Profile - / %li.price Always free - / %li.description Help people find and contact you on OFN - / %li.bullet-item - / %i.ofn-i_019-map-pin - / Pin on OFN Map - / %li.bullet-item - / %i.ofn-i_044-facebook - / Share your contact and social info - / %li.cta-button - / %a.neutral-btn.turquoise{:href => "/register"} - / Register now - / %i.ofn-i_007-caret-right - - / .small-12.medium-4.columns.text-center - / %ul.pricing-table - / %li.title Supplier - / %li.price Always free - / %li.description Sell your products through existing OFN shopfronts - / %li.bullet-item - / %i.ofn-i_019-map-pin - / Pin on OFN Map - / %li.bullet-item - / %i.ofn-i_044-facebook - / Share your contact and social info - / %li.bullet-item - / %i.ofn-i_067-shop - / Create and manage products - / %li.cta-button - / %a.neutral-btn.turquoise{:href => "/register"} - / Register now - / %i.ofn-i_007-caret-right - - / .small-12.medium-4.columns.text-center - / %ul.pricing-table - / %li.title Shopfront - / %li.price $200 setup fee - / %li.description + Sliding monthly fee of $5-$50/month - / %li.bullet-item - / %i.ofn-i_019-map-pin - / Pin on OFN Map - / %li.bullet-item - / %i.ofn-i_044-facebook - / Share your contact and social info - / %li.bullet-item - / %i.ofn-i_067-shop - / Create and manage products - / %li.bullet-item - / %i.ofn-i_051-check-big - / Create and manage order cycles - / %li.bullet-item - / %i.ofn-i_051-check-big - / Sell your products on Shopfront - / %li.cta-button - / %a.neutral-btn.turquoise{:href => "/register"} - / Register now - / %i.ofn-i_007-caret-right - - - - \ No newline at end of file diff --git a/app/views/home/_producers.html.haml b/app/views/home/_producers.html.haml deleted file mode 100644 index 3d4fcfbd68..0000000000 --- a/app/views/home/_producers.html.haml +++ /dev/null @@ -1,10 +0,0 @@ -#producers.pane - - .row - .small-12.columns.text-center - %h2 Producers - %h5 Looking for a specific producer or farmer? - %p - %a.neutral-btn.turquoise{href: "/producers"} - %i.ofn-i_036-producers - View all producers \ No newline at end of file diff --git a/app/views/home/_skinny.html.haml b/app/views/home/_skinny.html.haml index 9cf7a01c77..c7d91b676b 100644 --- a/app/views/home/_skinny.html.haml +++ b/app/views/home/_skinny.html.haml @@ -1,7 +1,6 @@ .row.active_table_row{"ng-if" => "hub.is_distributor", "ng-click" => "toggle($event)", "ng-class" => "{'closed' : !open(), 'is_distributor' : producer.is_distributor}", bindonce: true} - - .columns.small-12.medium-6.large-5.skinny-head - %a.hub{"bo-href" => "hub.path", "ng-class" => "{primary: hub.active, secondary: !hub.active}", "ofn-empties-cart" => "hub"} + .columns.small-12.medium-5.large-5.skinny-head + %a.hub{"bo-href" => "hub.path", "ng-class" => "{primary: hub.active, secondary: !hub.active}", "ofn-change-hub" => "hub", "data-is-link" => "true"} %i{bo: {class: "hub.icon_font"}} %span.margin-top.hub-name-listing{"bo-bind" => "hub.name | truncate:40"} @@ -9,23 +8,28 @@ %span.margin-top{"bo-text" => "hub.address.city"} .columns.small-2.medium-1.large-1 %span.margin-top{"bo-bind" => "hub.address.state_name | uppercase"} + %span.margin-top{"ng-if" => "hub.distance != null && hub.distance > 0"} ({{ hub.distance / 1000 | number:0 }} km) - .columns.small-6.medium-3.large-4.text-right{"bo-if" => "hub.active"} - %a.hub.open_closed{"bo-href" => "hub.path", "ng-class" => "{primary: hub.active, secondary: !hub.active}", "ofn-empties-cart" => "hub"} + .columns.small-4.medium-3.large-3.text-right{"bo-if" => "hub.active"} + %a.hub.open_closed{"bo-href" => "hub.path", "ng-class" => "{primary: hub.active, secondary: !hub.active}", "ofn-change-hub" => "hub"} %i.ofn-i_033-open-sign %span.margin-top{ bo: { if: "current()" } } %em Shopping here %span.margin-top{ bo: { if: "!current()" } } %span{"bo-bind" => "hub.orders_close_at | sensible_timeframe"} - .columns.small-6.medium-3.large-4.text-right{"bo-if" => "!hub.active"} - %a.hub.open_closed{"bo-href" => "hub.path", "ng-class" => "{primary: hub.active, secondary: !hub.active}", "ofn-empties-cart" => "hub"} + .columns.small-4.medium-3.large-3.text-right{"bo-if" => "!hub.active"} + %a.hub.open_closed{"bo-href" => "hub.path", "ng-class" => "{primary: hub.active, secondary: !hub.active}", "ofn-change-hub" => "hub"} %i.ofn-i_032-closed-sign %span.margin-top{ bo: { if: "current()" } } %em Shopping here %span.margin-top{ bo: { if: "!current()" } } Orders closed -.row.active_table_row{"ng-if" => "!hub.is_distributor", "ng-class" => "closed"} + .columns.small-2.medium-1.large-1.text-right + %span.margin-top + %i{"ng-class" => "{'ofn-i_005-caret-down' : !open(), 'ofn-i_006-caret-up' : open()}"} + +.row.active_table_row{"ng-if" => "!hub.is_distributor", "ng-class" => "closed", bindonce: true} .columns.small-12.medium-6.large-5.skinny-head %a.hub{"ng-click" => "openModal(hub)", "ng-class" => "{primary: hub.active, secondary: !hub.active}"} %i{ng: {class: "hub.icon_font"}} diff --git a/app/views/home/_stats.html.haml b/app/views/home/_stats.html.haml new file mode 100644 index 0000000000..6c686a22b7 --- /dev/null +++ b/app/views/home/_stats.html.haml @@ -0,0 +1,23 @@ +#stats.pane + .row.header + .small-12.medium-8.medium-offset-2.columns.text-center + %h2 We're creating a new food system. + + .row.content + - if ContentConfig.home_show_stats + .small-12.medium-3.columns.text-center + %h4 + %strong= number_with_delimiter @num_producers + food producers + .small-12.medium-3.columns.text-center + %h4 + %strong= number_with_delimiter @num_distributors + food shops + .small-12.medium-3.columns.text-center + %h4 + %strong= number_with_delimiter @num_users + food shoppers + .small-12.medium-3.columns.text-center + %h4 + %strong= number_with_delimiter @num_orders + food orders diff --git a/app/views/home/_system.html.haml b/app/views/home/_system.html.haml new file mode 100644 index 0000000000..ed32e305f4 --- /dev/null +++ b/app/views/home/_system.html.haml @@ -0,0 +1,23 @@ +#system.pane + .row + .small-12.medium-12.large-8.large-offset-2.columns.text-center + %h2 Here's how it works. + .row + .small-12.medium-4.columns.text-left + .home-icon-box + %a.search{href: "/shops"} + .home-icon-box-bottom + %h4 1. Search + %p.text-normal Search our diverse, independent shops for seasonal local food. Search by neighbourhood and food category, or whether you prefer delivery or pickup. + .small-12.medium-4.columns.text-left + .home-icon-box + %a.shop{href: "/shops"} + .home-icon-box-bottom + %h4 2. Shop + %p.text-normal Transform your transactions with affordable local food from diverse producers and hubs. Know the stories behind your food and the people who make it! + .small-12.medium-4.columns.text-left + .home-icon-box + %a.pick-up-delivery{href: "/shops"} + .home-icon-box-bottom + %h4 3. Pick-up / Delivery + %p.text-normal Hang on for your delivery, or visit your producer or hub for a more personal connection with your food. Food shopping as diverse as nature intended it. diff --git a/app/views/home/about_us.html.haml b/app/views/home/about_us.html.haml deleted file mode 100644 index 79e4a657a7..0000000000 --- a/app/views/home/about_us.html.haml +++ /dev/null @@ -1,8 +0,0 @@ -.row - .large-12.columns - %h2 What is open food network - %p - Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna - aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. - Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint - occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum \ No newline at end of file diff --git a/app/views/home/index.html.haml b/app/views/home/index.html.haml index a15442e794..ee4fca7b27 100644 --- a/app/views/home/index.html.haml +++ b/app/views/home/index.html.haml @@ -1,24 +1,25 @@ -#tagline - .row - .small-12.text-center.columns - %h1 - %img{src: "/assets/open-food-network-beta.png", srcset: "/assets/open-food-network-beta.svg", width: "550", height: "134", title: "Open Food Network (beta)"} - %h2 An open marketplace that makes it easy to find, buy, sell and move sustainable local food. - - %ofn-modal{title: "Learn more", "ng-cloak" => true} - = render partial: "modals/learn_more" +:css + #tagline:before { background-image: url("#{ContentConfig.home_hero.url}") } -.ng-cloak - = render partial: "home/hubs" - / = render partial: "home/map" +%div{"ng-controller" => "HomeCtrl"} + = render partial: "shared/menu/alert" - / = render partial: "home/producers" + #tagline + .row + .small-12.text-center.columns + %h1 + / TODO: Rohan - logo asset & width is content manageable: + %img{src: "/assets/logo-white-notext.png", width: "250", title: Spree::Config.site_name} + %br/ + %a.button.transparent{href: "/shops"} + Shop Now - / = render partial: "home/groups" -= render partial: "home/producer-register" + #panes + = render partial: "home/brandstory" + = render partial: "home/system" + = render partial: "home/cta" + = render partial: "home/stats" -= render partial: "home/beta" - -= render partial: "shared/footer" + = render partial: "shared/footer" diff --git a/app/views/layouts/darkswarm.html.haml b/app/views/layouts/darkswarm.html.haml index 5169efa942..33a3d6148a 100644 --- a/app/views/layouts/darkswarm.html.haml +++ b/app/views/layouts/darkswarm.html.haml @@ -3,16 +3,16 @@ %meta{charset: 'utf-8'}/ %meta{name: 'viewport', content: "width=device-width,initial-scale=1.0"}/ - %title= content_for?(:title) ? yield(:title) : 'Welcome to Open Food Network' + %title= content_for?(:title) ? "#{yield(:title)} - Open Food Network".html_safe : 'Welcome to Open Food Network' - if Rails.env.production? = favicon_link_tag - else = favicon_link_tag "/favicon-staging.ico" - %link{href: "https://fonts.googleapis.com/css?family=Open+Sans:400,700", rel: "stylesheet", type: "text/css"}/ + %link{href: "https://fonts.googleapis.com/css?family=Roboto:400,300italic,400italic,300,700,700italic|Oswald:300,400,700", rel: "stylesheet", type: "text/css"} = yield :scripts - %script{src: "//maps.googleapis.com/maps/api/js?libraries=places&sensor=false"} - = stylesheet_link_tag "darkswarm/all" + %script{src: "//maps.googleapis.com/maps/api/js?libraries=places,geometry&sensor=false"} + = split_stylesheet_link_tag "darkswarm/all" = javascript_include_tag "darkswarm/all" @@ -34,7 +34,7 @@ .off-canvas-wrap{offcanvas: true} .inner-wrap - = render partial: "shared/menu/menu" + = render "shared/menu/menu" %section{ role: "main" } = yield diff --git a/app/views/layouts/mailer.html.haml b/app/views/layouts/mailer.html.haml index 9042c91213..39f677b890 100644 --- a/app/views/layouts/mailer.html.haml +++ b/app/views/layouts/mailer.html.haml @@ -15,7 +15,7 @@ %table{:bgcolor => "#f2f2f2"} %tr %td - %img{:src => "#{ asset_path 'open-food-network-beta-black.png' }", :width => "200", :height => "49"}/ + %img{:src => "#{ asset_path 'logo-color.png' }", :width => "144", :height => "50"}/ %td{:align => "right"} %h6.collapse Open Food Network diff --git a/app/views/layouts/registration.html.haml b/app/views/layouts/registration.html.haml index d6122cdc3b..c0e0c4b7ec 100644 --- a/app/views/layouts/registration.html.haml +++ b/app/views/layouts/registration.html.haml @@ -3,12 +3,12 @@ %meta{charset: 'utf-8'}/ %meta{name: 'viewport', content: "width=device-width,initial-scale=1.0"}/ - %title= content_for?(:title) ? yield(:title) : 'Welcome to Open Food Network' + %title= content_for?(:title) ? "#{yield(:title)} - Open Food Network" : 'Welcome to Open Food Network' - if Rails.env.production? = favicon_link_tag - else = favicon_link_tag "/favicon-staging.ico" - %link{href: "https://fonts.googleapis.com/css?family=Open+Sans:400,700", rel: "stylesheet", type: "text/css"}/ + %link{href: "https://fonts.googleapis.com/css?family=Roboto:400,300italic,400italic,300,700,700italic|Oswald:300,400,700", rel: "stylesheet", type: "text/css"} = yield :scripts %script{src: "//maps.googleapis.com/maps/api/js?libraries=places&sensor=false"} @@ -19,7 +19,7 @@ = render "layouts/bugherd_script" = csrf_meta_tags - %body.off-canvas{"ng-app" => "Darkswarm", style: 'background-image: url("/assets/home/ofn_bg_1.jpg")' } + %body.off-canvas{"ng-app" => "Darkswarm", style: 'background-image: url("/assets/tile-wide.png")' } / [if lte IE 8] = render partial: "shared/ie_warning" = javascript_include_tag "iehack" diff --git a/app/views/map/index.html.haml b/app/views/map/index.html.haml index ce4052be57..30df47e28c 100644 --- a/app/views/map/index.html.haml +++ b/app/views/map/index.html.haml @@ -1,8 +1,11 @@ -= inject_enterprises +- content_for(:title) do + Map + += inject_enterprises .map-container{"fill-vertical" => true} %map{"ng-controller" => "MapCtrl"} %google-map{options: "map.additional_options", center: "map.center", zoom: "map.zoom", styles: "map.styles", draggable: "true"} %map-search - %markers{models: "OfnMap.enterprises", fit: "true", - coords: "'self'", icon: "'icon'", click: "'reveal'"} + %markers{models: "OfnMap.enterprises", fit: "true", + coords: "'self'", icon: "'icon'", click: "'reveal'"} diff --git a/app/views/producers/_fat.html.haml b/app/views/producers/_fat.html.haml index 99099a31f8..06d73666af 100644 --- a/app/views/producers/_fat.html.haml +++ b/app/views/producers/_fat.html.haml @@ -1,5 +1,5 @@ .row.active_table_row{"ng-if" => "open()", "ng-click" => "toggle($event)", "ng-class" => "{'open' : !ofn-i_032-closed-sign()}"} - + .columns.small-12.medium-7.large-7.fat / Will add in long description available once clean up HTML formatting producer.long_description %div{"bo-if" => "producer.description"} @@ -10,23 +10,23 @@ %label .columns.small-12.medium-5.large-5.fat - + %div{"bo-if" => "producer.supplied_taxons"} %label Shop for %p.trans-sentence %span.fat-taxons{"ng-repeat" => "taxon in producer.supplied_taxons"} %render-svg{path: "{{taxon.icon}}"} %span{"bo-text" => "taxon.name"} - + %div.show-for-medium-up{"ng-if" => "producer.supplied_taxons.length==0"} %div{"bo-if" => "producer.email || producer.website || producer.phone"} %label Contact - + %p.word-wrap{"bo-if" => "producer.phone"} - Call - %span{"bo-text" => "producer.phone"} + Call + %span{"bo-text" => "producer.phone"} %p.word-wrap{"bo-if" => "producer.email"} %a{"bo-href" => "producer.email | stripUrl", target: "_blank", mailto: true} @@ -39,20 +39,20 @@ %div{"bo-if" => "producer.twitter || producer.facebook || producer.linkedin || producer.instagram"} %label Follow .follow-icons{bindonce: true} - %span{"bo-if" => "producer.twitter"} + %span{"bo-if" => "producer.twitter"} %a{"bo-href-i" => "http://twitter.com/{{producer.twitter}}", target: "_blank"} %i.ofn-i_041-twitter - + %span{"bo-if" => "producer.facebook"} %a{"bo-href-i" => "http://{{producer.facebook | stripUrl}}", target: "_blank"} %i.ofn-i_044-facebook - + %span{"bo-if" => "producer.linkedin"} %a{"bo-href-i" => "http://{{producer.linkedin | stripUrl}}", target: "_blank"} %i.ofn-i_042-linkedin - + %span{"bo-if" => "producer.instagram"} - %a{"bo-href-i" => "http://instagram.com/{{producer.instagram}}", target: "_blank"} + %a{"bo-href-i" => "http://instagram.com/{{producer.instagram}}", target: "_blank"} %i.ofn-i_043-instagram .row.active_table_row.pad-top{"ng-if" => "open()", "bo-if" => "producer.hubs"} @@ -60,19 +60,18 @@ .row .columns.small-12.fat %div{"bo-if" => "producer.name"} - %label + %label Shop for - %span.turquoise{"bo-text" => "producer.name"} + %span.turquoise{"bo-text" => "producer.name"} products at: %div.show-for-medium-up{"bo-if" => "!producer.name"} .row.cta-container .columns.small-12 - %a.cta-hub{"ng-repeat" => "hub in producer.hubs | orderBy:'-active'", - "bo-href" => "hub.path", "ofn-empties-cart" => "hub", + %a.cta-hub{"ng-repeat" => "hub in producer.hubs | visible | orderBy:'-active'", + "bo-href" => "hub.path", "ofn-change-hub" => "hub", "bo-class" => "{primary: hub.active, secondary: !hub.active}"} %i.ofn-i_033-open-sign{"bo-if" => "hub.active"} %i.ofn-i_032-closed-sign{"bo-if" => "!hub.active"} .hub-name{"bo-text" => "hub.name"} .button-address{"bo-bind" => "[hub.address.city, hub.address.state_name] | printArray"} - diff --git a/app/views/producers/_filters.html.haml b/app/views/producers/_filters.html.haml index ec23c05b92..103b31d531 100644 --- a/app/views/producers/_filters.html.haml +++ b/app/views/producers/_filters.html.haml @@ -1,15 +1,14 @@ --# .row --# = render partial: 'shared/components/filter_controls' --# .small-12.medium-6.columns.text-right --# --# --# .row.animate-show{"ng-show" => "filtersActive"} --# .small-12.columns --# .row.filter-box --# .small-12.columns --# %h5.tdhead --# .light Filter by --# Type --# %ul.small-block-grid-2.medium-block-grid-4.large-block-grid-6 --# %filter-selector{objects: "Enterprises.producers | searchEnterprises:query | taxonsOf", "active-selectors" => "activeTaxons"} --# = render partial: 'shared/components/filter_box' +.row + = render partial: 'shared/components/filter_controls' + .small-12.medium-6.columns.text-right + + +.row.animate-show{"ng-show" => "filtersActive"} + .small-12.columns + .row.filter-box + .small-12.columns + %h5.tdhead + .light Filter by + Type + %filter-selector.small-block-grid-2.medium-block-grid-4.large-block-grid-6{objects: "Enterprises.producers | searchEnterprises:query | taxonsOf", "active-selectors" => "activeTaxons"} + = render partial: 'shared/components/filter_box' diff --git a/app/views/producers/index.html.haml b/app/views/producers/index.html.haml index e3ae9815e2..cb9c3d3826 100644 --- a/app/views/producers/index.html.haml +++ b/app/views/producers/index.html.haml @@ -1,6 +1,9 @@ +- content_for(:title) do + Producers + = inject_enterprises -.producers.pad-top{"ng-controller" => "EnterprisesCtrl", "ng-cloak" => true} +.producers{"ng-controller" => "EnterprisesCtrl", "ng-cloak" => true} .row .small-12.columns.pad-top %h1 Find local producers @@ -12,7 +15,6 @@ .small-12.columns .active_table %producer.active_table_node.row.animate-repeat{id: "{{producer.path}}", - "scroll-after-load" => true, "ng-repeat" => "producer in filteredEnterprises = (Enterprises.producers | visible | searchEnterprises:query | taxons:activeTaxons)", "ng-controller" => "ProducerNodeCtrl", "ng-class" => "{'closed' : !open(), 'open' : open(), 'inactive' : !producer.active}", diff --git a/app/views/producers/signup.html.haml b/app/views/producers/signup.html.haml new file mode 100644 index 0000000000..169d4b179f --- /dev/null +++ b/app/views/producers/signup.html.haml @@ -0,0 +1,46 @@ +- content_for(:title) do + Sign up as a producer + +#panes + #producer-signup.pane + .row.header + .small-12.medium-12.columns.text-center + %h2 Food producers, empowered. + .row.content + .small-12.medium-6.medium-offset-3.columns.text-center + %p.text-big Sell your food and tell your stories to diverse new markets. Save time and money on every overhead. We support innovation without the risk. We've levelled the playing field. + %br + %a.button.transparent{href: "/register"} + Join now + .pane + .row + .small-12.medium-10.medium-offset-1.columns.text-center + %h2 Enterprise Accounts + -# %p.text-big + -# If there is a time-sensitive offer you can write it here, e.g. + -# Sign up before 30th June for an extra month free! + %br + = ContentConfig.producer_signup_pricing_table_html.html_safe + + #producer-case-studies + .row + .small-12.medium-10.medium-offset-1.columns + %h2.text-center Stories from our producers. + %br + = ContentConfig.producer_signup_case_studies_html.html_safe + + .pane#cta + .row + .small-12.medium-6.medium-offset-3.columns.text-center + %h2 Join now! + %p.text-big Start with a free profile, and expand when you're ready! + %a.button.transparent{href: "/register"} + Join now + + #producer-details.pane.footer-pad + .row + .small-12.medium-10.medium-offset-1.columns + %h2.text-center Here's the detail. + = ContentConfig.producer_signup_detail_html.html_safe + += render partial: "shared/footer" diff --git a/app/views/registration/index.html.haml b/app/views/registration/index.html.haml index d110e98657..a05c383395 100644 --- a/app/views/registration/index.html.haml +++ b/app/views/registration/index.html.haml @@ -1,3 +1,6 @@ +- content_for(:title) do + Register + = inject_spree_api_key = inject_available_countries = inject_enterprise_attributes diff --git a/app/views/shared/_case_study.html.haml b/app/views/shared/_case_study.html.haml new file mode 100644 index 0000000000..12e59c6b6c --- /dev/null +++ b/app/views/shared/_case_study.html.haml @@ -0,0 +1,6 @@ +.case-study + %img.case-study-img{src: img_src, width: "100", height: "100", title: title} + %h4= title + %p.text-small= description + %a{href: link, target: "_blank"} + %strong More diff --git a/app/views/shared/_footer.html.haml b/app/views/shared/_footer.html.haml index e06ae0f0aa..7450966e5f 100644 --- a/app/views/shared/_footer.html.haml +++ b/app/views/shared/_footer.html.haml @@ -1,60 +1,136 @@ %footer - .row.landing-page-row - .small-12.medium-4.columns.text-left - %h4 Email us - %p - %a{href: "hello@openfoodnetwork.org".reverse, target: '_blank', mailto: true} - %span.email - = "hello@openfoodnetwork.org".reverse - %h4 Follow us - %p - %a{title:'Follow us on Facebook', href: 'https://www.facebook.com/OpenFoodNet', target: '_blank'} - %i.ofn-i_044-facebook - Facebook - %a{title:'Follow us on Twitter', href: 'https://twitter.com/OpenFoodNet', target: '_blank'} - %i.ofn-i_041-twitter - Twitter - %a{title:'Join our group on LinkedIn', href: 'http://www.linkedin.com/groups/Open-Food-Foundation-4743336', target: '_blank'} - %i.ofn-i_042-linkedin - LinkedIn - .small-12.medium-3.columns.text-left - %h4 Getting around - %ul.bullet-list - %li - %a{href: "/shop"} Shop - %li - %a{href: "/map"} Map - %li - %a{href: "/producers"} Producers - %li - %a{href: "/groups"} Groups - .small-12.medium-2.columns.text-left - %h4 Producers - %p - = t :producers_join - %p - %a{href: "/register"} Register now - .small-12.medium-3.columns.text-left - %h4 About us - %p OFN is a network of independent online food stores that connect farmers and food hubs with individuals and local businesses. It gives farmers and food hubs an easier and fairer way to distribute their food. - .row.landing-page-row - .small-12.columns.text-center.pad-top - %hr - %h5.pad-top - %a{title: 'Open Food Network', href:'http://www.openfoodnetwork.org', target: '_blank' } openfoodnetwork.org - %br - © Copyright - = Date.today.year - Open Food Foundation - %p - %small - %a{href:"https://creativecommons.org/licenses/by-sa/3.0/", target: "_blank" } Creative Commons Attribution-ShareAlike 3.0 Unported (CC BY-SA 3.0) - %p - %small - %a{href:"/Terms-of-service.pdf", target: "_blank" } Site terms & conditions - | - %a{href:"https://github.com/openfoodfoundation/openfoodnetwork", target: "_blank" } Open Source & developer info on GitHub + .footer-global + .row + .small-12.columns.text-center + .logo + %img{src: "/assets/logo-white-notext.png", width: "120px"} + .row + .small-12.medium-8.medium-offset-2.columns.text-center + .alert-box + %a.big-alert{href: "http://www.openfoodnetwork.org", target: "_blank"} + %h6 + Interested in selling food on the Open Food Network? + %strong Start here + %i.ofn-i_054-point-right + .row + .small-12.medium-4.medium-offset-2.columns.text-center + %h6 OFN Global + %p + %a{href: "http://www.openfoodnetwork.org", target: "_blank"} Home + %span | + %a{href: "http://www.openfoodnetwork.org/news/", target: "_blank"} News + %span | + %a{href: "http://www.openfoodnetwork.org/about/history-team/", target: "_blank"} About + %span | + %a{href: "http://www.openfoodnetwork.org/contact/", target: "_blank"} Contact - // To be added when Guy's pretty landing page is up: - //| - //%a{href:'' } Developers + + .small-12.medium-4.columns.text-center + %h6 OFN Sites + %p + %a{href: "http://dev.openfoodnetwork.org", target: "_blank"} Developer + %span | + %a{href: "http://community.openfoodnetwork.org", target: "_blank"} Community + %span | + %a{href: "http://www.openfoodnetwork.org/platform/user-guide/", target: "_blank"} User Guide + + .medium-2.columns.text-center + / Placeholder + + .footer-local + .row + .small-12.medium-2.medium-offset-2.columns.text-center + %p.secure-icon + %i.ofn-i_017-locked + .small-12.medium-6.columns.text-center + %p.text-big.secure-text Secure and trusted. + %p.secure-text Open Food Network uses SSL encryption (2048 bit RSA) everywhere to keep your shopping and payment information private. Our servers do not store your credit card details and payments are processed by PCI-compliant services. + .small-12.medium-2.columns + + .row + .small-12.medium-8.medium-offset-2.columns.text-center + %hr.hr-light + %br + + .row + .small-6.medium-3.medium-offset-2.columns.text-left + // This is the instance-managed set of links: + %h4 Keep in touch + %p.social-icons + - if ContentConfig.footer_facebook_url.present? + %a{href: ContentConfig.footer_facebook_url} + %i.ofn-i_044-facebook + - if ContentConfig.footer_twitter_url.present? + %a{href: ContentConfig.footer_twitter_url} + %i.ofn-i_041-twitter + - if ContentConfig.footer_instagram_url.present? + %a{href: ContentConfig.footer_instagram_url} + %i.ofn-i_043-instagram + - if ContentConfig.footer_linkedin_url.present? + %a{href: ContentConfig.footer_linkedin_url} + %i.ofn-i_042-linkedin + - if ContentConfig.footer_googleplus_url.present? + %a{href: ContentConfig.footer_googleplus_url} + %i.ofn-i_046-g + - if ContentConfig.footer_pinterest_url.present? + %a{href: ContentConfig.footer_pinterest_url} + %i.ofn-i_045-pintrest + - if ContentConfig.footer_email.present? + %p + %a{href: ContentConfig.footer_email.reverse, mailto: true, target: '_blank'} Email us + = render_markdown(ContentConfig.footer_links_md).html_safe + + + .small-6.medium-3.columns.text-left + %h4 Navigate + %p + %a{href: "/shops"} Shops + %p + %a{href: "/map"} Map + %p + %a{href: "/producers"} Producers + %p + %a{href: "/groups"} Groups + %p + %a{href: ContentConfig.footer_about_url} About + + .small-12.medium-2.columns.text-left + %h4 Join us + %p + %a{href: "/producers/signup"} Producers sign-up + %p + %a{href: "/shops/signup"} Hubs sign-up + %p + %a{href: "/groups/signup"} Groups sign-up + %p + %a{href: "http://www.openfoodnetwork.org/platform/food-system-partners/", target: "_blank"} Food systems partners + + .medium-2.columns.text-center + / Placeholder + + .row + .small-12.medium-8.medium-offset-2.columns.text-center + %hr.hr-light + %br + + .row + .small-12.medium-3.medium-offset-2.columns.text-left + %a{href: root_path} + %img{src: ContentConfig.footer_logo.url, width: "220"} + .small-12.medium-5.columns.text-left + %p.text-small + Read our + %a{href: ContentConfig.footer_tos_url} Terms & conditions + | + Find us on + %a{href:"https://github.com/openfoodfoundation/openfoodnetwork", target: "_blank"} Github + %p.text-small + Open Food Network is a free and open source software platform. Our content is licensed with + = succeed ',' do + %a{href:"https://creativecommons.org/licenses/by-sa/3.0/", target: "_blank" } CC BY-SA 3.0 + and our code with + = succeed '.' do + %a{href:"https://tldrlegal.com/license/gnu-affero-general-public-license-v3-(agpl-3.0)", target: "_blank" } AGPL 3 + + .medium-2.columns.text-center + / Placeholder diff --git a/app/views/shared/components/_enterprise_no_results.html.haml b/app/views/shared/components/_enterprise_no_results.html.haml index 2e6edd0875..1c8af802a2 100644 --- a/app/views/shared/components/_enterprise_no_results.html.haml +++ b/app/views/shared/components/_enterprise_no_results.html.haml @@ -1,4 +1,5 @@ -%producer.row{"ng-show" => "filteredEnterprises.length == 0"} +- enterprises ||= 'filteredEnterprises' +%producer.row{"ng-show" => "#{enterprises}.length == 0"} %p.no-results Sorry, no results found for %strong {{query}}. diff --git a/app/views/shared/components/_enterprise_search.html.haml b/app/views/shared/components/_enterprise_search.html.haml index 62dcec3ea6..4bb7ca446b 100644 --- a/app/views/shared/components/_enterprise_search.html.haml +++ b/app/views/shared/components/_enterprise_search.html.haml @@ -3,5 +3,5 @@ %input{type: :text, "ng-model" => "query", placeholder: t('search_by_name'), - "ng-debounce" => "150", + "ng-debounce" => "500", "ofn-disable-enter" => true} diff --git a/app/views/shared/menu/_alert.html.haml b/app/views/shared/menu/_alert.html.haml new file mode 100644 index 0000000000..3ce7da91f1 --- /dev/null +++ b/app/views/shared/menu/_alert.html.haml @@ -0,0 +1,9 @@ +.text-center.page-alert.fixed{ "ofn-page-alert" => true } + .alert-box + %a.alert-cta{href: "http://www.openfoodnetwork.org", target: "_blank"} + %h6 + Interested in selling food on the Open Food Network? + %strong + Start here + %i.ofn-i_054-point-right + %a.close{ ng: { click: "close()" } } × diff --git a/app/views/shared/menu/_cart.html.haml b/app/views/shared/menu/_cart.html.haml index f8287c8f61..f4d3e00c67 100644 --- a/app/views/shared/menu/_cart.html.haml +++ b/app/views/shared/menu/_cart.html.haml @@ -9,7 +9,11 @@ .joyride-tip-guide{"ng-class" => "{ in: open }", "ng-show" => "open"} %span.joyride-nub.top .joyride-content-wrapper - %h5 Your shopping cart + %h5.text-left Your shopping cart + .buttons.text-right + %a.button.secondary.tiny.add_to_cart{ href: cart_path, type: :submit, "ng-disabled" => "Cart.dirty || Cart.empty()", "ng-class" => "{ dirty: Cart.dirty }" } + {{ Cart.dirty ? 'Updating cart...' : (Cart.empty() ? 'Cart empty' : 'Edit your cart' ) }} + %a.button.primary.tiny{href: checkout_path, "ng-disabled" => "Cart.dirty || Cart.empty()"} Checkout now %table %tr.product-cart{"ng-repeat" => "line_item in Cart.line_items_present()", "ng-controller" => "LineItemCtrl", "id" => "cart-variant-{{ line_item.variant.id }}"} diff --git a/app/views/shared/menu/_large_menu.html.haml b/app/views/shared/menu/_large_menu.html.haml index a12d0030e0..4f71e1c93e 100644 --- a/app/views/shared/menu/_large_menu.html.haml +++ b/app/views/shared/menu/_large_menu.html.haml @@ -1,43 +1,33 @@ %nav.top-bar.show-for-large-up{'data-topbar' => true} %section.top-bar-section - %ul.left{} + %ul.left %li.ofn-logo - %a{href: root_path} - %img{src: "/assets/open-food-network-beta.png", srcset: "/assets/open-food-network-beta.svg", width: "110", height: "26"} - %li.divider - - if current_page? root_path - %li - %a{"ofn-scroll-to" => "hubs"} - %span.nav-primary Hubs - - else - %li - %a{href: root_path + "#/#hubs"} - %span.nav-primary Hubs - %li.divider + %a{href: root_path} + %img{src: ContentConfig.logo.url, width: "250", height: "51"} + %ul.center + %li + %a{href: main_app.shops_path} + %span.nav-primary Shops %li %a{href: main_app.map_path} %span.nav-primary Map - %li.divider %li %a{href: main_app.producers_path} %span.nav-primary Producers - %li.divider %li %a{href: main_app.groups_path} %span.nav-primary Groups - %li.divider - %section.top-bar-section + %li + %a{href: ContentConfig.footer_about_url} + %span.nav-primary About %ul.right - %li.divider - if spree_current_user.nil? = render 'shared/signed_out' - else = render 'shared/signed_in' - %li.divider %li.current_hub{"ng-controller" => "CurrentHubCtrl", "ng-show" => "CurrentHub.hub.id", "ng-cloak" => true} %a{href: main_app.shop_path} - %em Shopping @ - %span.nav-primary.nav-branded {{ CurrentHub.hub.name }} - %li.divider + %em Shopping @ + %span.nav-primary.nav-branded {{ CurrentHub.hub.name | truncate:25 }} %li.cart{"ng-cloak" => true} = render partial: "shared/menu/cart" diff --git a/app/views/shared/menu/_mobile_menu.html.haml b/app/views/shared/menu/_mobile_menu.html.haml index e5ba9af83b..d43c95ca46 100644 --- a/app/views/shared/menu/_mobile_menu.html.haml +++ b/app/views/shared/menu/_mobile_menu.html.haml @@ -2,6 +2,12 @@ %section.left %a.left-off-canvas-toggle.menu-icon %span + + %section.left + .ofn-logo + %a{href: root_path} + %img{src: ContentConfig.logo_mobile.url, srcset: ContentConfig.logo_mobile_svg.url, width: "75", height: "26"} + %section.right{"ng-cloak" => true} .cart = render partial: "shared/menu/cart" @@ -12,20 +18,18 @@ %ul.off-canvas-list %li.ofn-logo %a{href: root_path} - %img{src: "/assets/open-food-network-beta.png", srcset: "/assets/open-food-network-beta.svg", width: "110", height: "26"} - - - if current_page? root_path - %li.li-menu + %img{src: ContentConfig.logo_mobile.url, srcset: ContentConfig.logo_mobile_svg.url, width: "75", height: "26"} + %li.li-menu + - if current_page? main_app.shops_path %a{"ofn-scroll-to" => "hubs"} %span.nav-primary - %i.ofn-i_040-hub - Hubs - - else - %li.li-menu - %a{href: root_path + "#/#hubs"} + %i.ofn-i_019-map-pin + Shops + - else + %a{href: main_app.shops_path} %span.nav-primary - %i.ofn-i_040-hub - Hubs + %i.ofn-i_019-map-pin + Shops %li.li-menu %a{href: main_app.map_path} %span.nav-primary @@ -41,6 +45,11 @@ %span.nav-primary %i.ofn-i_035-groups Groups + %li.li-menu + %a{href: ContentConfig.footer_about_url} + %span.nav-primary + %i.ofn-i_013-help + About %li - if spree_current_user.nil? diff --git a/app/views/shop/products/_form.html.haml b/app/views/shop/products/_form.html.haml index bdaded0f0d..527ecbdd94 100644 --- a/app/views/shop/products/_form.html.haml +++ b/app/views/shop/products/_form.html.haml @@ -1,57 +1,58 @@ -%products.small-12.columns{"ng-controller" => "ProductsCtrl", "ng-show" => "order_cycle.order_cycle_id != null", "ng-cloak" => true, -"infinite-scroll" => "incrementLimit()", "infinite-scroll-distance" => "1"} +.footer-pad.small-12.columns + %products{"ng-controller" => "ProductsCtrl", "ng-show" => "order_cycle.order_cycle_id != null", "ng-cloak" => true, + "infinite-scroll" => "incrementLimit()", "infinite-scroll-distance" => "1"} - // TODO: Needs an ng-show to slide content down - .row.animate-slide{ "ng-show" => "query || appliedPropertiesList() || appliedTaxonsList()" } - .small-12.columns - .alert-box.search-alert.ng-scope - %a.right{"ng-click" => "clearAll()"} - Clear all - %i.ofn-i_009-close - %span.filter-label - Showing: - %span.applied-properties - {{ appliedPropertiesList() }} - %span.applied-taxons - {{ appliedTaxonsList() }} - %span{ ng: { hide: "!query"} } - %span{ "ng-show" => "appliedPropertiesList() || appliedTaxonsList()" } - with - %span.applied-search "{{ query }}" - .row - .small-12.medium-6.large-5.columns - %input#search.text{"ng-model" => "query", - placeholder: "Search by product or producer", - "ng-debounce" => "100", - "ofn-disable-enter" => true} + // TODO: Needs an ng-show to slide content down + .row.animate-slide{ "ng-show" => "query || appliedPropertiesList() || appliedTaxonsList()" } + .small-12.columns + .alert-box.search-alert.ng-scope + %a.right{"ng-click" => "clearAll()"} + Clear all + %i.ofn-i_009-close + %span.filter-label + Showing: + %span.applied-properties + {{ appliedPropertiesList() }} + %span.applied-taxons + {{ appliedTaxonsList() }} + %span{ ng: { hide: "!query"} } + %span{ "ng-show" => "appliedPropertiesList() || appliedTaxonsList()" } + with + %span.applied-search "{{ query }}" + .row + .small-12.medium-6.large-5.columns + %input#search.text{"ng-model" => "query", + placeholder: "Search by product or producer", + "ng-debounce" => "100", + "ofn-disable-enter" => true} - .small-12.medium-6.large-6.large-offset-1.columns - = render partial: "shop/products/filters" + .small-12.medium-6.large-6.large-offset-1.columns + = render partial: "shop/products/filters" - %div.pad-top{bindonce: true} - %product.animate-repeat{"ng-controller" => "ProductNodeCtrl", - "ng-repeat" => "product in filteredProducts = (Products.products | products:query | taxons:activeTaxons | properties: activeProperties) track by product.id ", "id" => "product-{{ product.id }}"} - = render partial: "shop/products/summary" - %shop-variant{variant: 'product.master', "bo-if" => "!product.hasVariants", "id" => "variant-{{ product.master.id }}"} - %shop-variant{variant: 'variant', "ng-repeat" => "variant in product.variants track by variant.id", "id" => "variant-{{ variant.id }}"} + %div.pad-top{bindonce: true} + %product.animate-repeat{"ng-controller" => "ProductNodeCtrl", + "ng-repeat" => "product in filteredProducts = (Products.products | products:query | taxons:activeTaxons | properties: activeProperties) track by product.id ", "id" => "product-{{ product.id }}"} + = render partial: "shop/products/summary" + %shop-variant{variant: 'product.master', "bo-if" => "!product.hasVariants", "id" => "variant-{{ product.master.id }}"} + %shop-variant{variant: 'variant', "ng-repeat" => "variant in product.variants track by variant.id", "id" => "variant-{{ variant.id }}"} - %product{"ng-show" => "Products.loading"} - .row.summary - .small-12.columns.text-center - Loading products... - .row - .small-12.columns.text-center - %img.spinner{ src: "/assets/spinning-circles.svg" } + %product{"ng-show" => "Products.loading"} + .row.summary + .small-12.columns.text-center + Loading products... + .row + .small-12.columns.text-center + %img.spinner{ src: "/assets/spinning-circles.svg" } - %div{"ng-show" => "filteredProducts.length == 0 && !Products.loading"} - .row.summary - .small-12.columns - %p.no-results - Sorry, no results found for - %strong {{query}}. - Try another search? - .row - .small-12.columns - %form{action: cart_path} - %i.ofn-i_011-spinner.cart-spinner{"ng-show" => "Cart.dirty"} - %input.small.button.primary.right.add_to_cart{type: :submit, value: "{{ Cart.dirty ? 'Updating cart...' : (Cart.empty() ? 'Cart empty' : 'Edit your cart' ) }}", "ng-disabled" => "Cart.dirty || Cart.empty()", "ng-class" => "{ dirty: Cart.dirty }" } + %div{"ng-show" => "filteredProducts.length == 0 && !Products.loading"} + .row.summary + .small-12.columns + %p.no-results + Sorry, no results found for + %strong {{query}}. + Try another search? + .row + .small-12.columns + %form{action: cart_path} + %i.ofn-i_011-spinner.cart-spinner{"ng-show" => "Cart.dirty"} + %input.small.button.primary.right.add_to_cart{type: :submit, value: "{{ Cart.dirty ? 'Updating cart...' : (Cart.empty() ? 'Cart empty' : 'Edit your cart' ) }}", "ng-disabled" => "Cart.dirty || Cart.empty()", "ng-class" => "{ dirty: Cart.dirty }" } diff --git a/app/views/shops/index.html.haml b/app/views/shops/index.html.haml new file mode 100644 index 0000000000..40a42da1ea --- /dev/null +++ b/app/views/shops/index.html.haml @@ -0,0 +1,12 @@ +- content_for(:title) do + Shops + +#panes + #shops.pane + .row + .small-12.medium-6.medium-offset-3.columns.text-center + %h2 Shopping, transformed. + %p.text-big Food grows in cycles, farmers harvest in cycles, and we order food in cycles. If you find an order cycle closed, check back soon. + += render partial: "home/hubs" += render partial: "shared/footer" diff --git a/app/views/shops/signup.html.haml b/app/views/shops/signup.html.haml new file mode 100644 index 0000000000..a04f7d82e2 --- /dev/null +++ b/app/views/shops/signup.html.haml @@ -0,0 +1,48 @@ +- content_for(:title) do + Sign up as a hub + +#panes + #shops-signup.pane + .row.header + .small-12.medium-12.columns.text-center + %h2 Food hubs, unlimited. + .row.content + .small-12.medium-6.medium-offset-3.columns.text-center + %p.text-big Whatever your model, we support you. However you change, we're with you. We're non-profit, independent, and open-sourced. We're the software partners you've dreamed of. + %br + %a.button.transparent{href: "/register"} + Join now + .pane + .row + .small-12.medium-10.medium-offset-1.columns.text-center + %h2 Enterprise Accounts + -# %p.text-big + -# If there is a time-sensitive offer you can write it here, e.g. + -# Sign up before 30th June for an extra month free! + %br + = ContentConfig.hub_signup_pricing_table_html.html_safe + + #shops-case-studies + .row + .small-12.medium-10.medium-offset-1.columns + %h2.text-center Stories from our hubs. + %br + = ContentConfig.hub_signup_case_studies_html.html_safe + + .pane#cta + .row + .small-12.medium-6.medium-offset-3.columns.text-center + %h2 We're ready to help. + %p.text-big You need a better return. You need new buyers and logistics partners. You need your story told across wholesale, retail, and the kitchen table. + %br + %a.button.transparent{href: "/register"} + Join now + + #hub-details.pane.footer-pad + .row + .small-12.medium-10.medium-offset-1.columns + %h2.text-center Here's the detail. + = ContentConfig.hub_signup_detail_html.html_safe + + += render partial: "shared/footer" diff --git a/app/views/spree/admin/orders/bulk_management.html.haml b/app/views/spree/admin/orders/bulk_management.html.haml index 71134e38a9..0e4c69b695 100644 --- a/app/views/spree/admin/orders/bulk_management.html.haml +++ b/app/views/spree/admin/orders/bulk_management.html.haml @@ -126,7 +126,7 @@ %a{ :href => '', 'ng-click' => "predicate = 'units_variant.full_name'; reverse = !reverse" } Product: Unit %th.quantity{ 'ng-show' => 'columns.quantity.visible' } Quantity %th.max{ 'ng-show' => 'columns.max.visible' } Max - %th.unit_value{ 'ng-show' => 'columns.unit_value.visible' } Weight/Volume + %th.final_weight_volume{ 'ng-show' => 'columns.final_weight_volume.visible' } Weight/Volume %th.price{ 'ng-show' => 'columns.price.visible' } Price %th.actions %th.actions @@ -146,12 +146,12 @@ %td.variant{ 'ng-show' => 'columns.variant.visible' } %a{ :href => '#', 'ng-click' => "setSelectedUnitsVariant(line_item.units_product,line_item.units_variant)" } {{ line_item.units_variant.full_name }} %td.quantity{ 'ng-show' => 'columns.quantity.visible' } - %input{ :type => 'number', :name => 'quantity', 'ng-model' => "line_item.quantity", 'obj-for-update' => "line_item", "attr-for-update" => "quantity" } + %input{ :type => 'number', :name => 'quantity', :id => 'quantity', :value => 'line_item.quantity', 'ng-model' => "line_item.quantity", 'ng-change' => "updateOnQuantity(line_item, {{ line_item.quantity }})", 'obj-for-update' => "line_item", "attr-for-update" => "quantity" } %td.max{ 'ng-show' => 'columns.max.visible' } {{ line_item.max_quantity }} - %td.unit_value{ 'ng-show' => 'columns.unit_value.visible' } - %input{ :type => 'number', :name => 'unit_value', :id => 'unit_value', 'ng-model' => "line_item.unit_value", 'ng-readonly' => "unitValueLessThanZero(line_item)", 'ng-change' => "weightAdjustedPrice(line_item, {{ line_item.unit_value }})", 'obj-for-update' => "line_item", "attr-for-update" => "unit_value" } + %td.final_weight_volume{ 'ng-show' => 'columns.final_weight_volume.visible' } + %input{ :type => 'text', :name => 'final_weight_volume', :id => 'final_weight_volume', :value => 'line_item.final_weight_volume', 'ng-model' => "line_item.final_weight_volume", 'ng-readonly' => "unitValueLessThanZero(line_item)", 'ng-change' => "weightAdjustedPrice(line_item, {{ line_item.final_weight_volume }})", 'obj-for-update' => "line_item", "attr-for-update" => "final_weight_volume" } %td.price{ 'ng-show' => 'columns.price.visible' } - %input{ :type => 'text', :name => 'price', :id => 'price', :value => '{{ line_item.price | currency }}', 'ng-readonly' => "true", 'obj-for-update' => "line_item", "attr-for-update" => "price" } + %input{ :type => 'text', :name => 'price', :id => 'price', :value => '{{ line_item.price * line_item.quantity | currency }}', 'ng-readonly' => "true", 'obj-for-update' => "line_item", "attr-for-update" => "price" } %td.actions %a{ :class => "edit-order icon-edit no-text", 'ofn-confirm-link-path' => "/admin/orders/{{line_item.order.number}}/edit" } %td.actions diff --git a/app/views/spree/admin/overview/_change_type_form.html.haml b/app/views/spree/admin/overview/_change_type_form.html.haml deleted file mode 100644 index 530d870307..0000000000 --- a/app/views/spree/admin/overview/_change_type_form.html.haml +++ /dev/null @@ -1,60 +0,0 @@ -= admin_inject_enterprise - -= form_for @enterprise, url: main_app.set_sells_admin_enterprise_path(@enterprise), - html: { name: "change_type", id: "change_type", novalidate: true, "ng-app" => "admin.enterprises", "ng-controller"=> 'changeTypeFormCtrl' } do |change_type_form| - -# Have to use hidden:'true' on this input rather than type:'hidden' as the latter seems to break ngPattern and therefore validation - %input{ hidden: "true", name: "sells", ng: { required: true, pattern: "/^(none|own)$/", model: 'sells', value: "sells"} } - - .row - .options.sixteen.columns.alpha - - if @enterprise.is_primary_producer - %input{ type: 'checkbox', hidden: true, name: "producer_profile_only", ng: { required: true, model: 'producer_profile_only', value: "producer_profile_only"} } - .basic_producer.option.one-third.column.alpha - %a.full-width.button.selector{ ng: { click: "sells='none';producer_profile_only=true;", class: "{selected: sells=='none' && producer_profile_only==true}" } } - .top - %h3 Producer Profile - %p Connect through OFN - .bottom ALWAYS FREE - %p.description - You want to use Open Food Network as a place for people to find and contact you. - - .producer_shop.option.one-third.column - %a.full-width.button.selector{ ng: { click: "sells='none';producer_profile_only=false;", class: "{selected: sells=='none' && producer_profile_only==false}" } } - .top - %h3 Sell products - %p As a supplier - .bottom ALWAYS FREE - %p.description - Add your products to Open Food Network, allowing customers to see your product range, and allowing you to act as a supplier to other shopfronts. - - .full_hub.option.one-third.column.omega.disabled - %a.full-width.button.selector{ ng: { click: "sells='own';producer_profile_only=false;", class: "{selected: sells=='own' && producer_profile_only==false}" } } - .top - %h3 Sell products - %p Through an OFN shopfront - .bottom 30 DAY TRIAL - %p.description - Test out having your own shopfront with full access to all Shopfront features for 30 days. - %br - %br - At the end of your trial, there is a one-off $200 fee to fully activate your account. Then you will be billed for 2% of your actual transactions, capped at $50 a month (so if you don’t sell anything you don’t pay anything, but you never pay more than $50 a month). - - - else - .shop_profile.option.one-third.column.alpha - %a.full-width.button.selector{ ng: { click: "sells='none'", class: "{selected: sells=='none'}" } } - .top - %h3 Shop Profile - %p Get a listing - .bottom ALWAYS FREE - %p.description - You want to use OFN as a place for people to find and contact you. - .row - .sixteen.columns.alpha - %span.error{ ng: { show: "(change_type.sells.$error.required || change_type.sells.$error.pattern) && submitted" } } - Please choose one of the options above. - - if @enterprise.sells == 'unspecified' - %input.button.big{ type: 'submit', value: 'Select and continue', ng: { click: "submit(change_type)" } } - - else - %input.button.big{ type: 'submit', value: 'Change now', ng: { click: "submit(change_type)" } } - %br - %hr diff --git a/app/views/spree/admin/overview/multi_enterprise_dashboard.html.haml b/app/views/spree/admin/overview/multi_enterprise_dashboard.html.haml index f230d7e44e..420196d3ce 100644 --- a/app/views/spree/admin/overview/multi_enterprise_dashboard.html.haml +++ b/app/views/spree/admin/overview/multi_enterprise_dashboard.html.haml @@ -1,3 +1,7 @@ +- content_for :page_actions do + = render 'admin/shared/user_guide_link' + + %h1{ :style => 'margin-bottom: 30px'} Dashboard - if @enterprises.unconfirmed.any? @@ -21,4 +25,4 @@ - if can? :admin, OrderCycle = render partial: "order_cycles" - = render partial: "enterprises" \ No newline at end of file + = render partial: "enterprises" diff --git a/app/views/spree/admin/overview/single_enterprise_dashboard.html.haml b/app/views/spree/admin/overview/single_enterprise_dashboard.html.haml index f8cb1fb5d9..3998f736de 100644 --- a/app/views/spree/admin/overview/single_enterprise_dashboard.html.haml +++ b/app/views/spree/admin/overview/single_enterprise_dashboard.html.haml @@ -5,24 +5,26 @@ = "(#{enterprise_type_name(@enterprise)})" - content_for :page_actions do + = render 'admin/shared/user_guide_link' + :javascript function toggleType(){ - if( $('#type_selection').is(":visible") ){ + if( $('#package_selection').is(":visible") ){ $('button#toggle_type i').switchClass("icon-chevron-up","icon-chevron-down") } else { $('button#toggle_type i').switchClass("icon-chevron-down","icon-chevron-up") } - $("#type_selection").slideToggle() + $("#package_selection").slideToggle() } - #type_button + #package_button %button#toggle_type{ onClick: 'toggleType()' } - Change type + Change Package %i.icon-chevron-down -#type_selection{ hidden: true } - = render partial: "change_type_form" +#package_selection{ hidden: true } + = render partial: "/admin/enterprises/change_type_form" - if @enterprise.confirmed_at.nil? @@ -94,7 +96,3 @@ %a.button.bottom{href: main_app.admin_order_cycles_path} Manage order cycles %span.icon-arrow-right - - - - diff --git a/app/views/spree/admin/products/_group_buy_form.html.haml b/app/views/spree/admin/products/_group_buy_form.html.haml index 6669563d15..8f1de2a884 100644 --- a/app/views/spree/admin/products/_group_buy_form.html.haml +++ b/app/views/spree/admin/products/_group_buy_form.html.haml @@ -9,6 +9,6 @@ = f.label :group_buy_0, 'No' %br.clear = f.field_container :group_buy_unit_size do - = f.label :group_buy_unit_size + = f.label :group_buy_unit_size, "Bulk unit size" %br = f.text_field :group_buy_unit_size diff --git a/app/views/spree/admin/products/_supplier_and_group_buy_for_new.html.haml b/app/views/spree/admin/products/_supplier_and_group_buy_for_new.html.haml deleted file mode 100644 index d17a45fa6c..0000000000 --- a/app/views/spree/admin/products/_supplier_and_group_buy_for_new.html.haml +++ /dev/null @@ -1,20 +0,0 @@ -.row - .alpha.six.columns - = f.field_container :supplier do - = f.label :supplier - = f.collection_select(:supplier_id, Enterprise.is_primary_producer.managed_by(spree_current_user).by_name, :id, :name, {:include_blank => true}, {:class => "select2 fullwidth"}) - = f.error_message_on :supplier - .four.columns - = f.field_container :group_buy do - = f.label :group_buy, 'Group buy?' - %br - .alpha.two.columns - = f.radio_button :group_buy, '1', :checked => f.object.group_buy - = f.label :group_buy_1, 'Yes' - .omega.two.columns - = f.radio_button :group_buy, '0', :checked => !f.object.group_buy - = f.label :group_buy_0, 'No' - .omega.six.columns - = f.field_container :group_buy_unit_size do - = f.label :group_buy_unit_size - = f.text_field :group_buy_unit_size, :class => "fullwidth" diff --git a/app/views/spree/admin/reports/_packing_description.html.haml b/app/views/spree/admin/reports/_packing_description.html.haml new file mode 100644 index 0000000000..3d7855cb89 --- /dev/null +++ b/app/views/spree/admin/reports/_packing_description.html.haml @@ -0,0 +1,4 @@ +%ul{style: "margin-left: 12pt"} + - report_types.each do |report_type| + %li + = link_to report_type[0], "#{packing_admin_reports_url}?report_type=#{report_type[1]}" diff --git a/app/views/spree/admin/reports/bulk_coop.html.haml b/app/views/spree/admin/reports/bulk_coop.html.haml index d80a449bb4..8d9a1234ce 100644 --- a/app/views/spree/admin/reports/bulk_coop.html.haml +++ b/app/views/spree/admin/reports/bulk_coop.html.haml @@ -1,4 +1,4 @@ -= form_for @search, :url => spree.bulk_coop_admin_reports_path do |f| += form_for @report.search, :url => spree.bulk_coop_admin_reports_path do |f| = render 'date_range_form', f: f .row @@ -20,7 +20,7 @@ %table#listing_orders.index %thead %tr{'data-hook' => "orders_header"} - - @header.each do |heading| + - @report.header.each do |heading| %th=heading %tbody - @table.each do |row| diff --git a/app/views/spree/admin/reports/orders_and_fulfillment.html.haml b/app/views/spree/admin/reports/orders_and_fulfillment.html.haml index 081cb43384..2465fe0ff2 100644 --- a/app/views/spree/admin/reports/orders_and_fulfillment.html.haml +++ b/app/views/spree/admin/reports/orders_and_fulfillment.html.haml @@ -1,4 +1,4 @@ -= form_for @search, :url => spree.orders_and_fulfillment_admin_reports_path do |f| += form_for @report.search, :url => spree.orders_and_fulfillment_admin_reports_path do |f| = render 'date_range_form', f: f .row @@ -30,7 +30,7 @@ %table#listing_orders.index %thead %tr{'data-hook' => "orders_header"} - - @header.each do |heading| + - @report.header.each do |heading| %th=heading %tbody - @table.each do |row| diff --git a/app/views/spree/admin/reports/packing.html.haml b/app/views/spree/admin/reports/packing.html.haml new file mode 100644 index 0000000000..2aa8d8c8b9 --- /dev/null +++ b/app/views/spree/admin/reports/packing.html.haml @@ -0,0 +1,42 @@ += form_for @report.search, :url => spree.packing_admin_reports_path do |f| + = render 'date_range_form', f: f + + .row + .alpha.two.columns= label_tag nil, "Hubs: " + .omega.fourteen.columns= f.collection_select(:distributor_id_in, @distributors, :id, :name, {}, {class: "select2 fullwidth", multiple: true}) + + .row + .alpha.two.columns= label_tag nil, "Producers: " + .omega.fourteen.columns= select_tag(:supplier_id_in, options_from_collection_for_select(@suppliers, :id, :name, params[:supplier_id_in]), {class: "select2 fullwidth", multiple: true}) + + .row + .alpha.two.columns= label_tag nil, "Order Cycles: " + .omega.fourteen.columns + = f.select(:order_cycle_id_in, report_order_cycle_options(@order_cycles), {selected: params[:q][:order_cycle_id_in]}, {class: "select2 fullwidth", multiple: true}) + + .row + .alpha.two.columns= label_tag nil, "Report Type: " + .omega.fourteen.columns= select_tag(:report_type, options_for_select(@report_types, @report_type)) + + .row + = check_box_tag :csv + = label_tag :csv, "Download as csv" + + .row + = button t(:search) + +%br +%br +%table#listing_orders.index + %thead + %tr{'data-hook' => "orders_header"} + - @report.header.each do |heading| + %th=heading + %tbody + - @table.each do |row| + %tr + - row.each do |column| + %td= column + - if @table.empty? + %tr + %td{:colspan => "2"}= t(:none) diff --git a/app/views/spree/admin/reports/payments.html.haml b/app/views/spree/admin/reports/payments.html.haml index 1a92d507db..909846f47f 100644 --- a/app/views/spree/admin/reports/payments.html.haml +++ b/app/views/spree/admin/reports/payments.html.haml @@ -1,4 +1,4 @@ -= form_for @search, :url => spree.payments_admin_reports_path do |f| += form_for @report.search, :url => spree.payments_admin_reports_path do |f| = render 'date_range_form', f: f .row @@ -20,7 +20,7 @@ %table#listing_orders.index %thead %tr{'data-hook' => "orders_header"} - - @header.each do |heading| + - @report.header.each do |heading| %th=heading %tbody - @table.each do |row| diff --git a/app/views/spree/admin/reports/xero_invoices.html.haml b/app/views/spree/admin/reports/xero_invoices.html.haml index 1ae4e3b279..be58420e13 100644 --- a/app/views/spree/admin/reports/xero_invoices.html.haml +++ b/app/views/spree/admin/reports/xero_invoices.html.haml @@ -1,6 +1,9 @@ = form_for @search, url: spree.xero_invoices_admin_reports_path do |f| = render 'date_range_form', f: f + .row + .four.columns.alpha= label_tag :report_type, "Report Type: " + .four.columns.omega= select_tag :report_type, options_for_select(xero_report_types, params[:report_type]), {include_blank: false, class: "select2 fullwidth"} .row .four.columns.alpha= label_tag nil, "Hub: " .four.columns.omega= f.collection_select(:distributor_id_eq, @distributors, :id, :name, {:include_blank => 'All'}, {:class => "select2 fullwidth"}) diff --git a/app/views/spree/admin/tax_settings/edit.html.haml b/app/views/spree/admin/tax_settings/edit.html.haml new file mode 100644 index 0000000000..102ec0609e --- /dev/null +++ b/app/views/spree/admin/tax_settings/edit.html.haml @@ -0,0 +1,32 @@ += render :partial => 'spree/admin/shared/configuration_menu' + +- content_for :page_title do + = t(:tax_settings) + += form_tag admin_tax_settings_path, :method => :put do + + .field.align-center{ "data-hook" => "products_require_tax_category" } + = hidden_field_tag 'preferences[products_require_tax_category]', '0' + = check_box_tag 'preferences[products_require_tax_category]', '1', Spree::Config[:products_require_tax_category] + = label_tag nil, t(:products_require_tax_category) + + .field.align-center{"data-hook" => "shipment_vat"} + = hidden_field_tag 'preferences[shipment_inc_vat]', '0' + = check_box_tag 'preferences[shipment_inc_vat]', '1', Spree::Config[:shipment_inc_vat] + = label_tag nil, t(:shipment_inc_vat) + + .field.align-center{ "data-hook" => "shipping_tax_rate" } + = number_field_tag "preferences[shipping_tax_rate]", Spree::Config[:shipping_tax_rate].to_f, in: 0.0..1.0, step: 0.01 + = label_tag nil, t(:shipping_tax_rate) + + .field.align-center{"data-hook" => "billing_tax"} + = hidden_field_tag 'preferences[account_bill_inc_tax]', '0' + = check_box_tag 'preferences[account_bill_inc_tax]', '1', Spree::Config[:account_bill_inc_tax] + = label_tag nil, t(:account_bill_inc_tax) + + .field.align-center{ "data-hook" => "account_bill_tax_rate" } + = number_field_tag "preferences[account_bill_tax_rate]", Spree::Config[:account_bill_tax_rate].to_f, in: 0.0..1.0, step: 0.01 + = label_tag nil, t(:account_bill_tax_rate) + + .form-buttons{"data-hook" => "buttons"} + = button t(:update), 'icon-refresh' diff --git a/app/views/spree/api/products/bulk_show.v1.rabl b/app/views/spree/api/products/bulk_show.v1.rabl index d2cd23bd03..c336d72530 100644 --- a/app/views/spree/api/products/bulk_show.v1.rabl +++ b/app/views/spree/api/products/bulk_show.v1.rabl @@ -1,5 +1,11 @@ object @product -attributes :id, :name, :variant_unit, :variant_unit_scale, :variant_unit_name, :on_demand + +# TODO: This is used by bulk product edit when a product is cloned. +# But the list of products is serialized by Api::Admin::ProductSerializer. +# This should probably be unified. + +attributes :id, :name, :sku, :variant_unit, :variant_unit_scale, :variant_unit_name, :on_demand, :inherits_properties +attributes :on_hand, :price, :available_on, :permalink_live, :tax_category_id # Infinity is not a valid JSON object, but Rails encodes it anyway node( :taxon_ids ) { |p| p.taxons.map{ |t| t.id }.join(",") } @@ -8,6 +14,8 @@ node( :price ) { |p| p.price.nil? ? '0.0' : p.price } node( :available_on ) { |p| p.available_on.blank? ? "" : p.available_on.strftime("%F %T") } node( :permalink_live ) { |p| p.permalink } +node( :producer_id ) { |p| p.supplier_id } +node( :category_id ) { |p| p.primary_taxon_id } node( :supplier ) do |p| partial 'api/enterprises/bulk_show', :object => p.supplier end diff --git a/app/views/spree/order_mailer/_shipping.html.haml b/app/views/spree/order_mailer/_shipping.html.haml index cb9cde72ca..b50a03323c 100644 --- a/app/views/spree/order_mailer/_shipping.html.haml +++ b/app/views/spree/order_mailer/_shipping.html.haml @@ -8,14 +8,14 @@ Delivery details - if @order.order_cycle.andand.pickup_time_for(@order.distributor) - %h4 + %h4 Delivery on: %strong #{@order.order_cycle.pickup_time_for(@order.distributor)} - if @order.shipping_method.andand.description - %p + %p %em #{@order.shipping_method.description.html_safe} %br - + - if @order.ship_address %h4 Delivery address: %p @@ -37,21 +37,15 @@ Collection details - if @order.order_cycle.andand.pickup_time_for(@order.distributor).present? - %h4 + %h4 Ready for collection: %strong #{@order.order_cycle.pickup_time_for(@order.distributor)} - + - if @order.shipping_method.andand.description.present? %p %em #{@order.shipping_method.description.html_safe} %br - - if @order.ship_address.full_address - %p - %strong Collecting from: - %br - #{@order.ship_address.full_address} - - if @order.order_cycle.andand.pickup_instructions_for(@order.distributor).present? %p %strong Collection instructions: diff --git a/app/views/spree/order_mailer/confirm_email_for_shop.html.haml b/app/views/spree/order_mailer/confirm_email_for_shop.html.haml index f62b3cdfb4..ac5d45d893 100644 --- a/app/views/spree/order_mailer/confirm_email_for_shop.html.haml +++ b/app/views/spree/order_mailer/confirm_email_for_shop.html.haml @@ -19,9 +19,10 @@ %h4 Order confirmation %strong ##{@order.number} -%p +%h5 %strong= "#{@order.bill_address.firstname} #{@order.bill_address.lastname}" - completed the following order at your shopfront: + = " <#{@order.email}>" if @order.email + = @order.bill_address.phone if @order.bill_address.phone = render 'order_summary' = render 'payment' diff --git a/app/views/spree/orders/edit.html.haml b/app/views/spree/orders/edit.html.haml index 5f8186e568..3a00558a52 100644 --- a/app/views/spree/orders/edit.html.haml +++ b/app/views/spree/orders/edit.html.haml @@ -1,19 +1,22 @@ -= inject_enterprises +- content_for(:title) do + Shopping Cart + += inject_enterprises .darkswarm - content_for :order_cycle_form do %closing Your shopping cart - %p + %p Order ready for %strong - if @order.order_cycle - = pickup_time @order.order_cycle + = pickup_time @order.order_cycle - else = @order.distributor.next_collection_at = render partial: "shopping_shared/details" - - %fieldset + + %fieldset.footer-pad - if @order.line_items.empty? %div.row{"data-hook" => "empty_cart"} %p= t(:your_cart_is_empty) @@ -31,9 +34,9 @@ .links{'data-hook' => "cart_buttons"} .row .columns.large-8{"data-hook" => ""} - + %a.button.large.secondary{href: main_app.shop_path} - %i.ofn-i_008-caret-left + %i.ofn-i_008-caret-left Continue shopping .columns.large-4.text-right %a#checkout-link.button.large.primary{href: main_app.checkout_path} @@ -41,4 +44,3 @@ %i.ofn-i_007-caret-right = render partial: "shared/footer" - diff --git a/app/views/spree/orders/show.html.haml b/app/views/spree/orders/show.html.haml index de142e3cac..7533ae981a 100644 --- a/app/views/spree/orders/show.html.haml +++ b/app/views/spree/orders/show.html.haml @@ -1,3 +1,6 @@ +- content_for(:title) do + Order Confirmation + = inject_enterprises .darkswarm @@ -11,7 +14,7 @@ = render "shopping_shared/details" if current_distributor.present? - %fieldset#order_summary{"data-hook" => ""} + %fieldset#order_summary.footer-pad{"data-hook" => ""} .row .columns.large-12.text-center %h2 @@ -30,3 +33,6 @@ - unless params.has_key? :checkout_complete - if try_spree_current_user && respond_to?(:spree_account_path) = link_to t(:my_account), spree_account_path, :class => "button" + + += render partial: "shared/footer" diff --git a/app/views/spree/shared/_order_details.html.haml b/app/views/spree/shared/_order_details.html.haml index 4fe07f07aa..e8315a00e9 100644 --- a/app/views/spree/shared/_order_details.html.haml +++ b/app/views/spree/shared/_order_details.html.haml @@ -65,12 +65,6 @@ %strong #{order.order_cycle.pickup_time_for(order.distributor)} %p.text-small.text-skinny.pre-line %em= order.shipping_method.description.andand.html_safe || "" - .order-summary.text-small - %strong - Collection Address - .pad - %p.text-small - = order.ship_address.full_address - if order.order_cycle.pickup_instructions_for(order.distributor).present? %br diff --git a/config/application.rb b/config/application.rb index 2461f9b72d..8a492f5121 100644 --- a/config/application.rb +++ b/config/application.rb @@ -82,7 +82,7 @@ module Openfoodnetwork config.assets.enabled = true # Version of your assets, change this if you want to expire all your assets - config.assets.version = '1.1' + config.assets.version = '1.2' config.sass.load_paths += [ "#{Gem.loaded_specs['foundation-rails'].full_gem_path}/vendor/assets/stylesheets/foundation/components", @@ -95,7 +95,7 @@ module Openfoodnetwork config.assets.initialize_on_precompile = true config.assets.precompile += ['store/all.css', 'store/all.js', 'store/shop_front.js', 'iehack.js'] config.assets.precompile += ['admin/all.css', 'admin/restore_spree_from_cms.css', 'admin/*.js', 'admin/**/*.js'] - config.assets.precompile += ['darkswarm/all.css', 'darkswarm/all.js'] + config.assets.precompile += ['darkswarm/all.css', 'darkswarm/all_split2.css', 'darkswarm/all.js'] config.assets.precompile += ['mail/all.css'] config.assets.precompile += ['comfortable_mexican_sofa/*'] config.assets.precompile += ['search/all.css', 'search/*.js'] diff --git a/config/environments/test.rb b/config/environments/test.rb index dd3413c8f0..67131b6489 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -11,6 +11,10 @@ Openfoodnetwork::Application.configure do config.serve_static_assets = true config.static_cache_control = "public, max-age=3600" + # Separate cache stores when running in parallel + config.cache_store = :file_store, Rails.root.join("tmp", "cache", "paralleltests#{ENV['TEST_ENV_NUMBER']}") + + # Log error messages when you accidentally call methods on nil config.whiny_nils = true @@ -37,6 +41,11 @@ Openfoodnetwork::Application.configure do # Print deprecation notices to the stderr config.active_support.deprecation = :stderr config.action_mailer.default_url_options = { :host => "test.host" } + + # To block requests before running the database cleaner + require 'open_food_network/rack_request_blocker' + # Make sure the middleware is inserted first in middleware chain + config.middleware.insert_before('ActionDispatch::Static', 'RackRequestBlocker') end # Allows us to use _url helpers in Rspec diff --git a/config/initializers/content_config.rb b/config/initializers/content_config.rb new file mode 100644 index 0000000000..abbd2c70e9 --- /dev/null +++ b/config/initializers/content_config.rb @@ -0,0 +1 @@ +ContentConfig = ContentConfiguration.new diff --git a/config/initializers/user_class_extensions.rb b/config/initializers/user_class_extensions.rb new file mode 100644 index 0000000000..cd9ff60a0c --- /dev/null +++ b/config/initializers/user_class_extensions.rb @@ -0,0 +1,11 @@ +Spree::Core::Engine.config.to_prepare do + if Spree.user_class + Spree.user_class.class_eval do + + # Override of spree method to ignore orders associated with account_invoices + def last_incomplete_spree_order + spree_orders.incomplete.where("id NOT IN (?)", account_invoices.map(&:order_id)).order('created_at DESC').first + end + end + end +end diff --git a/config/locales/en.yml b/config/locales/en.yml index fe7d3c53aa..3149620eab 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -19,3 +19,20 @@ en: producers: Aussie Producers producers_join: Australian producers are now welcome to join the Open Food Network. charges_sales_tax: Charges GST? + + logo: "Logo (640x130)" + logo_mobile: "Mobile logo (75x26)" + logo_mobile_svg: "Mobile logo (SVG)" + home_hero: "Hero image" + home_show_stats: "Show statistics" + footer_logo: "Logo (220x76)" + footer_facebook_url: "Facebook URL" + footer_twitter_url: "Twitter URL" + footer_instagram_url: "Instagram URL" + footer_linkedin_url: "LinkedIn URL" + footer_googleplus_url: "Google Plus URL" + footer_pinterest_url: "Pinterest URL" + footer_email: "Email" + footer_links_md: "Links" + footer_about_url: "About URL" + footer_tos_url: "Terms of Service URL" \ No newline at end of file diff --git a/config/ng-test.conf.js b/config/ng-test.conf.js index 0456d5eb88..56df589f0c 100644 --- a/config/ng-test.conf.js +++ b/config/ng-test.conf.js @@ -11,6 +11,7 @@ module.exports = function(config) { 'app/assets/javascripts/shared/angular-local-storage.js', 'app/assets/javascripts/shared/bindonce.min.js', 'app/assets/javascripts/shared/ng-infinite-scroll.min.js', + 'app/assets/javascripts/shared/angular-slideables.js', 'app/assets/javascripts/admin/*.js*', 'app/assets/javascripts/admin/*/*.js*', // Pull in top level files in each folder first (often these are module declarations) diff --git a/config/routes.rb b/config/routes.rb index 8e62cc3193..a21e3187a2 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,6 +1,12 @@ Openfoodnetwork::Application.routes.draw do root :to => 'home#index' + # Redirects from old URLs avoid server errors and helps search engines + get "/enterprises", to: redirect("/") + get "/products", to: redirect("/") + get "/products/:id", to: redirect("/") + get "/t/products/:id", to: redirect("/") + get "/about_us", to: redirect(ContentConfig.footer_about_url) get "/#/login", to: "home#index", as: :spree_login get "/login", to: redirect("/#/login") @@ -16,8 +22,23 @@ Openfoodnetwork::Application.routes.draw do get :order_cycle end - resources :groups - resources :producers + resources :producers, only: [:index] do + collection do + get :signup + end + end + + resources :shops, only: [:index] do + collection do + get :signup + end + end + + resources :groups, only: [:index, :show] do + collection do + get :signup + end + end get '/checkout', :to => 'checkout#edit' , :as => :checkout put '/checkout', :to => 'checkout#update' , :as => :update_checkout @@ -25,18 +46,16 @@ Openfoodnetwork::Application.routes.draw do resources :enterprises do collection do - get :suppliers - get :distributors post :search get :check_permalink end member do - get :shop_front # new world - get :shop # old world + get :shop end end get '/:id/shop', to: 'enterprises#shop', as: 'enterprise_shop' + get "/enterprises/:permalink", to: redirect("/") # Legacy enterprise URL devise_for :enterprise, controllers: { confirmations: 'enterprise_confirmations' } @@ -57,7 +76,8 @@ Openfoodnetwork::Application.routes.draw do end member do - put :set_sells + get :welcome + put :register end resources :producer_properties do @@ -85,6 +105,17 @@ Openfoodnetwork::Application.routes.draw do end resources :customers, only: [:index, :update] + + resource :content + + resource :accounts_and_billing_settings, only: [:edit, :update] do + collection do + get :show_methods + get :start_job + end + end + + resource :account, only: [:show], controller: 'account' end namespace :api do @@ -99,8 +130,6 @@ Openfoodnetwork::Application.routes.draw do end end - get "about_us", :controller => 'home', :action => "about_us" - namespace :open_food_network do resources :cart do post :add_variant @@ -130,6 +159,7 @@ end Spree::Core::Engine.routes.prepend do match '/admin/reports/orders_and_distributors' => 'admin/reports#orders_and_distributors', :as => "orders_and_distributors_admin_reports", :via => [:get, :post] match '/admin/reports/order_cycle_management' => 'admin/reports#order_cycle_management', :as => "order_cycle_management_admin_reports", :via => [:get, :post] + match '/admin/reports/packing' => 'admin/reports#packing', :as => "packing_admin_reports", :via => [:get, :post] match '/admin/reports/group_buys' => 'admin/reports#group_buys', :as => "group_buys_admin_reports", :via => [:get, :post] match '/admin/reports/bulk_coop' => 'admin/reports#bulk_coop', :as => "bulk_coop_admin_reports", :via => [:get, :post] match '/admin/reports/payments' => 'admin/reports#payments', :as => "payments_admin_reports", :via => [:get, :post] diff --git a/config/schedule.rb b/config/schedule.rb index 0c98f72dc6..a09ca55dce 100644 --- a/config/schedule.rb +++ b/config/schedule.rb @@ -18,3 +18,12 @@ end every 4.hours do rake 'db2fog:backup' end + +every 1.day, at: '1:00am' do + rake 'openfoodnetwork:billing:update_account_invoices' +end + +# On the 2nd of every month at 1:30am +every '30 1 2 * *' do + rake 'openfoodnetwork:billing:finalize_account_invoices' +end diff --git a/config/unicorn.rb b/config/unicorn.rb index e5d7317940..22e954b4d0 100644 --- a/config/unicorn.rb +++ b/config/unicorn.rb @@ -1,27 +1,2 @@ -preload_app true # https://newrelic.com/docs/ruby/no-data-with-unicorn -worker_processes 4 # amount of unicorn workers to spin up -timeout 60 # restarts workers that hang for 30 seconds - - -# https://devcenter.heroku.com/articles/forked-pg-connections -before_fork do |server, worker| - - Signal.trap 'TERM' do - puts 'Unicorn master intercepting TERM and sending myself QUIT instead' - Process.kill 'QUIT', Process.pid - end - - defined?(ActiveRecord::Base) and - ActiveRecord::Base.connection.disconnect! -end - -after_fork do |server, worker| - - Signal.trap 'TERM' do - puts 'Unicorn worker intercepting TERM and doing nothing. Wait for master to sent QUIT' - end - - defined?(ActiveRecord::Base) and - ActiveRecord::Base.establish_connection - -end +worker_processes 2 # amount of unicorn workers to spin up +timeout 120 # restarts workers that hang for 30 seconds diff --git a/db/migrate/20150612045544_make_enterprises_name_unique.rb b/db/migrate/20150612045544_make_enterprises_name_unique.rb new file mode 100644 index 0000000000..5722284d94 --- /dev/null +++ b/db/migrate/20150612045544_make_enterprises_name_unique.rb @@ -0,0 +1,20 @@ +class MakeEnterprisesNameUnique < ActiveRecord::Migration + def up + dup_names = Enterprise.group('name').select('name, COUNT(*) AS num_enterprises') + + dup_names.each do |data| + (data.num_enterprises.to_i - 1).times do |i| + e = Enterprise.find_by_name data.name + new_name = "#{data.name}-#{i+1}" + e.update_column :name, new_name + say "Renamed enterprise #{data.name} to #{new_name}" + end + end + + add_index :enterprises, :name, unique: true + end + + def down + remove_index :enterprises, :name + end +end diff --git a/db/migrate/20150619020711_create_versions.rb b/db/migrate/20150619020711_create_versions.rb new file mode 100644 index 0000000000..23be970c66 --- /dev/null +++ b/db/migrate/20150619020711_create_versions.rb @@ -0,0 +1,13 @@ +class CreateVersions < ActiveRecord::Migration + def change + create_table :versions do |t| + t.string :item_type, :null => false + t.integer :item_id, :null => false + t.string :event, :null => false + t.string :whodunnit + t.text :object + t.datetime :created_at + end + add_index :versions, [:item_type, :item_id] + end +end diff --git a/db/migrate/20150619100137_create_bill_items.rb b/db/migrate/20150619100137_create_bill_items.rb new file mode 100644 index 0000000000..fec80692b1 --- /dev/null +++ b/db/migrate/20150619100137_create_bill_items.rb @@ -0,0 +1,15 @@ +class CreateBillItems < ActiveRecord::Migration + def change + create_table :bill_items do |t| + t.references :enterprise, nil: false + t.references :owner, nil: false + t.datetime :begins_at, default: nil + t.datetime :ends_at, default: nil + t.string :sells, default: nil + t.boolean :trial, default: false + t.decimal :turnover, default: 0.0 + t.foreign_key :enterprises + t.foreign_key :spree_users, column: :owner_id + end + end +end diff --git a/db/migrate/20150626090338_rename_bill_items_to_billable_periods.rb b/db/migrate/20150626090338_rename_bill_items_to_billable_periods.rb new file mode 100644 index 0000000000..caa2fb9a4d --- /dev/null +++ b/db/migrate/20150626090338_rename_bill_items_to_billable_periods.rb @@ -0,0 +1,9 @@ +class RenameBillItemsToBillablePeriods < ActiveRecord::Migration + def up + rename_table :bill_items, :billable_periods + end + + def down + rename_table :billable_periods, :bill_items + end +end diff --git a/db/migrate/20150701034055_add_timestamps_to_billable_periods.rb b/db/migrate/20150701034055_add_timestamps_to_billable_periods.rb new file mode 100644 index 0000000000..6c3ea52356 --- /dev/null +++ b/db/migrate/20150701034055_add_timestamps_to_billable_periods.rb @@ -0,0 +1,8 @@ +class AddTimestampsToBillablePeriods < ActiveRecord::Migration + def change + change_table(:billable_periods) do |t| + t.datetime :deleted_at, default: nil + t.timestamps + end + end +end diff --git a/db/migrate/20150719153136_rename_line_item_unit_value.rb b/db/migrate/20150719153136_rename_line_item_unit_value.rb new file mode 100644 index 0000000000..9dbdce75c5 --- /dev/null +++ b/db/migrate/20150719153136_rename_line_item_unit_value.rb @@ -0,0 +1,5 @@ +class RenameLineItemUnitValue < ActiveRecord::Migration + def change + rename_column :spree_line_items, :unit_value, :final_weight_volume + end +end diff --git a/db/migrate/20150719153732_update_precision_on_line_item_final_weight_volume.rb b/db/migrate/20150719153732_update_precision_on_line_item_final_weight_volume.rb new file mode 100644 index 0000000000..c1b3482c0f --- /dev/null +++ b/db/migrate/20150719153732_update_precision_on_line_item_final_weight_volume.rb @@ -0,0 +1,11 @@ +class UpdatePrecisionOnLineItemFinalWeightVolume < ActiveRecord::Migration + def up + change_column :spree_line_items, :final_weight_volume, :decimal, :precision => 10, :scale => 2 + end + + def down + raise ActiveRecord::IrreversibleMigration + end +end + + diff --git a/db/migrate/20150730160010_update_spree_line_item_final_weight_volume.rb b/db/migrate/20150730160010_update_spree_line_item_final_weight_volume.rb new file mode 100644 index 0000000000..4d51a7160b --- /dev/null +++ b/db/migrate/20150730160010_update_spree_line_item_final_weight_volume.rb @@ -0,0 +1,9 @@ +class UpdateSpreeLineItemFinalWeightVolume < ActiveRecord::Migration + def up + execute "UPDATE spree_line_items SET final_weight_volume = final_weight_volume * quantity" + end + + def down + raise ActiveRecord::IrreversibleMigration + end +end diff --git a/db/migrate/20150916012814_create_account_invoices.rb b/db/migrate/20150916012814_create_account_invoices.rb new file mode 100644 index 0000000000..7e92a53593 --- /dev/null +++ b/db/migrate/20150916012814_create_account_invoices.rb @@ -0,0 +1,18 @@ +class CreateAccountInvoices < ActiveRecord::Migration + def change + create_table :account_invoices do |t| + t.references :user, null: false + t.references :order + t.integer :year, null: false + t.integer :month, null: false + t.datetime :issued_at + + t.timestamps + end + add_index :account_invoices, :user_id + add_index :account_invoices, :order_id + + add_foreign_key :account_invoices, :spree_orders, column: :order_id + add_foreign_key :account_invoices, :spree_users, column: :user_id + end +end diff --git a/db/migrate/20150916061809_add_account_invoice_to_billable_periods.rb b/db/migrate/20150916061809_add_account_invoice_to_billable_periods.rb new file mode 100644 index 0000000000..ee1d9d5d93 --- /dev/null +++ b/db/migrate/20150916061809_add_account_invoice_to_billable_periods.rb @@ -0,0 +1,7 @@ +class AddAccountInvoiceToBillablePeriods < ActiveRecord::Migration + def change + add_column :billable_periods, :account_invoice_id, :integer, null: false + add_index :billable_periods, :account_invoice_id + add_foreign_key :billable_periods, :account_invoices, column: :account_invoice_id + end +end diff --git a/db/migrate/20151002020537_ensure_address_for_account_invoice_orders.rb b/db/migrate/20151002020537_ensure_address_for_account_invoice_orders.rb new file mode 100644 index 0000000000..6d2c7d45c5 --- /dev/null +++ b/db/migrate/20151002020537_ensure_address_for_account_invoice_orders.rb @@ -0,0 +1,15 @@ +class EnsureAddressForAccountInvoiceOrders < ActiveRecord::Migration + def up + AccountInvoice.where('order_id IS NOT NULL').each do |account_invoice| + billable_periods = account_invoice.billable_periods.order(:enterprise_id).reject{ |bp| bp.turnover == 0 } + + if billable_periods.any? + address = billable_periods.first.enterprise.address + account_invoice.order.update_attributes(bill_address: address, ship_address: address) + end + end + end + + def down + end +end diff --git a/db/schema.rb b/db/schema.rb index 48309e3fd7..37ad6f1a44 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,20 @@ # # It's strongly recommended to check this file into your version control system. -ActiveRecord::Schema.define(:version => 20150605052516) do +ActiveRecord::Schema.define(:version => 20151002020537) do + + create_table "account_invoices", :force => true do |t| + t.integer "user_id", :null => false + t.integer "order_id" + t.integer "year", :null => false + t.integer "month", :null => false + t.datetime "issued_at" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + + add_index "account_invoices", ["order_id"], :name => "index_account_invoices_on_order_id" + add_index "account_invoices", ["user_id"], :name => "index_account_invoices_on_user_id" create_table "adjustment_metadata", :force => true do |t| t.integer "adjustment_id" @@ -24,6 +37,22 @@ ActiveRecord::Schema.define(:version => 20150605052516) do add_index "adjustment_metadata", ["adjustment_id"], :name => "index_adjustment_metadata_on_adjustment_id" add_index "adjustment_metadata", ["enterprise_id"], :name => "index_adjustment_metadata_on_enterprise_id" + create_table "billable_periods", :force => true do |t| + t.integer "enterprise_id" + t.integer "owner_id" + t.datetime "begins_at" + t.datetime "ends_at" + t.string "sells" + t.boolean "trial", :default => false + t.decimal "turnover", :default => 0.0 + t.datetime "deleted_at" + t.datetime "created_at" + t.datetime "updated_at" + t.integer "account_invoice_id", :null => false + end + + add_index "billable_periods", ["account_invoice_id"], :name => "index_billable_periods_on_account_invoice_id" + create_table "carts", :force => true do |t| t.integer "user_id" end @@ -322,6 +351,7 @@ ActiveRecord::Schema.define(:version => 20150605052516) do add_index "enterprises", ["address_id"], :name => "index_enterprises_on_address_id" add_index "enterprises", ["confirmation_token"], :name => "index_enterprises_on_confirmation_token", :unique => true add_index "enterprises", ["is_primary_producer", "sells"], :name => "index_enterprises_on_is_primary_producer_and_sells" + add_index "enterprises", ["name"], :name => "index_enterprises_on_name", :unique => true add_index "enterprises", ["owner_id"], :name => "index_enterprises_on_owner_id" add_index "enterprises", ["permalink"], :name => "index_enterprises_on_permalink", :unique => true add_index "enterprises", ["sells"], :name => "index_enterprises_on_sells" @@ -564,7 +594,7 @@ ActiveRecord::Schema.define(:version => 20150605052516) do t.string "currency" t.decimal "distribution_fee", :precision => 10, :scale => 2 t.string "shipping_method_name" - t.decimal "unit_value", :precision => 8, :scale => 2 + t.decimal "final_weight_volume", :precision => 10, :scale => 2 end add_index "spree_line_items", ["order_id"], :name => "index_line_items_on_order_id" @@ -1127,9 +1157,27 @@ ActiveRecord::Schema.define(:version => 20150605052516) do add_index "variant_overrides", ["variant_id", "hub_id"], :name => "index_variant_overrides_on_variant_id_and_hub_id" + create_table "versions", :force => true do |t| + t.string "item_type", :null => false + t.integer "item_id", :null => false + t.string "event", :null => false + t.string "whodunnit" + t.text "object" + t.datetime "created_at" + end + + add_index "versions", ["item_type", "item_id"], :name => "index_versions_on_item_type_and_item_id" + + add_foreign_key "account_invoices", "spree_orders", name: "account_invoices_order_id_fk", column: "order_id" + add_foreign_key "account_invoices", "spree_users", name: "account_invoices_user_id_fk", column: "user_id" + add_foreign_key "adjustment_metadata", "enterprises", name: "adjustment_metadata_enterprise_id_fk" add_foreign_key "adjustment_metadata", "spree_adjustments", name: "adjustment_metadata_adjustment_id_fk", column: "adjustment_id", dependent: :delete + add_foreign_key "billable_periods", "account_invoices", name: "billable_periods_account_invoice_id_fk" + add_foreign_key "billable_periods", "enterprises", name: "bill_items_enterprise_id_fk" + add_foreign_key "billable_periods", "spree_users", name: "bill_items_owner_id_fk", column: "owner_id" + add_foreign_key "carts", "spree_users", name: "carts_user_id_fk", column: "user_id" add_foreign_key "cms_blocks", "cms_pages", name: "cms_blocks_page_id_fk", column: "page_id" diff --git a/lib/open_food_network/accounts_and_billing_settings_validator.rb b/lib/open_food_network/accounts_and_billing_settings_validator.rb new file mode 100644 index 0000000000..0269f9ff43 --- /dev/null +++ b/lib/open_food_network/accounts_and_billing_settings_validator.rb @@ -0,0 +1,41 @@ +# This class is a lightweight model used to validate preferences for accounts and billing settings +# when they are submitted to the AccountsAndBillingSettingsController + +module OpenFoodNetwork + class AccountsAndBillingSettingsValidator + include ActiveModel::Validations + + attr_accessor :accounts_distributor_id, :default_accounts_payment_method_id, :default_accounts_shipping_method_id + attr_accessor :auto_update_invoices, :auto_finalize_invoices + + validate :ensure_accounts_distributor_set + validate :ensure_default_payment_method_set + validate :ensure_default_shipping_method_set + # validate :ensure_billing_info_collected, unless: lambda { create_invoices_for_enterprise_users == '0' } + + def initialize(attr, button=nil) + attr.each { |k,v| instance_variable_set("@#{k}", v) } + @button = button + end + + def ensure_accounts_distributor_set + unless Enterprise.find_by_id(accounts_distributor_id) + errors.add(:accounts_distributor, "must be set if you wish to create invoices for enterprise users.") + end + end + + def ensure_default_payment_method_set + unless Enterprise.find_by_id(accounts_distributor_id) && + Enterprise.find_by_id(accounts_distributor_id).payment_methods.find_by_id(default_accounts_payment_method_id) + errors.add(:default_payment_method, "must be set if you wish to create invoices for enterprise users.") + end + end + + def ensure_default_shipping_method_set + unless Enterprise.find_by_id(accounts_distributor_id) && + Enterprise.find_by_id(accounts_distributor_id).shipping_methods.find_by_id(default_accounts_shipping_method_id) + errors.add(:default_shipping_method, "must be set if you wish to create invoices for enterprise users.") + end + end + end +end diff --git a/lib/open_food_network/bulk_coop_report.rb b/lib/open_food_network/bulk_coop_report.rb new file mode 100644 index 0000000000..57f34d9edc --- /dev/null +++ b/lib/open_food_network/bulk_coop_report.rb @@ -0,0 +1,104 @@ +require 'open_food_network/reports/bulk_coop_supplier_report' +require 'open_food_network/reports/bulk_coop_allocation_report' + +module OpenFoodNetwork + class BulkCoopReport + attr_reader :params + def initialize(user, params = {}) + @params = params + @user = user + + @supplier_report = OpenFoodNetwork::Reports::BulkCoopSupplierReport.new + @allocation_report = OpenFoodNetwork::Reports::BulkCoopAllocationReport.new + end + + def header + case params[:report_type] + when "bulk_coop_supplier_report" + @supplier_report.header + when "bulk_coop_allocation" + @allocation_report.header + when "bulk_coop_packing_sheets" + ["Customer", "Product", "Variant", "Sum Total"] + when "bulk_coop_customer_payments" + ["Customer", "Date of Order", "Total Cost", "Amount Owing", "Amount Paid"] + else + ["Supplier", "Product", "Bulk Unit Size", "Variant", "Weight", "Sum Total", "Sum Max Total", "Units Required", "Remainder"] + end + end + + def search + Spree::Order.complete.not_state(:canceled).managed_by(@user).search(params[:q]) + end + + def table_items + orders = search.result + orders.map { |o| o.line_items.managed_by(@user) }.flatten + end + + def rules + case params[:report_type] + when "bulk_coop_supplier_report" + @supplier_report.rules + when "bulk_coop_allocation" + @allocation_report.rules + when "bulk_coop_packing_sheets" + [ { group_by: proc { |li| li.variant.product }, + sort_by: proc { |product| product.name } }, + { group_by: proc { |li| li.variant }, + sort_by: proc { |variant| variant.full_name } }, + { group_by: proc { |li| li.order }, + sort_by: proc { |order| order.to_s } } ] + when "bulk_coop_customer_payments" + [ { group_by: proc { |li| li.order }, + sort_by: proc { |order| order.completed_at } } ] + else + [ { group_by: proc { |li| li.variant.product.supplier }, + sort_by: proc { |supplier| supplier.name } }, + { group_by: proc { |li| li.variant.product }, + sort_by: proc { |product| product.name }, + summary_columns: [ proc { |lis| lis.first.variant.product.supplier.name }, + proc { |lis| lis.first.variant.product.name }, + proc { |lis| lis.first.variant.product.group_buy_unit_size || 0.0 }, + proc { |lis| "" }, + proc { |lis| "" }, + proc { |lis| lis.sum { |li| li.quantity * (li.variant.weight || 0) } }, + proc { |lis| lis.sum { |li| (li.max_quantity || 0) * (li.variant.weight || 0) } }, + proc { |lis| ( (lis.first.variant.product.group_buy_unit_size || 0).zero? ? 0 : ( lis.sum { |li| ( [li.max_quantity || 0, li.quantity || 0].max ) * (li.variant.weight || 0) } / lis.first.variant.product.group_buy_unit_size ) ).floor }, + proc { |lis| lis.sum { |li| ( [li.max_quantity || 0, li.quantity || 0].max ) * (li.variant.weight || 0) } - ( ( (lis.first.variant.product.group_buy_unit_size || 0).zero? ? 0 : ( lis.sum { |li| ( [li.max_quantity || 0, li.quantity || 0].max ) * (li.variant.weight || 0) } / lis.first.variant.product.group_buy_unit_size ) ).floor * (lis.first.variant.product.group_buy_unit_size || 0) ) } ] }, + { group_by: proc { |li| li.variant }, + sort_by: proc { |variant| variant.full_name } } ] + end + end + + def columns + case params[:report_type] + when "bulk_coop_supplier_report" + @supplier_report.columns + when "bulk_coop_allocation" + @allocation_report.columns + when "bulk_coop_packing_sheets" + [ proc { |lis| lis.first.order.bill_address.firstname + " " + lis.first.order.bill_address.lastname }, + proc { |lis| lis.first.variant.product.name }, + proc { |lis| lis.first.variant.full_name }, + proc { |lis| lis.sum { |li| li.quantity } } ] + when "bulk_coop_customer_payments" + [ proc { |lis| lis.first.order.bill_address.firstname + " " + lis.first.order.bill_address.lastname }, + proc { |lis| lis.first.order.completed_at.to_s }, + proc { |lis| lis.map { |li| li.order }.uniq.sum { |o| o.total } }, + proc { |lis| lis.map { |li| li.order }.uniq.sum { |o| o.outstanding_balance } }, + proc { |lis| lis.map { |li| li.order }.uniq.sum { |o| o.payment_total } } ] + else + [ proc { |lis| lis.first.variant.product.supplier.name }, + proc { |lis| lis.first.variant.product.name }, + proc { |lis| lis.first.variant.product.group_buy_unit_size || 0.0 }, + proc { |lis| lis.first.variant.full_name }, + proc { |lis| lis.first.variant.weight || 0 }, + proc { |lis| lis.sum { |li| li.quantity } }, + proc { |lis| lis.sum { |li| li.max_quantity || 0 } }, + proc { |lis| "" }, + proc { |lis| "" } ] + end + end + end +end diff --git a/lib/open_food_network/enterprise_fee_calculator.rb b/lib/open_food_network/enterprise_fee_calculator.rb index c8070f2ab2..d65a80c0dc 100644 --- a/lib/open_food_network/enterprise_fee_calculator.rb +++ b/lib/open_food_network/enterprise_fee_calculator.rb @@ -7,17 +7,35 @@ module OpenFoodNetwork @order_cycle = order_cycle end + def indexed_fees_for(variant) + load_enterprise_fees unless @indexed_enterprise_fees + + indexed_enterprise_fees_for(variant).sum do |enterprise_fee| + calculate_fee_for variant, enterprise_fee + end + end + + def indexed_fees_by_type_for(variant) + load_enterprise_fees unless @indexed_enterprise_fees + + indexed_enterprise_fees_for(variant).inject({}) do |fees, enterprise_fee| + fees[enterprise_fee.fee_type.to_sym] ||= 0 + fees[enterprise_fee.fee_type.to_sym] += calculate_fee_for variant, enterprise_fee + fees + end.select { |fee_type, amount| amount > 0 } + end + def fees_for(variant) per_item_enterprise_fee_applicators_for(variant).sum do |applicator| - calculate_fee_for variant, applicator + calculate_fee_for variant, applicator.enterprise_fee end end def fees_by_type_for(variant) per_item_enterprise_fee_applicators_for(variant).inject({}) do |fees, applicator| fees[applicator.enterprise_fee.fee_type.to_sym] ||= 0 - fees[applicator.enterprise_fee.fee_type.to_sym] += calculate_fee_for variant, applicator + fees[applicator.enterprise_fee.fee_type.to_sym] += calculate_fee_for variant, applicator.enterprise_fee fees end.select { |fee_type, amount| amount > 0 } end @@ -45,12 +63,50 @@ module OpenFoodNetwork private - def calculate_fee_for(variant, applicator) + def load_enterprise_fees + @indexed_enterprise_fees = {} + + exchange_fees = per_item_enterprise_fees_with_exchange_details + load_exchange_fees exchange_fees + load_coordinator_fees + end + + def per_item_enterprise_fees_with_exchange_details + EnterpriseFee. + per_item. + joins(:exchanges => :exchange_variants). + where('exchanges.order_cycle_id = ?', @order_cycle.id). + merge(Exchange.supplying_to(@distributor)). + select('enterprise_fees.*, exchange_variants.variant_id AS variant_id') + end + + def load_exchange_fees(exchange_fees) + exchange_fees.each do |enterprise_fee| + @indexed_enterprise_fees[enterprise_fee.variant_id.to_i] ||= [] + @indexed_enterprise_fees[enterprise_fee.variant_id.to_i] << enterprise_fee + end + end + + def load_coordinator_fees + @order_cycle.coordinator_fees.per_item.each do |enterprise_fee| + @order_cycle.variants.map(&:id).each do |variant_id| + @indexed_enterprise_fees[variant_id] ||= [] + @indexed_enterprise_fees[variant_id] << enterprise_fee + end + end + end + + def indexed_enterprise_fees_for(variant) + @indexed_enterprise_fees[variant.id] || [] + end + + + def calculate_fee_for(variant, enterprise_fee) # Spree's Calculator interface accepts Orders or LineItems, # so we meet that interface with a struct. # Amount is faked, this is a method on LineItem line_item = OpenStruct.new variant: variant, quantity: 1, amount: variant.price - applicator.enterprise_fee.compute_amount(line_item) + enterprise_fee.compute_amount(line_item) end def per_item_enterprise_fee_applicators_for(variant) diff --git a/lib/open_food_network/lettuce_share_report.rb b/lib/open_food_network/lettuce_share_report.rb new file mode 100644 index 0000000000..1dc283eba8 --- /dev/null +++ b/lib/open_food_network/lettuce_share_report.rb @@ -0,0 +1,78 @@ +require 'open_food_network/products_and_inventory_report_base' + +module OpenFoodNetwork + class LettuceShareReport < ProductsAndInventoryReportBase + def header + [ + "PRODUCT", + "Description", + "Qty", + "Pack Size", + "Unit", + "Unit Price", + "Total", + "GST incl.", + "Grower and growing method", + "Taxon" + ] + end + + def table + variants.map do |variant| + [ + variant.product.name, + variant.full_name, + '', + OptionValueNamer.new(variant).value, + OptionValueNamer.new(variant).unit, + variant.price - gst(variant), + variant.price, + gst(variant), + grower_and_method(variant), + variant.product.primary_taxon.name + ] + end + end + + + private + + def gst(variant) + tax_category = variant.product.tax_category + if tax_category && tax_category.tax_rates.present? + tax_rate = tax_category.tax_rates.first + line_item = mock_line_item(variant, tax_category) + tax_rate.calculator.compute line_item + else + 0 + end + end + + def mock_line_item(variant, tax_category) + product = OpenStruct.new tax_category: tax_category + line_item = Spree::LineItem.new quantity: 1 + line_item.define_singleton_method(:product) { variant.product } + line_item.define_singleton_method(:price) { variant.price } + line_item + end + + def grower_and_method(variant) + cert = certification(variant) + + result = producer_name(variant) + result += " (#{cert})" if cert.present? + result + end + + def producer_name(variant) + variant.product.supplier.name + end + + def certification(variant) + variant.product.properties_including_inherited.map do |p| + "#{p[:name]} - #{p[:value]}" + end.join(', ') + end + + end +end diff --git a/lib/open_food_network/option_value_namer.rb b/lib/open_food_network/option_value_namer.rb index 2c2383906d..6a77d9cb35 100644 --- a/lib/open_food_network/option_value_namer.rb +++ b/lib/open_food_network/option_value_namer.rb @@ -15,6 +15,17 @@ module OpenFoodNetwork name_fields.join ' ' end + def value + value, _ = option_value_value_unit + value + end + + def unit + _, unit = option_value_value_unit + unit + end + + private def value_scaled? diff --git a/lib/open_food_network/order_grouper.rb b/lib/open_food_network/order_grouper.rb index c0a0fc51ea..c24d1069b8 100644 --- a/lib/open_food_network/order_grouper.rb +++ b/lib/open_food_network/order_grouper.rb @@ -8,7 +8,7 @@ module OpenFoodNetwork def build_tree(items, remaining_rules) rules = remaining_rules.clone - unless rules.empty? + if rules.any? rule = rules.delete_at(0) # Remove current rule for subsequent groupings group_and_sort(rule, rules, items) else @@ -20,7 +20,7 @@ module OpenFoodNetwork branch = {} groups = items.group_by { |item| rule[:group_by].call(item) } sorted_groups = groups.sort_by { |key, value| rule[:sort_by].call(key) } - sorted_groups.each do |property,items_by_property| + sorted_groups.each do |property, items_by_property| branch[property] = build_tree(items_by_property, remaining_rules) branch[property][:summary_row] = { items: items_by_property, columns: rule[:summary_columns] } unless rule[:summary_columns] == nil || is_leaf_node(branch[property]) end @@ -44,15 +44,15 @@ module OpenFoodNetwork end def table(items) - tree = build_tree(items,@rules) + tree = build_tree(items, @rules) table = build_table(tree) table end - + private - + def is_leaf_node(node) node.is_a? Array end end -end \ No newline at end of file +end diff --git a/lib/open_food_network/orders_and_fulfillments_report.rb b/lib/open_food_network/orders_and_fulfillments_report.rb new file mode 100644 index 0000000000..03c4ec801a --- /dev/null +++ b/lib/open_food_network/orders_and_fulfillments_report.rb @@ -0,0 +1,256 @@ +include Spree::ReportsHelper + +module OpenFoodNetwork + class OrdersAndFulfillmentsReport + attr_reader :params + def initialize(user, params = {}) + @params = params + @user = user + end + + def header + case params[:report_type] + when "order_cycle_supplier_totals" + ["Producer", "Product", "Variant", "Amount", "Total Units", "Curr. Cost per Unit", "Total Cost", "Status", "Incoming Transport"] + when "order_cycle_supplier_totals_by_distributor" + ["Producer", "Product", "Variant", "To Hub", "Amount", "Curr. Cost per Unit", "Total Cost", "Shipping Method"] + when "order_cycle_distributor_totals_by_supplier" + ["Hub", "Producer", "Product", "Variant", "Amount", "Curr. Cost per Unit", "Total Cost", "Total Shipping Cost", "Shipping Method"] + when "order_cycle_customer_totals" + ["Hub", "Customer", "Email", "Phone", "Producer", "Product", "Variant", + "Amount", "Item (#{currency_symbol})", "Item + Fees (#{currency_symbol})", "Admin & Handling (#{currency_symbol})", "Ship (#{currency_symbol})", "Total (#{currency_symbol})", "Paid?", + "Shipping", "Delivery?", + "Ship Street", "Ship Street 2", "Ship City", "Ship Postcode", "Ship State", + "Comments", "SKU", + "Order Cycle", "Payment Method", "Customer Code", "Tags", + "Billing Street 1", "Billing Street 2", "Billing City", "Billing Postcode", "Billing State" + ] + else + ["Producer", "Product", "Variant", "Amount", "Curr. Cost per Unit", "Total Cost", "Status", "Incoming Transport"] + end + + end + + def search + Spree::Order.complete.not_state(:canceled).search(params[:q]) + end + + def table_items + permissions = OpenFoodNetwork::Permissions.new(@user) + orders = permissions.visible_orders.merge(search.result) + + line_items = permissions.visible_line_items.merge(Spree::LineItem.where(order_id: orders)) + line_items = line_items.supplied_by_any(params[:supplier_id_in]) if params[:supplier_id_in].present? + + line_items_with_hidden_details = line_items.where('"spree_line_items"."id" NOT IN (?)', permissions.editable_line_items) + line_items.select{ |li| line_items_with_hidden_details.include? li }.each do |line_item| + # TODO We should really be hiding customer code here too, but until we + # have an actual association between order and customer, it's a bit tricky + line_item.order.bill_address.assign_attributes(firstname: "HIDDEN", lastname: "", phone: "", address1: "", address2: "", city: "", zipcode: "", state: nil) + line_item.order.ship_address.assign_attributes(firstname: "HIDDEN", lastname: "", phone: "", address1: "", address2: "", city: "", zipcode: "", state: nil) + line_item.order.assign_attributes(email: "HIDDEN") + end + line_items + end + + def rules + case params[:report_type] + when "order_cycle_supplier_totals" + [ { group_by: proc { |line_item| line_item.variant.product.supplier }, + sort_by: proc { |supplier| supplier.name } }, + { group_by: proc { |line_item| line_item.variant.product }, + sort_by: proc { |product| product.name } }, + { group_by: proc { |line_item| line_item.variant }, + sort_by: proc { |variant| variant.full_name } } ] + when "order_cycle_supplier_totals_by_distributor" + [ { group_by: proc { |line_item| line_item.variant.product.supplier }, + sort_by: proc { |supplier| supplier.name } }, + { group_by: proc { |line_item| line_item.variant.product }, + sort_by: proc { |product| product.name } }, + { group_by: proc { |line_item| line_item.variant }, + sort_by: proc { |variant| variant.full_name }, + summary_columns: [ proc { |line_items| "" }, + proc { |line_items| "" }, + proc { |line_items| "" }, + proc { |line_items| "TOTAL" }, + proc { |line_items| "" }, + proc { |line_items| "" }, + proc { |line_items| line_items.sum { |li| li.amount } }, + proc { |line_items| "" } ] }, + { group_by: proc { |line_item| line_item.order.distributor }, + sort_by: proc { |distributor| distributor.name } } ] + when "order_cycle_distributor_totals_by_supplier" + [ { group_by: proc { |line_item| line_item.order.distributor }, + sort_by: proc { |distributor| distributor.name }, + summary_columns: [ proc { |line_items| "" }, + proc { |line_items| "TOTAL" }, + proc { |line_items| "" }, + proc { |line_items| "" }, + proc { |line_items| "" }, + proc { |line_items| "" }, + proc { |line_items| line_items.sum { |li| li.amount } }, + proc { |line_items| line_items.map { |li| li.order }.uniq.sum { |o| o.ship_total } }, + proc { |line_items| "" } ] }, + { group_by: proc { |line_item| line_item.variant.product.supplier }, + sort_by: proc { |supplier| supplier.name } }, + { group_by: proc { |line_item| line_item.variant.product }, + sort_by: proc { |product| product.name } }, + { group_by: proc { |line_item| line_item.variant }, + sort_by: proc { |variant| variant.full_name } } ] + when "order_cycle_customer_totals" + [ { group_by: proc { |line_item| line_item.order.distributor }, + sort_by: proc { |distributor| distributor.name } }, + { group_by: proc { |line_item| line_item.order }, + sort_by: proc { |order| order.bill_address.lastname + " " + order.bill_address.firstname }, + summary_columns: [ + proc { |line_items| line_items.first.order.distributor.name }, + proc { |line_items| line_items.first.order.bill_address.firstname + " " + line_items.first.order.bill_address.lastname }, + proc { |line_items| "" }, + proc { |line_items| "" }, + proc { |line_items| "" }, + proc { |line_items| "TOTAL" }, + proc { |line_items| "" }, + + proc { |line_items| "" }, + proc { |line_items| line_items.sum { |li| li.amount } }, + proc { |line_items| line_items.sum { |li| li.amount_with_adjustments } }, + proc { |line_items| line_items.map { |li| li.order }.uniq.sum { |o| o.admin_and_handling_total } }, + proc { |line_items| line_items.map { |li| li.order }.uniq.sum { |o| o.ship_total } }, + proc { |line_items| line_items.map { |li| li.order }.uniq.sum { |o| o.total } }, + proc { |line_items| line_items.all? { |li| li.order.paid? } ? "Yes" : "No" }, + + proc { |line_items| "" }, + proc { |line_items| "" }, + + proc { |line_items| "" }, + proc { |line_items| "" }, + proc { |line_items| "" }, + proc { |line_items| "" }, + proc { |line_items| "" }, + + proc { |line_items| line_items.first.order.special_instructions } , + proc { |line_items| "" }, + + proc { |line_items| line_items.first.order.order_cycle.andand.name }, + proc { |line_items| line_items.first.order.payments.first.andand.payment_method.andand.name }, + proc { |line_items| "" }, + proc { |line_items| "" }, + + proc { |line_items| "" }, + proc { |line_items| "" }, + proc { |line_items| "" }, + proc { |line_items| "" }, + proc { |line_items| "" } + ] }, + + { group_by: proc { |line_item| line_item.variant.product }, + sort_by: proc { |product| product.name } }, + { group_by: proc { |line_item| line_item.variant }, + sort_by: proc { |variant| variant.full_name } } ] + else + [ { group_by: proc { |line_item| line_item.variant.product.supplier }, + sort_by: proc { |supplier| supplier.name } }, + { group_by: proc { |line_item| line_item.variant.product }, + sort_by: proc { |product| product.name } }, + { group_by: proc { |line_item| line_item.variant }, + sort_by: proc { |variant| variant.full_name } } ] + end + end + + def columns + case params[:report_type] + when "order_cycle_supplier_totals" + [ proc { |line_items| line_items.first.variant.product.supplier.name }, + proc { |line_items| line_items.first.variant.product.name }, + proc { |line_items| line_items.first.variant.full_name }, + proc { |line_items| line_items.sum { |li| li.quantity } }, + proc { |line_items| total_units(line_items) }, + proc { |line_items| line_items.first.price }, + proc { |line_items| line_items.sum { |li| li.amount } }, + proc { |line_items| "" }, + proc { |line_items| "incoming transport" } ] + when "order_cycle_supplier_totals_by_distributor" + [ proc { |line_items| line_items.first.variant.product.supplier.name }, + proc { |line_items| line_items.first.variant.product.name }, + proc { |line_items| line_items.first.variant.full_name }, + proc { |line_items| line_items.first.order.distributor.name }, + proc { |line_items| line_items.sum { |li| li.quantity } }, + proc { |line_items| line_items.first.price }, + proc { |line_items| line_items.sum { |li| li.amount } }, + proc { |line_items| "shipping method" } ] + when "order_cycle_distributor_totals_by_supplier" + [ proc { |line_items| line_items.first.order.distributor.name }, + proc { |line_items| line_items.first.variant.product.supplier.name }, + proc { |line_items| line_items.first.variant.product.name }, + proc { |line_items| line_items.first.variant.full_name }, + proc { |line_items| line_items.sum { |li| li.quantity } }, + proc { |line_items| line_items.first.price }, + proc { |line_items| line_items.sum { |li| li.amount } }, + proc { |line_items| "" }, + proc { |line_items| "shipping method" } ] + when "order_cycle_customer_totals" + rsa = proc { |line_items| line_items.first.order.shipping_method.andand.require_ship_address } + [ + proc { |line_items| line_items.first.order.distributor.name }, + proc { |line_items| line_items.first.order.bill_address.firstname + " " + line_items.first.order.bill_address.lastname }, + proc { |line_items| line_items.first.order.email }, + proc { |line_items| line_items.first.order.bill_address.phone }, + proc { |line_items| line_items.first.variant.product.supplier.name }, + proc { |line_items| line_items.first.variant.product.name }, + proc { |line_items| line_items.first.variant.full_name }, + + proc { |line_items| line_items.sum { |li| li.quantity } }, + proc { |line_items| line_items.sum { |li| li.amount } }, + proc { |line_items| line_items.sum { |li| li.amount_with_adjustments } }, + proc { |line_items| "" }, + proc { |line_items| "" }, + proc { |line_items| "" }, + proc { |line_items| "" }, + + proc { |line_items| line_items.first.order.shipping_method.andand.name }, + proc { |line_items| rsa.call(line_items) ? 'Y' : 'N' }, + + proc { |line_items| line_items.first.order.ship_address.andand.address1 if rsa.call(line_items) }, + proc { |line_items| line_items.first.order.ship_address.andand.address2 if rsa.call(line_items) }, + proc { |line_items| line_items.first.order.ship_address.andand.city if rsa.call(line_items) }, + proc { |line_items| line_items.first.order.ship_address.andand.zipcode if rsa.call(line_items) }, + proc { |line_items| line_items.first.order.ship_address.andand.state if rsa.call(line_items) }, + + proc { |line_items| "" }, + proc { |line_items| line_items.first.variant.product.sku }, + + proc { |line_items| line_items.first.order.order_cycle.andand.name }, + proc { |line_items| line_items.first.order.payments.first.andand.payment_method.andand.name }, + proc { |line_items| line_items.first.order.user.andand.customer_of(line_items.first.order.distributor).andand.code }, + proc { |line_items| line_items.first.order.user.andand.customer_of(line_items.first.order.distributor).andand.tags.andand.join(', ') }, + + proc { |line_items| line_items.first.order.bill_address.andand.address1 }, + proc { |line_items| line_items.first.order.bill_address.andand.address2 }, + proc { |line_items| line_items.first.order.bill_address.andand.city }, + proc { |line_items| line_items.first.order.bill_address.andand.zipcode }, + proc { |line_items| line_items.first.order.bill_address.andand.state } ] + else + [ proc { |line_items| line_items.first.variant.product.supplier.name }, + proc { |line_items| line_items.first.variant.product.name }, + proc { |line_items| line_items.first.variant.full_name }, + proc { |line_items| line_items.sum { |li| li.quantity } }, + proc { |line_items| line_items.first.price }, + proc { |line_items| line_items.sum { |li| li.quantity * li.price } }, + proc { |line_items| "" }, + proc { |line_items| "incoming transport" } ] + end + end + + private + + def total_units(line_items) + return " " if line_items.map{ |li| li.variant.unit_value.nil? }.any? + total_units = line_items.sum do |li| + scale_factor = ( li.product.variant_unit == 'weight' ? 1000 : 1 ) + li.quantity * li.variant.unit_value / scale_factor + end + total_units.round(3) + end + + end +end diff --git a/lib/open_food_network/packing_report.rb b/lib/open_food_network/packing_report.rb new file mode 100644 index 0000000000..fcc5df5e88 --- /dev/null +++ b/lib/open_food_network/packing_report.rb @@ -0,0 +1,124 @@ +module OpenFoodNetwork + class PackingReport + attr_reader :params + def initialize(user, params = {}) + @params = params + @user = user + end + + def header + if is_by_customer? + ["Hub", "Code", "First Name", "Last Name", "Supplier", "Product", "Variant", "Quantity", "TempControlled?"] + else + ["Hub", "Supplier", "Code", "First Name", "Last Name", "Product", "Variant", "Quantity", "TempControlled?"] + end + end + + def search + Spree::Order.complete.not_state(:canceled).managed_by(@user).search(params[:q]) + end + + def orders + search.result + end + + def table_items + @line_items = orders.map do |o| + lis = o.line_items.managed_by(@user) + lis = lis.supplied_by_any(params[:supplier_id_in]) if params[:supplier_id_in].present? + lis + end.flatten + end + + def rules + if is_by_customer? +# customer_rows orders +# table_items = @line_items + + [ + { group_by: proc { |line_item| line_item.order.distributor }, + sort_by: proc { |distributor| distributor.name } }, + { group_by: proc { |line_item| line_item.order }, + sort_by: proc { |order| order.bill_address.lastname }, + summary_columns: [ proc { |line_items| "" }, + proc { |line_items| "" }, + proc { |line_items| "" }, + proc { |line_items| "" }, + proc { |line_items| "" }, + proc { |line_items| "TOTAL ITEMS" }, + proc { |line_items| "" }, + proc { |line_items| line_items.sum { |li| li.quantity } }, + proc { |line_items| "" } ] }, + { group_by: proc { |line_item| line_item.variant.product.supplier }, + sort_by: proc { |supplier| supplier.name } }, + { group_by: proc { |line_item| line_item.variant }, + sort_by: proc { |variant| variant.product.name } } ] + else +# supplier_rows orders +# table_items = supplier_rows orders +# + [ { group_by: proc { |line_item| line_item.order.distributor }, + sort_by: proc { |distributor| distributor.name } }, + { group_by: proc { |line_item| line_item.variant.product.supplier }, + sort_by: proc { |supplier| supplier.name }, + summary_columns: [ proc { |line_items| "" }, + proc { |line_items| "" }, + proc { |line_items| "" }, + proc { |line_items| "" }, + proc { |line_items| "" }, + proc { |line_items| "TOTAL ITEMS" }, + proc { |line_items| "" }, + proc { |line_items| line_items.sum { |li| li.quantity } }, + proc { |line_items| "" } ] }, + { group_by: proc { |line_item| line_item.variant }, + sort_by: proc { |variant| variant.product.name } } ] + end + end + + def columns + if is_by_customer? + [ proc { |line_items| line_items.first.order.distributor.name }, + proc { |line_items| customer_code(line_items.first.order.email) }, + proc { |line_items| line_items.first.order.bill_address.firstname }, + proc { |line_items| line_items.first.order.bill_address.lastname }, + proc { |line_items| line_items.first.variant.product.supplier.name }, + proc { |line_items| line_items.first.variant.product.name }, + proc { |line_items| line_items.first.variant.full_name }, + proc { |line_items| line_items.sum { |li| li.quantity } }, + proc { |line_items| is_temperature_controlled?(line_items.first) } + ] + else + [ + proc { |line_items| line_items.first.order.distributor.name }, + proc { |line_items| line_items.first.variant.product.supplier.name }, + proc { |line_items| customer_code(line_items.first.order.email) }, + proc { |line_items| line_items.first.order.bill_address.firstname }, + proc { |line_items| line_items.first.order.bill_address.lastname }, + proc { |line_items| line_items.first.variant.product.name }, + proc { |line_items| line_items.first.variant.full_name }, + proc { |line_items| line_items.sum { |li| li.quantity } }, + proc { |line_items| is_temperature_controlled?(line_items.first) } + ] + end + end + + private + + def is_temperature_controlled?(line_item) + if line_item.product.shipping_category.andand.temperature_controlled + "Yes" + else + "No" + end + end + + def is_by_customer? + params[:report_type] == "pack_by_customer" + end + + def customer_code(email) + customer = Customer.where(email: email).first + customer.nil? ? "" : customer.code + end + end +end diff --git a/lib/open_food_network/paperclippable.rb b/lib/open_food_network/paperclippable.rb new file mode 100644 index 0000000000..78b4d1a100 --- /dev/null +++ b/lib/open_food_network/paperclippable.rb @@ -0,0 +1,45 @@ +# Allow use of Paperclip's has_attached_file on non-ActiveRecord classes +# https://gist.github.com/basgys/5712426 + +module OpenFoodNetwork + module Paperclippable + def self.included(base) + base.send :extend, ActiveModel::Naming + base.send :extend, ActiveModel::Callbacks + base.send :include, ActiveModel::Validations + base.send :include, Paperclip::Glue + + # Paperclip required callbacks + base.send :define_model_callbacks, :save, only: [:after] + base.send :define_model_callbacks, :commit, only: [:after] + base.send :define_model_callbacks, :destroy, only: [:before, :after] + + # Initialise an ID + base.send :attr_accessor, :id + base.instance_variable_set :@id, 1 + end + + # ActiveModel requirements + def to_model + self + end + + def valid?() true end + def new_record?() true end + def destroyed?() true end + + def save + run_callbacks :save do + end + true + end + + def errors + obj = Object.new + def obj.[](key) [] end + def obj.full_messages() [] end + def obj.any?() false end + obj + end + end +end diff --git a/lib/open_food_network/payments_report.rb b/lib/open_food_network/payments_report.rb new file mode 100644 index 0000000000..4872da6b2f --- /dev/null +++ b/lib/open_food_network/payments_report.rb @@ -0,0 +1,101 @@ +module OpenFoodNetwork + class PaymentsReport + attr_reader :params + def initialize(user, params = {}) + @params = params + @user = user + end + + def header + case params[:report_type] + when "payments_by_payment_type" + ["Payment State", "Distributor", "Payment Type", "Total (#{currency_symbol})"] + when "itemised_payment_totals" + ["Payment State", "Distributor", "Product Total (#{currency_symbol})", "Shipping Total (#{currency_symbol})", "Outstanding Balance (#{currency_symbol})", "Total (#{currency_symbol})"] + when "payment_totals" + ["Payment State", "Distributor", "Product Total (#{currency_symbol})", "Shipping Total (#{currency_symbol})", "Total (#{currency_symbol})", "EFT (#{currency_symbol})", "PayPal (#{currency_symbol})", "Outstanding Balance (#{currency_symbol})"] + else + ["Payment State", "Distributor", "Payment Type", "Total (#{currency_symbol})"] + end + end + + def search + Spree::Order.complete.not_state(:canceled).managed_by(@user).search(params[:q]) + end + + def table_items + orders = search.result + payments = orders.map { |o| o.payments.select { |payment| payment.completed? } }.flatten # Only select completed payments + case params[:report_type] + when "payments_by_payment_type" + payments + when "itemised_payment_totals" + orders + when "payment_totals" + orders + else + payments + end + end + + def rules + case params[:report_type] + when "payments_by_payment_type" + [ { group_by: proc { |payment| payment.order.payment_state }, + sort_by: proc { |payment_state| payment_state } }, + { group_by: proc { |payment| payment.order.distributor }, + sort_by: proc { |distributor| distributor.name } }, + { group_by: proc { |payment| Spree::PaymentMethod.unscoped { payment.payment_method } }, + sort_by: proc { |method| method.name } } ] + when "itemised_payment_totals" + [ { group_by: proc { |order| order.payment_state }, + sort_by: proc { |payment_state| payment_state } }, + { group_by: proc { |order| order.distributor }, + sort_by: proc { |distributor| distributor.name } } ] + when "payment_totals" + [ { group_by: proc { |order| order.payment_state }, + sort_by: proc { |payment_state| payment_state } }, + { group_by: proc { |order| order.distributor }, + sort_by: proc { |distributor| distributor.name } } ] + else + [ { group_by: proc { |payment| payment.order.payment_state }, + sort_by: proc { |payment_state| payment_state } }, + { group_by: proc { |payment| payment.order.distributor }, + sort_by: proc { |distributor| distributor.name } }, + { group_by: proc { |payment| payment.payment_method }, + sort_by: proc { |method| method.name } } ] + end + end + + def columns + case params[:report_type] + when "payments_by_payment_type" + [ proc { |payments| payments.first.order.payment_state }, + proc { |payments| payments.first.order.distributor.name }, + proc { |payments| payments.first.payment_method.name }, + proc { |payments| payments.sum { |payment| payment.amount } } ] + when "itemised_payment_totals" + [ proc { |orders| orders.first.payment_state }, + proc { |orders| orders.first.distributor.name }, + proc { |orders| orders.sum { |o| o.item_total } }, + proc { |orders| orders.sum { |o| o.ship_total } }, + proc { |orders| orders.sum { |o| o.outstanding_balance } }, + proc { |orders| orders.sum { |o| o.total } } ] + when "payment_totals" + [ proc { |orders| orders.first.payment_state }, + proc { |orders| orders.first.distributor.name }, + proc { |orders| orders.sum { |o| o.item_total } }, + proc { |orders| orders.sum { |o| o.ship_total } }, + proc { |orders| orders.sum { |o| o.total } }, + proc { |orders| orders.sum { |o| o.payments.select { |payment| payment.completed? && (payment.payment_method.name.to_s.include? "EFT") }.sum { |payment| payment.amount } } }, + proc { |orders| orders.sum { |o| o.payments.select { |payment| payment.completed? && (payment.payment_method.name.to_s.include? "PayPal") }.sum{ |payment| payment.amount } } }, + proc { |orders| orders.sum { |o| o.outstanding_balance } } ] + else + [ proc { |payments| payments.first.order.payment_state }, + proc { |payments| payments.first.order.distributor.name }, + proc { |payments| payments.first.payment_method.name }, + proc { |payments| payments.sum { |payment| payment.amount } } ] + end + end + end +end diff --git a/lib/open_food_network/permalink_generator.rb b/lib/open_food_network/permalink_generator.rb new file mode 100644 index 0000000000..99d923fd1e --- /dev/null +++ b/lib/open_food_network/permalink_generator.rb @@ -0,0 +1,22 @@ +module PermalinkGenerator + def self.included(base) + base.extend(ClassMethods) + end + + module ClassMethods + def find_available_value(existing, requested) + return requested unless existing.include?(requested) + used_indices = existing.map do |p| + p.slice!(/^#{requested}/) + p.match(/^\d+$/).to_s.to_i + end + options = (1..used_indices.length + 1).to_a - used_indices + requested + options.first.to_s + end + end + + def create_unique_permalink(requested) + existing = self.class.where('id != ?', id).where("permalink LIKE ?", "#{requested}%").pluck(:permalink) + self.class.find_available_value(existing, requested) + end +end diff --git a/lib/open_food_network/products_and_inventory_report.rb b/lib/open_food_network/products_and_inventory_report.rb index 2b797bd944..39109cb104 100644 --- a/lib/open_food_network/products_and_inventory_report.rb +++ b/lib/open_food_network/products_and_inventory_report.rb @@ -1,106 +1,36 @@ +require 'open_food_network/products_and_inventory_report_base' + module OpenFoodNetwork - - class ProductsAndInventoryReport - attr_reader :params - def initialize(user, params = {}) - @user = user - @params = params - end - + class ProductsAndInventoryReport < ProductsAndInventoryReportBase def header [ - "Supplier", - "Producer Suburb", - "Product", - "Product Properties", - "Taxons", - "Variant Value", - "Price", - "Group Buy Unit Quantity", - "Amount" - ] + "Supplier", + "Producer Suburb", + "Product", + "Product Properties", + "Taxons", + "Variant Value", + "Price", + "Group Buy Unit Quantity", + "Amount" + ] end def table variants.map do |variant| - [variant.product.supplier.name, - variant.product.supplier.address.city, - variant.product.name, - variant.product.properties.map(&:name).join(", "), - variant.product.taxons.map(&:name).join(", "), - variant.full_name, - variant.price, - variant.product.group_buy_unit_size, - "" + [ + variant.product.supplier.name, + variant.product.supplier.address.city, + variant.product.name, + variant.product.properties.map(&:name).join(", "), + variant.product.taxons.map(&:name).join(", "), + variant.full_name, + variant.price, + variant.product.group_buy_unit_size, + "" ] end end - def permissions - return @permissions unless @permissions.nil? - @permissions = OpenFoodNetwork::Permissions.new(@user) - end - - def visible_products - return @visible_products unless @visible_products.nil? - @visible_products = permissions.visible_products - end - - def variants - filter(child_variants) - end - - def child_variants - Spree::Variant.where(:is_master => false) - .joins(:product) - .merge(visible_products) - .order("spree_products.name") - end - - def filter(variants) - # NOTE: Ordering matters. - # filter_to_order_cycle and filter_to_distributor return Arrays not Arel - filter_to_distributor filter_to_order_cycle filter_on_hand filter_to_supplier filter_not_deleted variants - end - - def filter_not_deleted(variants) - variants.not_deleted - end - - def filter_on_hand(variants) - if params[:report_type] == "inventory" - variants.where("spree_variants.count_on_hand > 0") - else - variants - end - end - - def filter_to_supplier(variants) - if params[:supplier_id].to_i > 0 - variants.where("spree_products.supplier_id = ?", params[:supplier_id]) - else - variants - end - end - - def filter_to_distributor(variants) - if params[:distributor_id].to_i > 0 - distributor = Enterprise.find params[:distributor_id] - variants.select do |v| - Enterprise.distributing_product(v.product_id).include? distributor - end - else - variants - end - end - - def filter_to_order_cycle(variants) - if params[:order_cycle_id].to_i > 0 - order_cycle = OrderCycle.find params[:order_cycle_id] - variants.select { |v| order_cycle.variants.include? v } - else - variants - end - end end end diff --git a/lib/open_food_network/products_and_inventory_report_base.rb b/lib/open_food_network/products_and_inventory_report_base.rb new file mode 100644 index 0000000000..df5b656848 --- /dev/null +++ b/lib/open_food_network/products_and_inventory_report_base.rb @@ -0,0 +1,76 @@ +module OpenFoodNetwork + class ProductsAndInventoryReportBase + attr_reader :params + + def initialize(user, params = {}) + @user = user + @params = params + end + + def permissions + @permissions ||= OpenFoodNetwork::Permissions.new(@user) + end + + def visible_products + @visible_products ||= permissions.visible_products + end + + def variants + filter(child_variants) + end + + def child_variants + Spree::Variant. + where(is_master: false). + joins(:product). + merge(visible_products). + order('spree_products.name') + end + + def filter(variants) + # NOTE: Ordering matters. + # filter_to_order_cycle and filter_to_distributor return arrays not relations + filter_to_distributor filter_to_order_cycle filter_on_hand filter_to_supplier filter_not_deleted variants + end + + def filter_not_deleted(variants) + variants.not_deleted + end + + def filter_on_hand(variants) + if params[:report_type] == 'inventory' + variants.where('spree_variants.count_on_hand > 0') + else + variants + end + end + + def filter_to_supplier(variants) + if params[:supplier_id].to_i > 0 + variants.where("spree_products.supplier_id = ?", params[:supplier_id]) + else + variants + end + end + + def filter_to_distributor(variants) + if params[:distributor_id].to_i > 0 + distributor = Enterprise.find params[:distributor_id] + variants.select do |v| + Enterprise.distributing_product(v.product_id).include? distributor + end + else + variants + end + end + + def filter_to_order_cycle(variants) + if params[:order_cycle_id].to_i > 0 + order_cycle = OrderCycle.find params[:order_cycle_id] + variants.select { |v| order_cycle.variants.include? v } + else + variants + end + end + end +end diff --git a/lib/open_food_network/rack_request_blocker.rb b/lib/open_food_network/rack_request_blocker.rb new file mode 100644 index 0000000000..71f1c450e2 --- /dev/null +++ b/lib/open_food_network/rack_request_blocker.rb @@ -0,0 +1,77 @@ +# Copied from http://blog.salsify.com/engineering/tearing-capybara-ajax-tests +# https://gist.github.com/jturkel/9317269/raw/ff7838684370fd8a468ffe1e5ce1f3e46ba39951/rack_request_blocker.rb + +require 'atomic' + +# Rack middleware that keeps track of the number of active requests and can block new requests. +class RackRequestBlocker + + @@num_active_requests = Atomic.new(0) + @@block_requests = Atomic.new(false) + + # Returns the number of requests the server is currently processing. + def self.num_active_requests + @@num_active_requests.value + end + + # Prevents the server from accepting new requests. Any new requests will return an HTTP + # 503 status. + def self.block_requests! + @@block_requests.value = true + end + + # Allows the server to accept requests again. + def self.allow_requests! + @@block_requests.value = false + end + + def initialize(app) + @app = app + end + + def call(env) + increment_active_requests + if block_requests? + block_request(env) + else + @app.call(env) + end + ensure + decrement_active_requests + end + + def self.wait_for_requests_complete + self.block_requests! + max_wait_time = 30 + polling_interval = 0.01 + wait_until = Time.now + max_wait_time.seconds + while true + return if self.num_active_requests == 0 + if Time.now > wait_until + raise "Failed waiting for completing requests, #{self.num_active_requests} running." + else + sleep(polling_interval) + end + end + ensure + self.allow_requests! + end + + private + + def block_requests? + @@block_requests.value + end + + def block_request(env) + [503, {}, []] + end + + def increment_active_requests + @@num_active_requests.update { |v| v + 1 } + end + + def decrement_active_requests + @@num_active_requests.update { |v| v - 1 } + end +end diff --git a/lib/open_food_network/reports/bulk_coop_allocation_report.rb b/lib/open_food_network/reports/bulk_coop_allocation_report.rb new file mode 100644 index 0000000000..2bd19ea66a --- /dev/null +++ b/lib/open_food_network/reports/bulk_coop_allocation_report.rb @@ -0,0 +1,50 @@ +require 'open_food_network/reports/bulk_coop_report' + +module OpenFoodNetwork::Reports + class BulkCoopAllocationReport < BulkCoopReport + header "Customer", "Product", "Bulk Unit Size", "Variant", "Variant value", "Variant unit", "Weight", "Sum Total", "Total Available", "Unallocated", "Max quantity excess" + + organise do + group { |li| li.variant.product } + sort &:name + + summary_row do + column { |lis| "TOTAL" } + column { |lis| product_name(lis) } + column { |lis| group_buy_unit_size_f(lis) } + column { |lis| "" } + column { |lis| "" } + column { |lis| "" } + column { |lis| "" } + column { |lis| total_amount(lis) } + column { |lis| total_available(lis) } + column { |lis| remainder(lis) } + column { |lis| max_quantity_excess(lis) } + end + + organise do + group { |li| li.variant } + sort &:full_name + + organise do + group { |li| li.order } + sort { |order| order.to_s } + end + end + end + + columns do + column { |lis| lis.first.order.bill_address.firstname + " " + lis.first.order.bill_address.lastname } + column { |lis| lis.first.variant.product.name } + column { |lis| lis.first.variant.product.group_buy_unit_size || 0.0 } + column { |lis| lis.first.variant.full_name } + column { |lis| OpenFoodNetwork::OptionValueNamer.new(lis.first.variant).value } + column { |lis| OpenFoodNetwork::OptionValueNamer.new(lis.first.variant).unit } + column { |lis| lis.first.variant.weight || 0 } + column { |lis| total_amount(lis) } + column { |lis| "" } + column { |lis| "" } + column { |lis| "" } + end + end +end diff --git a/lib/open_food_network/reports/bulk_coop_report.rb b/lib/open_food_network/reports/bulk_coop_report.rb new file mode 100644 index 0000000000..dbce047963 --- /dev/null +++ b/lib/open_food_network/reports/bulk_coop_report.rb @@ -0,0 +1,66 @@ +require 'open_food_network/reports/report' + +module OpenFoodNetwork::Reports + class BulkCoopReport < Report + + + private + + class << self + + def supplier_name(lis) + lis.first.variant.product.supplier.name + end + + def product_name(lis) + lis.first.variant.product.name + end + + def group_buy_unit_size(lis) + (lis.first.variant.product.group_buy_unit_size || 0.0) / + (lis.first.product.variant_unit_scale || 1) + end + + def group_buy_unit_size_f(lis) + group_buy_unit_size(lis) + end + + def total_amount(lis) + lis.sum { |li| (li.quantity || 0) * scaled_amount(li) } + end + + def units_required(lis) + if group_buy_unit_size(lis).zero? + 0 + else + ( total_amount(lis) / group_buy_unit_size(lis) ).ceil + end + end + + def total_available(lis) + units_required(lis) * group_buy_unit_size(lis) + end + + def remainder(lis) + remainder = total_available(lis) - total_amount(lis) + remainder >= 0 ? remainder : '' + end + + def max_quantity_excess(lis) + max_quantity_amount(lis) - total_amount(lis) + end + + def max_quantity_amount(lis) + lis.sum do |li| + max_quantity = [li.max_quantity || 0, li.quantity || 0].max + max_quantity * scaled_amount(li) + end + end + + def scaled_amount(li) + (li.variant.unit_value || 0) / (li.product.variant_unit_scale || 1) + end + + end + end +end diff --git a/lib/open_food_network/reports/bulk_coop_supplier_report.rb b/lib/open_food_network/reports/bulk_coop_supplier_report.rb new file mode 100644 index 0000000000..df3b40c519 --- /dev/null +++ b/lib/open_food_network/reports/bulk_coop_supplier_report.rb @@ -0,0 +1,50 @@ +require 'open_food_network/reports/bulk_coop_report' + +module OpenFoodNetwork::Reports + class BulkCoopSupplierReport < BulkCoopReport + header "Supplier", "Product", "Bulk Unit Size", "Variant", "Variant value", "Variant unit", "Weight", "Sum Total", "Units Required", "Unallocated", "Max quantity excess" + + organise do + group { |li| li.variant.product.supplier } + sort &:name + + organise do + group { |li| li.variant.product } + sort &:name + + summary_row do + column { |lis| supplier_name(lis) } + column { |lis| product_name(lis) } + column { |lis| group_buy_unit_size_f(lis) } + column { |lis| "" } + column { |lis| "" } + column { |lis| "" } + column { |lis| "" } + column { |lis| total_amount(lis) } + column { |lis| units_required(lis) } + column { |lis| remainder(lis) } + column { |lis| max_quantity_excess(lis) } + end + + organise do + group { |li| li.variant } + sort &:full_name + end + end + end + + columns do + column { |lis| supplier_name(lis) } + column { |lis| product_name(lis) } + column { |lis| group_buy_unit_size_f(lis) } + column { |lis| lis.first.variant.full_name } + column { |lis| OpenFoodNetwork::OptionValueNamer.new(lis.first.variant).value } + column { |lis| OpenFoodNetwork::OptionValueNamer.new(lis.first.variant).unit } + column { |lis| lis.first.variant.weight || 0 } + column { |lis| total_amount(lis) } + column { |lis| '' } + column { |lis| '' } + column { |lis| '' } + end + end +end diff --git a/lib/open_food_network/reports/report.rb b/lib/open_food_network/reports/report.rb new file mode 100644 index 0000000000..d575fcd014 --- /dev/null +++ b/lib/open_food_network/reports/report.rb @@ -0,0 +1,46 @@ +require 'open_food_network/reports/row' +require 'open_food_network/reports/rule' + +module OpenFoodNetwork::Reports + class Report + class_attribute :_header, :_columns, :_rules_head + + # -- API + def header + self._header + end + + def columns + self._columns.to_a + end + + def rules + # Flatten linked list and return as hashes + rules = [] + + rule = self._rules_head + while rule + rules << rule + rule = rule.next + end + + rules.map &:to_h + end + + + # -- DSL + def self.header(*columns) + self._header = columns + end + + def self.columns(&block) + self._columns = Row.new + Blockenspiel.invoke block, self._columns + end + + def self.organise(&block) + self._rules_head = Rule.new + Blockenspiel.invoke block, self._rules_head + end + end +end diff --git a/lib/open_food_network/reports/row.rb b/lib/open_food_network/reports/row.rb new file mode 100644 index 0000000000..01c8dd192e --- /dev/null +++ b/lib/open_food_network/reports/row.rb @@ -0,0 +1,17 @@ +module OpenFoodNetwork::Reports + class Row + include Blockenspiel::DSL + + def initialize + @columns = [] + end + + def column(&block) + @columns << block + end + + def to_a + @columns + end + end +end diff --git a/lib/open_food_network/reports/rule.rb b/lib/open_food_network/reports/rule.rb new file mode 100644 index 0000000000..e2f94f32a2 --- /dev/null +++ b/lib/open_food_network/reports/rule.rb @@ -0,0 +1,32 @@ +require 'open_food_network/reports/row' + +module OpenFoodNetwork::Reports + class Rule + include Blockenspiel::DSL + attr_reader :next + + def group(&block) + @group = block + end + + def sort(&block) + @sort = block + end + + def summary_row(&block) + @summary_row = Row.new + Blockenspiel.invoke block, @summary_row + end + + def organise(&block) + @next = Rule.new + Blockenspiel.invoke block, @next + end + + def to_h + h = {group_by: @group, sort_by: @sort} + h.merge!({summary_columns: @summary_row.to_a}) if @summary_row + h + end + end +end diff --git a/lib/open_food_network/scope_product_to_hub.rb b/lib/open_food_network/scope_product_to_hub.rb index b214abc0d8..d3e018f295 100644 --- a/lib/open_food_network/scope_product_to_hub.rb +++ b/lib/open_food_network/scope_product_to_hub.rb @@ -1,16 +1,23 @@ require 'open_food_network/scope_variant_to_hub' module OpenFoodNetwork - module ScopeProductToHub - def variants_distributed_by(order_cycle, distributor) - super.each { |v| v.scope_to_hub @hub } - end - end - - module ProductScopableToHub - def scope_to_hub(hub) - extend OpenFoodNetwork::ScopeProductToHub + class ScopeProductToHub + def initialize(hub) @hub = hub + @variant_overrides = VariantOverride.indexed @hub + end + + def scope(product) + product.send :extend, OpenFoodNetwork::ScopeProductToHub::ScopeProductToHub + product.instance_variable_set :@hub, @hub + product.instance_variable_set :@variant_overrides, @variant_overrides + end + + + module ScopeProductToHub + def variants_distributed_by(order_cycle, distributor) + super.each { |v| ScopeVariantToHub.new(@hub, @variant_overrides).scope(v) } + end end end end diff --git a/lib/open_food_network/scope_variant_to_hub.rb b/lib/open_food_network/scope_variant_to_hub.rb index 91ed634043..80396ffa17 100644 --- a/lib/open_food_network/scope_variant_to_hub.rb +++ b/lib/open_food_network/scope_variant_to_hub.rb @@ -1,30 +1,48 @@ module OpenFoodNetwork - module ScopeVariantToHub - def price - VariantOverride.price_for(@hub, self) || super + class ScopeVariantToHub + def initialize(hub, variant_overrides=nil) + @hub = hub + @variant_overrides = variant_overrides || VariantOverride.indexed(@hub) end - def price_in(currency) - Spree::Price.new(amount: price, currency: currency) + def scope(variant) + variant.send :extend, OpenFoodNetwork::ScopeVariantToHub::ScopeVariantToHub + variant.instance_variable_set :@hub, @hub + variant.instance_variable_set :@variant_override, @variant_overrides[variant] end - def count_on_hand - VariantOverride.count_on_hand_for(@hub, self) || super - end - def decrement!(attribute, by=1) - if attribute == :count_on_hand && VariantOverride.stock_overridden?(@hub, self) - VariantOverride.decrement_stock! @hub, self, by - else - super + module ScopeVariantToHub + def price + @variant_override.andand.price || super + end + + def price_in(currency) + Spree::Price.new(amount: price, currency: currency) + end + + def count_on_hand + @variant_override.andand.count_on_hand || super + end + + def on_demand + if @variant_override.andand.count_on_hand.present? + # If we're overriding the stock level of an on_demand variant, show it as not + # on_demand, so our stock control can take effect. + false + else + super + end + end + + def decrement!(attribute, by=1) + if attribute == :count_on_hand && @variant_override.andand.stock_overridden? + @variant_override.decrement_stock! by + else + super + end end end - end - module VariantScopableToHub - def scope_to_hub(hub) - extend OpenFoodNetwork::ScopeVariantToHub - @hub = hub - end end end diff --git a/lib/open_food_network/xero_invoices_report.rb b/lib/open_food_network/xero_invoices_report.rb index 1a7f7bd636..cf546efc90 100644 --- a/lib/open_food_network/xero_invoices_report.rb +++ b/lib/open_food_network/xero_invoices_report.rb @@ -5,7 +5,8 @@ module OpenFoodNetwork @opts = opts. reject { |k, v| v.blank? }. - reverse_merge({invoice_date: Date.today, + reverse_merge({report_type: 'summary', + invoice_date: Date.today, due_date: 2.weeks.from_now.to_date, account_code: 'food sales'}) end @@ -19,49 +20,110 @@ module OpenFoodNetwork @orders.each_with_index do |order, i| invoice_number = invoice_number_for(order, i) - rows += rows_for_order(order, invoice_number, @opts) + rows += detail_rows_for_order(order, invoice_number, @opts) if detail? + rows += summary_rows_for_order(order, invoice_number, @opts) end - rows + rows.compact end private - def invoice_number_for(order, i) - @opts[:initial_invoice_number] ? @opts[:initial_invoice_number].to_i+i : order.number + def detail_rows_for_order(order, invoice_number, opts) + rows = [] + + rows += line_item_detail_rows(order, invoice_number, opts) + + if order.account_invoice? + rows += adjustment_detail_rows(order, invoice_number, opts) + end + + rows end - def rows_for_order(order, invoice_number, opts) - [ - summary_row(order, 'Total untaxable produce (no tax)', total_untaxable_products(order), invoice_number, 'GST Free Income', opts), - summary_row(order, 'Total taxable produce (tax inclusive)', total_taxable_products(order), invoice_number, 'GST on Income', opts), - summary_row(order, 'Total untaxable fees (no tax)', total_untaxable_fees(order), invoice_number, 'GST Free Income', opts), - summary_row(order, 'Total taxable fees (tax inclusive)', total_taxable_fees(order), invoice_number, 'GST on Income', opts), - summary_row(order, 'Delivery Shipping Cost (tax inclusive)', total_shipping(order), invoice_number, tax_on_shipping_s(order), opts) - ].compact + def line_item_detail_rows(order, invoice_number, opts) + order.line_items.map do |line_item| + line_item_detail_row(line_item, invoice_number, opts) + end + end + + def line_item_detail_row(line_item, invoice_number, opts) + row(line_item.order, + line_item.product.sku, + line_item.variant.product_and_variant_name, + line_item.quantity.to_s, + line_item.price.to_s, + invoice_number, + tax_type(line_item), + opts) + end + + def adjustment_detail_rows(order, invoice_number, opts) + account_invoice_adjustments(order).map do |adjustment| + adjustment_detail_row(adjustment, invoice_number, opts) + end + end + + def adjustment_detail_row(adjustment, invoice_number, opts) + row(adjustment.source.andand.account_invoice.andand.order, + '', + adjustment.label, + 1, + adjustment.amount, + invoice_number, + tax_type(adjustment), + opts) + end + + def summary_rows_for_order(order, invoice_number, opts) + rows = [] + + rows += produce_summary_rows(order, invoice_number, opts) unless detail? + rows += fee_summary_rows(order, invoice_number, opts) unless detail? && order.account_invoice? + rows += shipping_summary_rows(order, invoice_number, opts) + + rows + end + + def produce_summary_rows(order, invoice_number, opts) + [summary_row(order, 'Total untaxable produce (no tax)', total_untaxable_products(order), invoice_number, 'GST Free Income', opts), + summary_row(order, 'Total taxable produce (tax inclusive)', total_taxable_products(order), invoice_number, 'GST on Income', opts)] + end + + def fee_summary_rows(order, invoice_number, opts) + [summary_row(order, 'Total untaxable fees (no tax)', total_untaxable_fees(order), invoice_number, 'GST Free Income', opts), + summary_row(order, 'Total taxable fees (tax inclusive)', total_taxable_fees(order), invoice_number, 'GST on Income', opts)] + end + + def shipping_summary_rows(order, invoice_number, opts) + [summary_row(order, 'Delivery Shipping Cost (tax inclusive)', total_shipping(order), invoice_number, tax_on_shipping_s(order), opts)] end def summary_row(order, description, amount, invoice_number, tax_type, opts={}) + row order, '', description, '1', amount, invoice_number, tax_type, opts + end + + def row(order, sku, description, quantity, amount, invoice_number, tax_type, opts={}) return nil if amount == 0 - [order.bill_address.full_name, + [order.bill_address.andand.full_name, order.email, - order.bill_address.address1, - order.bill_address.address2, + order.bill_address.andand.address1, + order.bill_address.andand.address2, '', '', - order.bill_address.city, - order.bill_address.state, - order.bill_address.zipcode, - order.bill_address.country.andand.name, + order.bill_address.andand.city, + order.bill_address.andand.state, + order.bill_address.andand.zipcode, + order.bill_address.andand.country.andand.name, invoice_number, order.number, opts[:invoice_date], opts[:due_date], - '', + sku, description, - '1', + quantity, amount, '', opts[:account_code], @@ -76,6 +138,16 @@ module OpenFoodNetwork ] end + def account_invoice_adjustments(order) + order.adjustments. + billable_period. + select { |a| a.source.present? } + end + + def invoice_number_for(order, i) + @opts[:initial_invoice_number] ? @opts[:initial_invoice_number].to_i+i : order.number + end + def total_untaxable_products(order) order.line_items.without_tax.sum &:amount end @@ -100,5 +172,13 @@ module OpenFoodNetwork tax_on_shipping = order.adjustments.shipping.sum(&:included_tax) > 0 tax_on_shipping ? 'GST on Income' : 'GST Free Income' end + + def detail? + @opts[:report_type] == 'detailed' + end + + def tax_type(taxable) + taxable.has_tax? ? 'GST on Income' : 'GST Free Income' + end end end diff --git a/lib/spree/core/controller_helpers/order_decorator.rb b/lib/spree/core/controller_helpers/order_decorator.rb index 88ae412ec2..dc38e861e6 100644 --- a/lib/spree/core/controller_helpers/order_decorator.rb +++ b/lib/spree/core/controller_helpers/order_decorator.rb @@ -3,8 +3,9 @@ Spree::Core::ControllerHelpers::Order.class_eval do order = current_order_without_scoped_variants(create_order_if_necessary) if order + scoper = OpenFoodNetwork::ScopeVariantToHub.new(order.distributor) order.line_items.each do |li| - li.variant.scope_to_hub order.distributor + scoper.scope(li.variant) end end diff --git a/lib/tasks/billing.rake b/lib/tasks/billing.rake new file mode 100644 index 0000000000..6bfd0a53e7 --- /dev/null +++ b/lib/tasks/billing.rake @@ -0,0 +1,13 @@ +namespace :openfoodnetwork do + namespace :billing do + desc 'Update enterprise user invoices' + task update_account_invoices: :environment do + Delayed::Job.enqueue(UpdateAccountInvoices.new) if Spree::Config[:auto_update_invoices] + end + + desc 'Finalize enterprise user invoices' + task finalize_account_invoices: :environment do + Delayed::Job.enqueue(FinalizeAccountInvoices.new) if Spree::Config[:auto_finalize_invoices] + end + end +end diff --git a/public/AveniBla.eot b/public/AveniBla.eot deleted file mode 100644 index 1ae59a3381..0000000000 Binary files a/public/AveniBla.eot and /dev/null differ diff --git a/public/AveniMed.eot b/public/AveniMed.eot deleted file mode 100644 index 61c35da54b..0000000000 Binary files a/public/AveniMed.eot and /dev/null differ diff --git a/public/AvenirLTStd-Black.otf b/public/AvenirLTStd-Black.otf deleted file mode 100644 index 1a934a2af3..0000000000 Binary files a/public/AvenirLTStd-Black.otf and /dev/null differ diff --git a/public/AvenirLTStd-Medium.otf b/public/AvenirLTStd-Medium.otf deleted file mode 100644 index e902718152..0000000000 Binary files a/public/AvenirLTStd-Medium.otf and /dev/null differ diff --git a/public/favicon-staging.ico b/public/favicon-staging.ico index c54a7c4528..91eac8cce8 100644 Binary files a/public/favicon-staging.ico and b/public/favicon-staging.ico differ diff --git a/public/favicon.ico b/public/favicon.ico index 9e0f9a2681..c62c6eeb38 100644 Binary files a/public/favicon.ico and b/public/favicon.ico differ diff --git a/script/archive_branch.sh b/script/archive_branch.sh new file mode 100755 index 0000000000..0177592a54 --- /dev/null +++ b/script/archive_branch.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +# Archive an old branch as a tagged named archive/branch-name to declutter the branch list + +BRANCH_NAME=$1 + +git tag archive/$BRANCH_NAME $BRANCH_NAME +git checkout -q archive/$BRANCH_NAME +git branch -d $BRANCH_NAME +git checkout -q master diff --git a/script/ci/includes.sh b/script/ci/includes.sh index d7619f7d23..619d03153e 100644 --- a/script/ci/includes.sh +++ b/script/ci/includes.sh @@ -5,20 +5,60 @@ function load_environment { fi } +function master_merged { + if [[ `git tag -l "$BUILDKITE_BRANCH"` != '' ]]; then + echo "'$BUILDKITE_BRANCH' is a tag." + if [[ `git tag -l --contains origin/master "$BUILDKITE_BRANCH"` != '' ]]; then + echo "This tag contains the current master." + return 0 + else + echo "This tag does not contain the current master." + return 1 + fi + fi + if [[ `git branch -r --merged origin/$BUILDKITE_BRANCH` == *origin/master* ]]; then + echo "This branch already has the current master merged." + return 0 + fi + return 1 +} + function exit_unless_master_merged { - if [[ `git branch -a --merged origin/$BUILDKITE_BRANCH` != *origin/master* ]]; then + if ! master_merged; then echo "This branch does not have the current master merged. Please merge master and push again." exit 1 fi } function succeed_if_master_merged { - if [[ `git branch -a --merged origin/$BUILDKITE_BRANCH` == *origin/master* ]]; then - echo "This branch already has the current master merged." - exit 0 + master_merged && exit 0 +} + +function set_ofn_commit { + echo "Setting commit to $1" + buildkite-agent meta-data set "openfoodnetwork:git:commit" $1 +} + +function get_ofn_commit { + OFN_COMMIT=`buildkite-agent meta-data get "openfoodnetwork:git:commit"` + + # If we don't catch this failure case, push will execute: + # git push remote :master --force + # Which will delete the master branch on the server + + if [[ `expr length "$OFN_COMMIT"` == 0 ]]; then + echo 'OFN_COMMIT_NOT_FOUND' + else + echo $OFN_COMMIT fi } +function checkout_ofn_commit { + OFN_COMMIT=`buildkite-agent meta-data get "openfoodnetwork:git:commit"` + echo "Checking out stored commit $OFN_COMMIT" + git checkout -qf "$OFN_COMMIT" +} + function drop_and_recreate_database { # Adapted from: http://stackoverflow.com/questions/12924466/capistrano-with-postgresql-error-database-is-being-accessed-by-other-users psql -U openfoodweb postgres <