Merge pull request #5426 from luisramos0/3-0-stable-may13

[Spree 2.1] Merge master into 3-0-stable
This commit is contained in:
Luis Ramos
2020-05-18 15:05:11 +01:00
committed by GitHub
182 changed files with 2432 additions and 729 deletions

View File

@@ -52,3 +52,5 @@ The default admin user is 'ofn@example.com' with 'ofn123' password.
Check the app in the browser at `http://localhost:3000`.
You will then get the trace of the containers in the terminal. You can stop the containers using Ctrl-C in the terminal.
You can find some useful tips and commands [here](https://github.com/openfoodfoundation/openfoodnetwork/wiki/Docker:-useful-tips-and-commands).

View File

@@ -27,6 +27,9 @@ RUN sh -c "echo 'deb https://apt.postgresql.org/pub/repos/apt/ bionic-pgdg main'
apt-get update && \
apt-get install -yqq --no-install-recommends postgresql-client-9.5 libpq-dev
# Install node
RUN apt-get install -y nodejs
# Install Chrome
RUN wget --quiet -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - && \
sh -c "echo 'deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main' >> /etc/apt/sources.list.d/google-chrome.list" && \

View File

@@ -2,6 +2,8 @@
This is a general guide to setting up an Open Food Network development environment on your local machine.
The fastest way to make it work locally is to use Docker, see the [Docker setup guide](DOCKER.md).
The following guides are located in the wiki and provide more OS-specific step-by-step instructions:
- [Ubuntu Setup Guide][ubuntu]
@@ -11,7 +13,7 @@ The following guides are located in the wiki and provide more OS-specific step-b
### Dependencies
* Rails 3.2.x
* Ruby 2.1.9
* Ruby 2.3.7
* PostgreSQL database
* PhantomJS (for testing)
* See Gemfile for a list of gems required
@@ -58,10 +60,10 @@ Now, your dreams of spinning up a development server can be realised:
bundle exec rails server
To login as Spree default user, use:
To login as the default user, use:
email: spree@example.com
password: spree123
email: ofn@example.com
password: ofn123
### Testing

View File

@@ -11,6 +11,7 @@ gem 'rails_safe_tasks', '~> 1.0'
gem "activerecord-import"
gem "catalog", path: "./engines/catalog"
gem 'dfc_provider', path: './engines/dfc_provider'
gem "order_management", path: "./engines/order_management"
gem 'web', path: './engines/web'

View File

@@ -60,6 +60,13 @@ PATH
specs:
catalog (0.0.1)
PATH
remote: engines/dfc_provider
specs:
dfc_provider (0.0.1)
jwt (~> 2.2)
rspec (~> 3.9)
PATH
remote: engines/order_management
specs:
@@ -198,7 +205,7 @@ GEM
activerecord (>= 3.2.0, < 5.0)
fog (~> 1.0)
rails (>= 3.2.0, < 5.0)
ddtrace (0.35.0)
ddtrace (0.35.2)
msgpack
debugger-linecache (1.2.0)
delayed_job (4.1.8)
@@ -712,6 +719,7 @@ DEPENDENCIES
delayed_job_web
devise (~> 3.0.1)
devise-encryptable
dfc_provider!
diffy
eventmachine (>= 1.2.3)
factory_bot_rails (= 4.10.0)

Binary file not shown.

After

Width:  |  Height:  |  Size: 540 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 534 B

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="28px" height="33px" enable-background="new 0 0 28 33" version="1.1" viewBox="0 0 28 33" xmlns="http://www.w3.org/2000/svg"><path d="M14,25c-6.059,0-10.988,1.679-10.988,3.333c0,2.485,10.307,4.542,10.746,4.643 C13.828,32.992,13.914,33,14,33c0.084,0,0.17-0.008,0.239-0.023c0.439-0.099,10.749-2.114,10.749-4.643 C24.988,26.679,20.059,25,14,25z" fill="#282828" opacity=".25"/><path d="M14,0C6.28,0,0,6.717,0,13.332c0,9.941,13.132,18.169,13.691,18.572C13.78,31.968,13.891,32,14,32 c0.107,0,0.217-0.031,0.305-0.094C14.864,31.511,28,23.45,28,13.332C28,6.717,21.72,0,14,0z" fill="#fff"/><g><g fill="#0b8c61"><path d="m14 0c-7.72 0-14 6.717-14 13.333 0 9.941 13.132 18.169 13.691 18.571 0.089 0.064 0.2 0.096 0.309 0.096 0.107 0 0.217-0.031 0.305-0.094 0.559-0.395 13.695-8.456 13.695-18.573 0-6.616-6.28-13.333-14-13.333zm9.5 12.057c0 0.863-0.567 1.661-1.325 1.942l-1.025 5.889c-0.015 0.976-0.889 1.831-1.94 1.831h-10.466c-1.052 0-1.925-0.855-1.947-1.906l-1.024-5.828c-0.737-0.294-1.273-1.075-1.273-1.928v-0.827c0-1.074 0.874-1.948 1.948-1.948h2.302l1.396-2.247c0.4-0.698 1.417-0.978 2.145-0.555 0.755 0.435 1.015 1.403 0.58 2.159l-0.41 0.642h2.662l-0.39-0.61c-0.227-0.391-0.284-0.823-0.174-1.235 0.109-0.406 0.37-0.745 0.734-0.955 0.716-0.417 1.739-0.148 2.159 0.579l1.381 2.223h2.718c1.074 0 1.948 0.874 1.948 1.948v0.826z"/><rect x="3.6006" y="6.9915" width="20.415" height="15.521"/><rect x="8.9841" y="5.6631" width="10.487" height="3.6356"/></g><text x="5.0698848" y="20.243376" fill="#000000" font-family="sans-serif" font-size="16px" style="line-height:1.25" xml:space="preserve"><tspan x="5.0698848" y="20.243376" fill="#ffffff" font-family="Arial" font-size="16px" font-weight="bold">1+</tspan></text></g></svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -29,7 +29,7 @@ angular.module("admin.enterprises")
# from a directive "nav-check" in the page - if we pass it here it will be called in the test suite,
# and on all new uses of this contoller, and we might not want that.
enterpriseNavCallback = ->
if $scope.enterprise_form != undefined && $scope.enterprise_form.$dirty
if $scope.enterprise_form?.$dirty
t('admin.unsaved_confirm_leave')
# Register the NavigationCheck callback

View File

@@ -1,5 +1,5 @@
angular.module('admin.orderCycles')
.controller 'AdminEditOrderCycleCtrl', ($scope, $controller, $filter, $location, $window, OrderCycle, Enterprise, EnterpriseFee, StatusMessage, Schedules, RequestMonitor, ocInstance) ->
.controller 'AdminEditOrderCycleCtrl', ($scope, $controller, $filter, $location, $window, OrderCycle, Enterprise, EnterpriseFee, StatusMessage, Schedules, RequestMonitor, NavigationCheck, ocInstance) ->
$controller('AdminOrderCycleBasicCtrl', {$scope: $scope, ocInstance: ocInstance})
order_cycle_id = $location.absUrl().match(/\/admin\/order_cycles\/(\d+)/)[1]
@@ -18,5 +18,12 @@ angular.module('admin.orderCycles')
$scope.submit = ($event, destination) ->
$event.preventDefault()
NavigationCheck.clear()
StatusMessage.display 'progress', t('js.saving')
OrderCycle.update(destination, $scope.order_cycle_form)
warnAboutUnsavedChanges = ->
if $scope.order_cycle_form?.$dirty
t('admin.unsaved_confirm_leave')
NavigationCheck.register(warnAboutUnsavedChanges)

View File

@@ -1,4 +1,4 @@
Darkswarm.controller "ProductsCtrl", ($scope, $filter, $rootScope, Products, OrderCycle, OrderCycleResource, FilterSelectorsService, Cart, Dereferencer, Taxons, Properties, currentHub, $timeout) ->
Darkswarm.controller "ProductsCtrl", ($scope, $sce, $filter, $rootScope, Products, OrderCycle, OrderCycleResource, FilterSelectorsService, Cart, Dereferencer, Taxons, Properties, currentHub, $timeout) ->
$scope.Products = Products
$scope.Cart = Cart
$scope.query = ""
@@ -10,6 +10,7 @@ Darkswarm.controller "ProductsCtrl", ($scope, $filter, $rootScope, Products, Ord
$scope.order_cycle = OrderCycle.order_cycle
$scope.supplied_taxons = null
$scope.supplied_properties = null
$scope.showFilterSidebar = false
$rootScope.$on "orderCycleSelected", ->
$scope.update_filters()
@@ -75,15 +76,24 @@ Darkswarm.controller "ProductsCtrl", ($scope, $filter, $rootScope, Products, Ord
$scope.appliedTaxonsList = ->
$scope.activeTaxons.map( (taxon_id) ->
Taxons.taxons_by_id[taxon_id].name
).join(" #{t('products_or')} ") if $scope.activeTaxons?
).join($scope.filtersJoinWord()) if $scope.activeTaxons?
$scope.appliedPropertiesList = ->
$scope.activeProperties.map( (property_id) ->
Properties.properties_by_id[property_id].name
).join(" #{t('products_or')} ") if $scope.activeProperties?
).join($scope.filtersJoinWord()) if $scope.activeProperties?
$scope.filtersJoinWord = ->
$sce.trustAsHtml(" <span class='join-word'>#{t('products_or')}</span> ")
$scope.clearAll = ->
$scope.clearQuery()
$scope.clearFilters()
$scope.clearQuery = ->
$scope.query = ""
$scope.clearFilters = ->
$scope.taxonSelectors.clearAll()
$scope.propertySelectors.clearAll()
@@ -94,3 +104,9 @@ Darkswarm.controller "ProductsCtrl", ($scope, $filter, $rootScope, Products, Ord
$scope.Products.products = []
$scope.update_filters()
$scope.loadProducts()
$scope.filtersCount = () ->
$scope.taxonSelectors.totalActive() + $scope.propertySelectors.totalActive()
$scope.toggleFilterSidebar = ->
$scope.showFilterSidebar = !$scope.showFilterSidebar

View File

@@ -1,2 +1,2 @@
Darkswarm.controller "ProducersTabCtrl", ($scope, Shopfront, EnterpriseModal) ->
Darkswarm.controller "ProducersTabCtrl", ($scope, Shopfront) ->
$scope.shopfront = Shopfront.shopfront

View File

@@ -0,0 +1,11 @@
Darkswarm.directive "darkerBackground", ->
restrict: "A"
link: (scope, elm, attr)->
toggleClass = (value) ->
elm.closest('.page-view').toggleClass("with-darker-background", value)
toggleClass(true)
# if an OrderCycle is selected, disable darker background
scope.$watch 'order_cycle.order_cycle_id', (newvalue, oldvalue) ->
toggleClass(false) if newvalue

View File

@@ -7,4 +7,4 @@ Darkswarm.directive "enterpriseModal", (EnterpriseModal) ->
elem.on "click", (event) =>
event.stopPropagation()
scope.modalInstance = EnterpriseModal.open scope.enterprise
scope.modalInstance = EnterpriseModal.open scope.enterprise

View File

@@ -0,0 +1,6 @@
Darkswarm.directive "focusSearch", ->
restrict: 'A'
link: (scope, element, attr)->
element.bind 'click', (event) ->
# Focus seach field, ready for typing
$(element).siblings('#search').focus()

View File

@@ -1,4 +1,4 @@
Darkswarm.factory 'Cart', (CurrentOrder, Variants, $timeout, $http, $modal, $rootScope, $resource, localStorageService) ->
Darkswarm.factory 'Cart', (CurrentOrder, Variants, $timeout, $http, $modal, $rootScope, $resource, localStorageService, RailsFlashLoader) ->
# Handles syncing of current cart/order state to server
new class Cart
dirty: false
@@ -50,7 +50,7 @@ Darkswarm.factory 'Cart', (CurrentOrder, Variants, $timeout, $http, $modal, $roo
@popQueue() if @update_enqueued
.error (response, status)=>
@scheduleRetry(status)
RailsFlashLoader.loadFlash({error: t('js.cart.add_to_cart_failed')})
@update_running = false
compareAndNotifyStockLevels: (stockLevels) =>
@@ -87,13 +87,6 @@ Darkswarm.factory 'Cart', (CurrentOrder, Variants, $timeout, $http, $modal, $roo
max_quantity: li.max_quantity
{variants: variants}
scheduleRetry: (status) =>
console.log "Error updating cart: #{status}. Retrying in 3 seconds..."
$timeout =>
console.log "Retrying cart update"
@orderChanged()
, 3000
saved: =>
@dirty = false
$(window).unbind "beforeunload"

View File

@@ -0,0 +1,10 @@
Darkswarm.factory "EnterpriseListModal", ($modal, $rootScope, $http, EnterpriseModal)->
new class EnterpriseListModal
open: (enterprises)->
scope = $rootScope.$new(true)
scope.enterprises = enterprises
scope.openModal = EnterpriseModal.open
if Object.keys(enterprises).length > 1
$modal.open(templateUrl: "enterprise_list_modal.html", scope: scope)
else
EnterpriseModal.open enterprises[Object.keys(enterprises)[0]]

View File

@@ -1,21 +1,39 @@
Darkswarm.factory "OfnMap", (Enterprises, EnterpriseModal) ->
Darkswarm.factory "OfnMap", (Enterprises, EnterpriseListModal, MapConfiguration) ->
new class OfnMap
constructor: ->
@enterprises = @enterprise_markers(Enterprises.enterprises)
@enterprises = @enterprises.filter (enterprise) ->
enterprise.latitude != null || enterprise.longitude != null # Remove enterprises w/o lat or long
@coordinates = {}
@enterprises = Enterprises.enterprises.filter (enterprise) ->
# Remove enterprises w/o lat or long
enterprise.latitude != null || enterprise.longitude != null
@enterprises = @enterprise_markers(@enterprises)
enterprise_markers: (enterprises) ->
@extend(enterprise) for enterprise in enterprises
enterprise_hash: (hash, enterprise) ->
hash[enterprise.id] = { id: enterprise.id, name: enterprise.name, icon: enterprise.icon_font }
hash
extend_marker: (marker, enterprise) ->
marker.latitude = enterprise.latitude
marker.longitude = enterprise.longitude
marker.icon = enterprise.icon
marker.id = [enterprise.id]
marker.enterprises = @enterprise_hash({}, enterprise)
# Adding methods to each enterprise
extend: (enterprise) ->
new class MapMarker
# 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
id: enterprise.id
reveal: =>
EnterpriseModal.open enterprise
marker = @coordinates[[enterprise.latitude, enterprise.longitude]]
if marker
marker.icon = MapConfiguration.options.cluster_icon
@enterprise_hash(marker.enterprises, enterprise)
marker.id.push(enterprise.id)
else
marker = new class MapMarker
# We cherry-pick attributes because GMaps tries to crawl
# our data, and our data is cyclic, so it breaks
reveal: =>
EnterpriseListModal.open this.enterprises
@extend_marker(marker, enterprise)
@coordinates[[enterprise.latitude, enterprise.longitude]] = marker
marker

View File

@@ -4,6 +4,7 @@ Darkswarm.factory "MapConfiguration", ->
center:
latitude: -37.4713077
longitude: 144.7851531
cluster_icon: 'assets/map_009-cluster.svg'
zoom: 12
additional_options:
# mapTypeId: 'satellite'

View File

@@ -0,0 +1,2 @@
%ng-include{src: "'partials/enterprise_listing.html'"}
%ng-include{src: "'partials/close.html'"}

View File

@@ -1,4 +1,3 @@
%ul
%active-selector{ ng: { repeat: "selector in allSelectors", show: "ifDefined(selector.fits, true)" } }
%render-svg{path: "{{selector.object.icon}}", ng: { if: "selector.object.icon"} }
%span{"ng-bind" => "::selector.object.name"}

View File

@@ -0,0 +1,10 @@
.modal-list
.row{"ng-repeat" => "(id, enterprise) in ::enterprises"}
.highlight
.highlight-top.row.enterprise
.small-12.medium-12.large-12.columns
%h4
%a.heading{"ng-click" => "::openModal(enterprise)"}
%i{"ng-class" => "enterprise.icon"}
%span{"ng-bind" => "enterprise.name"}
%img.hero-img{"ng-src" => "{{::enterprise.promo_image}}"}

View File

@@ -1,2 +1,2 @@
%button.graph-button{"ng-class" => "{open: tt_isOpen}"}
%button.graph-button{"ng-class" => "{open: tt_isOpen}", type: 'button'}
/ %i.ofn-i_058-graph

View File

@@ -2,6 +2,7 @@
@import "branding";
@import "big-input";
@import "animations";
@import "variables";
@mixin filter-selector($base-clr, $border-clr, $hover-clr) {
&.inline-block, ul.inline-block {
@@ -14,7 +15,7 @@
@include border-radius(0);
padding: 0;
margin: 0 0 0.25rem 0.25rem;
margin: 0 0.5rem 0.5rem 0;
&:hover, &:focus {
background: transparent;
@@ -107,26 +108,63 @@
// Alert when search, taxon, filter is triggered
.alert-box.search-alert {
background-color: $clr-yellow-light;
border-color: $clr-yellow-light;
background-color: $white;
color: #777;
font-size: 0.75rem;
padding: 0.5rem 0.75rem;
font-size: 1em;
padding: 0.35em 0 0;
border: 0;
margin: 0;
span.applied-properties {
color: #333;
.clear-all {
color: $grey-500;
margin-left: 1.5em;
&:hover {
color: $grey-600;
}
}
span.applied-taxons {
color: $clr-blue;
.no-results-bar {
@include breakpoint(desktop) {
text-align: center;
}
}
span.applied-search {
color: $clr-brick;
.no-results {
color: $grey-800;
font-style: italic;
font-size: 1.25em;
}
span.filter-label {
opacity: 0.75;
.clear-search {
background-color: transparent;
padding: 0;
margin: 0;
color: $orange-500;
font-size: 1.25em;
&:hover {
color: $orange-400;
}
}
span {
color: $grey-800;
font-style: italic;
&.applied-taxons, &.applied-properties {
color: $clr-blue;
font-weight: bold;
.join-word {
font-weight: normal;
}
}
&.applied-search {
font-weight: bold;
color: $teal-500;
}
}
}
@@ -140,25 +178,6 @@
.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
@@ -170,4 +189,8 @@
&.property-selectors {
@include filter-selector(#666, #ccc, #777);
}
ul {
margin: 0;
}
}

View File

@@ -7,13 +7,6 @@
// #search
@include placeholder(rgba(0, 0, 0, 0.4), #777);
input#search {
@include medium-input(rgba(0, 0, 0, 0.3), #777, $clr-brick);
// avoid zoom on iphone, see issue #4535
font-size: 1rem;
}
// ordering
product {
input {
@@ -30,22 +23,22 @@
border-color: #b3b3b3;
text-align: right;
@media all and (max-width: 1024px) {
@include breakpoint(desktop) {
width: 8rem;
}
@media all and (max-width: 768px) {
@include breakpoint(tablet) {
width: 7rem;
}
@media all and (max-width: 640px) {
@include breakpoint(phablet) {
float: left !important;
font-size: 0.75rem;
padding-left: 0.25rem;
padding-right: 0.25rem;
}
@media all and (max-width: 480px) {
@include breakpoint(mobile) {
width: 5.8rem;
}
@@ -69,15 +62,15 @@
input.bulk {
width: 5rem;
@media all and (max-width: 1024px) {
@include breakpoint(desktop) {
width: 4rem;
}
@media all and (max-width: 768px) {
@include breakpoint(tablet) {
width: 3.5rem;
}
@media all and (max-width: 480px) {
@include breakpoint(mobile) {
width: 2.8rem;
}
}
@@ -93,7 +86,7 @@
.bulk-input-container {
float: right;
@media all and (max-width: 640px) {
@include breakpoint(phablet) {
float: left !important;
}

View File

@@ -1,11 +1,13 @@
@import "mixins";
@import "typography";
@import "variables";
ordercycle {
float: right;
background: $grey-050;
color: $grey-800;
width: 100%;
border-radius: 0.5em 0.5em 0 0;
border-radius: $radius-medium $radius-medium 0 0;
margin-top: 1em;
padding: 1em 1.25em 0;
@@ -17,7 +19,7 @@ ordercycle {
margin-right: 0.3rem;
}
@media all and (max-width: 1024px) {
@include breakpoint(desktop) {
float: none;
padding: 0.5em 1em;
width: 100%;
@@ -33,7 +35,7 @@ ordercycle {
}
}
@media all and (max-width: 480px) {
@include breakpoint(mobile) {
padding: 0.5em 1em 0.75em;
}
@@ -41,15 +43,15 @@ ordercycle {
border: 1px solid $teal-300;
display: inline-block;
font-size: 1em;
border-radius: 0.25em;
border-radius: $radius-small;
.select-label {
background-color: rgba($teal-300, 0.5);
display: inline-block;
border-radius: 0.25em 0 0 0.25em;
border-radius: $radius-small 0 0 $radius-small;
float: left;
font-size: 1em;
line-height: 1.5em;
line-height: 1.3em;
padding: 0.5em 0.75em;
height: 2.35em;
@@ -60,6 +62,15 @@ ordercycle {
}
select {
background-image: url('/assets/white-caret.svg');
}
p {
text-align: left;
}
select,
p {
width: inherit;
display: inline-block;
color: $white;
@@ -67,29 +78,34 @@ ordercycle {
border: 0;
margin-bottom: 0;
font-size: 1em;
line-height: 1.5em;
line-height: 1.3em;
padding: 0.5em 1.25em 0.5em 0.75em;
height: 2.35em;
background-image: url('/assets/white-caret.svg');
background-size: 30px auto;
border-radius: 0 0.25em 0.25em 0;
border-radius: 0 $radius-small $radius-small 0;
min-width: 13em;
@media all and (max-width: 480px) {
@include breakpoint(mobile) {
width: 100%;
min-width: 0;
}
}
@media all and (max-width: 1024px) {
option {
color: $grey-700;
}
@include breakpoint(desktop) {
float: none;
margin-right: 1em;
}
@media all and (max-width: 768px) {
@include breakpoint(tablet) {
float: none;
margin-right: 0;
}
@media all and (max-width: 480px) {
@include breakpoint(mobile) {
display: flex;
}
}
@@ -102,7 +118,7 @@ ordercycle {
padding: 0.5em 0;
span {
@media all and (max-width: 768px) {
@include breakpoint(tablet) {
font-size: 0.875em;
}
}
@@ -136,7 +152,7 @@ shop ordercycle {
color: $white;
padding: 0 0 12px;
@media all and (max-width: 1024px) {
@include breakpoint(desktop) {
float: none;
display: inline-block;
padding: 0.2em 0 0;
@@ -144,7 +160,7 @@ shop ordercycle {
margin-right: 1em;
}
@media all and (max-width: 768px) {
@include breakpoint(tablet) {
float: none;
padding: 0 0 10px;
}

View File

@@ -114,7 +114,7 @@ button.graph-button {
}
}
@media all and (max-width: 768px) {
@include breakpoint(tablet) {
// Hide for small
display: none;
}

View File

@@ -1,3 +1,4 @@
@import "mixins";
@import "branding";
@import "animations";
@@ -14,11 +15,11 @@
// outline: 1px solid red
@media all and (max-width: 768px) {
@include breakpoint(tablet) {
font-size: 0.875rem;
}
@media all and (max-width: 640px) {
@include breakpoint(phablet) {
font-size: 0.75rem;
}
}
@@ -56,13 +57,13 @@
.variant-name {
padding-left: 7.9375rem;
@media all and (max-width: 768px) {
@include breakpoint(tablet) {
padding-left: 4.9375rem;
}
}
.variant-name {
@media all and (max-width: 640px) {
@include breakpoint(phablet) {
background: #333;
color: white;
padding-left: 0.9375rem;
@@ -82,7 +83,7 @@
font-size: 0.875rem;
overflow: hidden;
@media all and (max-width: 768px) {
@include breakpoint(tablet) {
font-size: 0.75rem;
}
}
@@ -92,7 +93,7 @@
padding-left: 0.25rem;
padding-right: 0.25rem;
@media all and (max-width: 640px) {
@include breakpoint(phablet) {
text-align: right;
}
}
@@ -106,7 +107,7 @@
color: $med-drk-grey;
}
@media all and (max-width: 640px) {
@include breakpoint(phablet) {
background: #777;
color: $disabled-med;
@@ -132,7 +133,7 @@
padding-bottom: 1em;
line-height: 1;
@media all and (max-width: 768px) {
@include breakpoint(tablet) {
padding-top: 0.65rem;
padding-bottom: 0.65rem;
}
@@ -141,11 +142,11 @@
.summary-header {
padding-left: 7.9375rem;
@media all and (max-width: 768px) {
@include breakpoint(tablet) {
padding-left: 4.9375rem;
}
@media all and (max-width: 640px) {
@include breakpoint(phablet) {
padding-left: 0.9375rem;
}

View File

@@ -1,3 +1,4 @@
@import "mixins";
@import "branding";
@import "animations";
@@ -56,7 +57,7 @@
}
}
@media all and (max-width: 768px) {
@include breakpoint(tablet) {
top: 2px;
width: 4rem;
height: 4rem;
@@ -70,7 +71,7 @@
}
}
@media all and (max-width: 640px) {
@include breakpoint(phablet) {
display: none;
width: 0rem;
height: 0rem;

View File

@@ -1,3 +1,5 @@
@import "mixins";
.darkswarm {
products {
product {
@@ -10,7 +12,7 @@
padding-top: 0.25rem;
z-index: 999999;
@media all and (max-width: 480px) {
@include breakpoint(mobile) {
background-size: 28px 32px;
min-height: 32px;
width: 28px;
@@ -27,11 +29,11 @@
}
}
@media all and (max-width: 768px) {
@include breakpoint(tablet) {
margin-top: -0.85rem;
}
@media all and (max-width: 480px) {
@include breakpoint(mobile) {
render-svg {
svg {
width: 18px;

View File

@@ -1,5 +1,6 @@
@import "branding";
@import "mixins";
@import "variables";
.account-summary {
color: #4a4a4a;
@@ -99,7 +100,7 @@
table {
width: 100%;
border-radius: 0.5em 0.5em 0 0;
border-radius: $radius-medium $radius-medium 0 0;
tr:nth-of-type(even) {
background: transparent;

View File

@@ -26,7 +26,7 @@
display: block;
border: 0;
@media all and (max-width: 640px) {
@include breakpoint(phablet) {
margin-bottom: 1rem;
}
@@ -45,7 +45,7 @@
}
// Generic text resize
@media all and (max-width: 640px) {
@include breakpoint(phablet) {
&, & * {
font-size: 0.875rem;
}
@@ -114,7 +114,7 @@
.fat > div {
border-top: 1px solid #aaa;
@media all and (max-width: 640px) {
@include breakpoint(phablet) {
margin-top: 1em;
}

View File

@@ -16,7 +16,7 @@
margin-top: 2px;
@media all and (max-width: 640px) {
@include breakpoint(phablet) {
margin-bottom: 1em;
}
}

View File

@@ -23,7 +23,7 @@
box-shadow: none;
color: $inputactv;
@media all and (max-width: 640px) {
@include breakpoint(phablet) {
font-size: 1.25rem;
}

View File

@@ -38,14 +38,18 @@ $med-drk-grey: #444;
$dark-grey: #333;
$light-grey: #ddd;
$light-grey-transparency: rgba(0, 0, 0, .1);
$very-light-grey-transparency: rgba(0, 0, 0, .05);
$black: #000;
$white: #fff;
$grey-050: #f7f7f7;
$grey-100: #e6e6e6;
$grey-200: #ddd;
$grey-300: #ccc;
$grey-400: #bbb;
$grey-500: #999;
$grey-600: #777;
$grey-650: #666;
$grey-700: #555;
$grey-800: #333;

View File

@@ -13,7 +13,7 @@
checkout {
display: block;
@media all and (max-width: 640px) {
@include breakpoint(phablet) {
&.row .row {
margin-left: 0;
margin-right: 0;
@@ -24,7 +24,7 @@ checkout {
.button, table {
width: 100%;
}
@media all and (max-width: 640px) {
@include breakpoint(phablet) {
form.edit_order {
border: 1px solid $disabled-bright;
margin-bottom: 2rem;

View File

@@ -1,3 +1,4 @@
@import "mixins";
@import 'typography';
section {
@@ -34,7 +35,7 @@ section {
@include headingFont;
}
@media all and (max-width: 768px) {
@include breakpoint(tablet) {
location, location + small {
display: block;
}
@@ -44,7 +45,7 @@ section {
margin-top: 0;
padding-top: 0.45em;
@media all and (max-width: 768px) {
@include breakpoint(tablet) {
margin-bottom: 8px;
}
}

View File

@@ -1,3 +1,4 @@
@import "mixins";
@import "typography";
$large-menu-height: 4.6875rem;
@@ -97,7 +98,7 @@ body.embedded {
display: none;
}
@media all and (max-width: 640px) {
@include breakpoint(phablet) {
nav.top-bar {
height: 3.4rem;
padding: 0.2rem $gutter-width;
@@ -141,7 +142,7 @@ body.embedded {
}
}
@media all and (max-width: 480px) {
@include breakpoint(mobile) {
ul.left li.powered-by span {
display: none;
}

View File

@@ -66,7 +66,7 @@
font-weight: 300;
}
@media all and (max-width: 768px) {
@include breakpoint(tablet) {
h2 {
font-size: 52px;
}
@@ -87,7 +87,7 @@
padding-bottom: 0;
}
@media all and (max-width: 640px) {
@include breakpoint(phablet) {
.row .row {
padding: 0;
}
@@ -139,7 +139,7 @@
font-weight: 300;
color: $brand-colour;
@media all and (max-width: 640px) {
@include breakpoint(phablet) {
font-size: 45px;
}
}

View File

@@ -45,7 +45,7 @@
}
//Hub Link
@media all and (max-width: 640px) {
@include breakpoint(phablet) {
a.hub {
display: block;
}
@@ -67,7 +67,7 @@
.active_table_row {
border: 1px solid transparent;
@media all and (max-width: 640px) {
@include breakpoint(phablet) {
border-color: $clr-brick-light;
}
@@ -85,7 +85,7 @@
}
&.open, &.closed {
@media all and (max-width: 640px) {
@include breakpoint(phablet) {
.active_table_row:first-child .skinny-head {
background-color: $clr-brick-light;
@@ -164,7 +164,7 @@
}
}
@media all and (max-width: 640px) {
@include breakpoint(phablet) {
.active_table_row:first-child .skinny-head {
background-color: rgba(255, 255, 255, 0.85);
}
@@ -218,7 +218,7 @@
}
// Small devices
@media all and (max-width: 640px) {
@include breakpoint(phablet) {
.active_table_row:first-child .skinny-head {
background-color: $disabled-bright;
}
@@ -226,7 +226,7 @@
}
// Small devices
@media all and (max-width: 640px) {
@include breakpoint(phablet) {
.active_table_row, .active_table_row:first-child, .active_table_row:last-child {
border-color: $disabled-bright;
background-color: transparent;
@@ -253,7 +253,7 @@
cursor: auto;
}
@media all and (max-width: 640px) {
@include breakpoint(phablet) {
border-color: transparent;
}
}

View File

@@ -13,7 +13,7 @@
&.placeholder {
opacity: 0.35;
@media all and (max-width: 1024px) {
@include breakpoint(desktop) {
display: none;
}
}
@@ -31,7 +31,7 @@
max-height: 260px;
overflow: hidden;
@media all and (max-width: 640px) {
@include breakpoint(phablet) {
min-height: 68px;
}
}

View File

@@ -1,6 +1,7 @@
// Place all the styles related to the map controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/
@import "mixins";
@import "big-input";
.map-container {
@@ -29,7 +30,7 @@
margin-top: 1.2rem;
margin-left: 1rem;
@media all and (max-width: 768px) {
@include breakpoint(tablet) {
width: 80%;
}

View File

@@ -171,7 +171,7 @@ nav.top-bar {
.tab-bar {
background-color: white;
border-bottom: 1px solid $light-grey-transparency;
height: 2.8em;
height: $mobile-nav-height;
position: fixed;
width: 100%;
z-index: 1;
@@ -210,6 +210,10 @@ nav.top-bar {
}
}
.off-canvas-wrap {
overflow: inherit;
}
.off-canvas-list li.language-switcher ul li {
list-style-type: none;
padding-left: 0.5em;

View File

@@ -16,7 +16,7 @@
padding-top: 100px;
padding-bottom: 100px;
@media all and (max-width: 640px) {
@include breakpoint(phablet) {
padding-top: 25px;
}
}
@@ -255,3 +255,18 @@
background-repeat: no-repeat;
background-size: 922px 922px;
}
@mixin breakpoint($point) {
@if $point == desktop {
@media all and (max-width: 1024px) { @content; }
}
@else if $point == tablet {
@media all and (max-width: 768px) { @content; }
}
@else if $point == phablet {
@media all and (max-width: 640px) { @content; }
}
@else if $point == mobile {
@media all and (max-width: 480px) { @content; }
}
}

View File

@@ -1,5 +1,6 @@
@import "branding";
@import "mixins";
@import 'branding';
@import 'mixins';
@import 'admin/globals/variables';
// Generic styles for use
@@ -22,6 +23,24 @@
margin-bottom: 0.5rem;
}
.modal-list {
text-align: center;
font-size: 1rem;
font-weight: 400;
border-bottom: 1px solid $light-grey;
margin-top: 0.75rem;
margin-bottom: 0.5rem;
a.heading {
color: $color-link;
&:hover {
color: $color-link-hover;
text-decoration: underline;
}
}
}
// Enterprise promo image and text
.highlight {
@@ -54,10 +73,16 @@
color: $clr-brick;
}
&.enterprise {
margin: auto;
text-align: center;
width: 100%;
}
p {
line-height: 2.4;
@media all and (max-width: 640px) {
@include breakpoint(phablet) {
line-height: 1.4;
}
}
@@ -193,7 +218,7 @@
display: inline-block;
border-bottom: 1px solid transparent;
@media all and (max-width: 640px) {
@include breakpoint(phablet) {
display: none;
}
}

View File

@@ -1,3 +1,4 @@
@import "mixins";
@import "branding";
@import "animations";
@import "compass/css3/transition";
@@ -19,7 +20,7 @@ $page-alert-height: 55px;
margin: 0;
h6 {
@media all and (max-width: 480px) {
@include breakpoint(mobile) {
font-size: 10px;
line-height: 24px;
}

View File

@@ -4,7 +4,7 @@
.producers {
.active_table .active_table_node {
// Header row
@media all and (max-width: 640px) {
@include breakpoint(phablet) {
.skinny-head {
background-color: $clr-turquoise-light;
@@ -137,7 +137,7 @@
.active_table_row.closed {
border: 1px solid transparent;
@media all and (max-width: 640px) {
@include breakpoint(phablet) {
border-color: $clr-turquoise-light;
}

View File

@@ -11,18 +11,98 @@
@import "shop-taxon-flag";
@import "shop-popovers";
$sidebar-small-width: 75%;
$sidebar-medium-width: 65%;
$sidebar-large-width: 45%;
$sidebar-footer-height: 5em;
.darkswarm {
.shop-filters-sidebar {
display: flex;
flex-direction: column;
height: 100%;
.background {
position: fixed;
top: 0;
right: 0;
z-index: 200;
height: 100%;
width: 100%;
background-color: $shop-sidebar-overlay;
opacity: 0;
transition: opacity $transition-sidebar;
}
&.shown {
.background {
opacity: 1;
}
.sidebar, .sidebar-footer {
margin-right: 0;
}
}
.sidebar {
position: fixed;
top: 0;
right: 0;
z-index: 210;
height: 100%;
width: $sidebar-large-width;
margin-right: -$sidebar-large-width;
background-color: rgba($white, 0.95);
padding: 1em;
transition: margin $transition-sidebar;
overflow-y: scroll;
.property-selectors {
margin-bottom: $sidebar-footer-height + 2em;
}
}
.sidebar-footer {
background-color: $grey-800;
width: $sidebar-large-width;
margin-right: -$sidebar-large-width;
height: $sidebar-footer-height;
position: fixed;
bottom: 0;
right: 0;
transition: margin $transition-sidebar;
padding: 1em;
button {
width: 48%;
}
}
@include breakpoint(tablet) {
.sidebar, .sidebar-footer {
width: $sidebar-medium-width;
margin-right: -$sidebar-medium-width;
}
}
@include breakpoint(mobile) {
.sidebar, .sidebar-footer {
width: $sidebar-small-width;
margin-right: -$sidebar-small-width;
}
}
}
products {
display: block;
padding-top: 20px;
@media all and (max-width: 768px) {
@include breakpoint(tablet) {
input.button.right {
float: left;
}
}
@media all and (max-width: 480px) {
@include breakpoint(mobile) {
.add_to_cart {
margin-top: 2rem;
}
@@ -69,7 +149,7 @@
.bulk-buy {
font-size: 0.875rem;
@media all and (max-width: 768px) {
@include breakpoint(tablet) {
font-size: 0.75rem;
}
}
@@ -92,7 +172,7 @@
font-size: 0.75em;
padding-right: 0.9375rem;
@media all and (max-width: 640px) {
@include breakpoint(phablet) {
padding-right: 0.25rem;
}
}
@@ -114,7 +194,16 @@
margin-bottom: 0px;
}
.shopfront-message {
.select-oc-message {
margin-top: 1rem;
.highlighted {
color: $red-700;
font-weight: bold;
}
}
.open-shop-message {
a {
color: #0096ad;
@@ -125,21 +214,44 @@
}
}
.shopfront_closed_message, .shopfront_hidden_message {
padding: 15px;
border-radius: 5px;
.closed-shop-header {
background-color: $grey-650;
color: $white;
h4 {
color: $white;
}
p {
margin: 1rem 0 0.4rem;
}
.message {
display: inline-block;
}
}
.shopfront_closed_message {
border: 2px solid #eb4c46;
}
.warning-sign {
margin: 0 10px 0 5px;
display: inline-block;
.shopfront_closed_message {
margin: 2em 0em;
}
strong {
color: $grey-650;
display: block;
position: relative;
text-align: center;
width: 23px;
}
.shopfront_hidden_message {
border: 2px solid #db4;
margin: 2em 0em;
.rectangle {
background-color: $white;
border-radius: 4px;
color: $grey-650;
height: 23px;
position: absolute;
top: 27px;
transform: rotate(-315deg);
width: 23px;
}
}
}

View File

@@ -0,0 +1,75 @@
@import "mixins";
@import "branding";
@import "variables";
.shop-searchbar {
background-color: $grey-100;
height: 5em;
padding: 1em 0;
margin-bottom: 1em;
position: relative;
z-index: 5;
.search-wrap {
position: relative;
width: 100%;
display: inline-flex;
.clear {
height: 1em;
width: 1em;
margin-top: 1em;
position: absolute;
right: 1em;
}
}
input#search {
height: 3em;
border-radius: $radius-small;
border: solid 1px $grey-300;
margin: 0;
padding: 0 2.25em 0 2.75em;
width: 100%;
min-width: 0;
background: $white url("/assets/icn-search-grey.png") 1em center no-repeat;
font-size: 1rem; // avoid zoom on iphone, see issue #4535
&::placeholder {
font-style: italic;
}
// Remove conflicting "clear search" buttons added by Chrome
&::-webkit-search-decoration,
&::-webkit-search-cancel-button,
&::-webkit-search-results-button,
&::-webkit-search-results-decoration {
display: none;
}
}
button {
background-color: $grey-600;
margin-left: 1em;
height: 3em;
width: 7em;
padding: 0;
font-size: 1em;
border-radius: $radius-small;
transition: none;
&:hover {
background-color: $grey-700;
}
@include breakpoint(mobile) {
margin-left: 0.75em;
}
}
@include breakpoint(desktop) {
position: -webkit-sticky;
position: sticky;
top: $mobile-nav-height;
}
}

View File

@@ -8,16 +8,18 @@
.tab-buttons {
color: $dark-grey;
box-shadow: $distributor-header-shadow;
position: relative;
z-index: 10;
.columns {
display: flex;
@media all and (max-width: 1024px) {
@include breakpoint(desktop) {
display: table;
width: 100%;
}
@media all and (max-width: 480px) {
@include breakpoint(mobile) {
padding: 0;
}
}
@@ -54,7 +56,7 @@
background: none;
}
@media all and (max-width: 640px) {
@include breakpoint(phablet) {
padding: 0.35em 0 0.65em 0;
}
}
@@ -67,7 +69,7 @@
}
}
@media all and (max-width: 1024px) {
@include breakpoint(desktop) {
display: table-cell;
width: auto;
}
@@ -76,9 +78,9 @@
// content revealed in accordion
.page-view {
margin-bottom: 5em;
background: none;
border: none;
padding-bottom: 5em;
.content {
padding: 1.25em 0;
@@ -104,7 +106,7 @@
p {
max-width: 100%;
@media all and (max-width: 768px) {
@include breakpoint(tablet) {
height: auto !important;
}
}
@@ -121,5 +123,13 @@
margin-bottom: 2px;
}
}
&.with-darker-background {
background-color: $very-light-grey-transparency;
a {
color: $teal-500;
}
}
}
}

View File

@@ -10,7 +10,7 @@
.tab {
text-align: center;
@media all and (max-width: 640px) {
@include breakpoint(phablet) {
text-align: left;
}
@@ -24,7 +24,7 @@
padding: 1em;
border: none;
@media all and (max-width: 640px) {
@include breakpoint(phablet) {
padding: 0.35em 0 0.65em 0;
text-shadow: none;
}
@@ -37,7 +37,7 @@
border-bottom: 4px solid $clr-brick-bright;
cursor: pointer;
@media all and (max-width: 640px) {
@include breakpoint(phablet) {
transition: none;
color: white;
background-color: $clr-brick-bright;
@@ -46,7 +46,7 @@
a {
color: $clr-brick-bright;
@media all and (max-width: 640px) {
@include breakpoint(phablet) {
color: #ffffff;
}
}
@@ -55,14 +55,14 @@
&.selected {
border-bottom: 4px solid $clr-brick;
@media all and (max-width: 640px) {
@include breakpoint(phablet) {
background-color: $clr-brick;
}
a {
color: $clr-brick;
@media all and (max-width: 640px) {
@include breakpoint(phablet) {
color: #ffffff;
}
}

View File

@@ -57,7 +57,7 @@
}
}
@media all and (max-width: 640px) {
@include breakpoint(phablet) {
render-svg {
svg {
width: 24px;

View File

@@ -2,6 +2,7 @@
@import "branding";
@import "mixins";
@import "typography";
@import "variables";
// Button class extensions
@@ -123,9 +124,37 @@ button.success, .button.success {
}
}
button.large {
height: 3em;
font-size: 1em;
color: $white;
border-radius: $radius-medium;
margin: 0;
padding: 0;
&.dark {
background-color: $grey-800;
border: 1px solid $grey-600;
}
&.bright {
background-color: $orange-500;
border: none;
}
}
// Responsive
@media screen and (min-width: 768px) {
[role="main"] {
padding: 0;
}
}
.flex {
display: flex;
}
.no-gutter {
padding-right: 0;
padding-left: 0;
}

View File

@@ -30,3 +30,11 @@ $topbar-dropdown-link-color: $black;
$topbar-dropdown-bg: $white;
$topbar-dropdown-link-bg: $white;
$topbar-dropdown-link-bg-hover: $white;
$mobile-nav-height: 2.8em;
$radius-small: 0.25em;
$radius-medium: 0.5em;
$shop-sidebar-overlay: rgba(0, 0, 0, 0.5);
$transition-sidebar: 250ms ease-in-out 0s;

View File

@@ -1,13 +1,17 @@
module Api
class OrderCyclesController < Api::BaseController
include EnterprisesHelper
respond_to :json
include ApiActionCaching
skip_authorization_check
skip_before_filter :authenticate_user, :ensure_api_key, only: [:taxons, :properties]
caches_action :taxons, :properties,
expires_in: CacheService::FILTERS_EXPIRY,
cache_path: proc { |controller| controller.request.url }
def products
render_no_products unless order_cycle.open?
return render_no_products unless order_cycle.open?
products = ProductsRenderer.new(
distributor,

View File

@@ -18,9 +18,9 @@ class HomeController < BaseController
private
# Cache the value of the query count for 24 hours
def cached_count(key, query)
Rails.cache.fetch("home_stats_count_#{key}", expires_in: 1.day, race_condition_ttl: 10) do
# Cache the value of the query count
def cached_count(statistic, query)
CacheService.home_stats(statistic) do
query.count
end
end

View File

@@ -13,7 +13,7 @@ module ShopHelper
end
def require_customer?
current_distributor.require_login? && !user_is_related_to_distributor?
@require_customer ||= current_distributor.require_login? && !user_is_related_to_distributor?
end
def user_is_related_to_distributor?
@@ -48,6 +48,6 @@ module ShopHelper
end
def no_open_order_cycles?
@order_cycles && @order_cycles.empty?
@no_open_order_cycles ||= @order_cycles&.empty?
end
end

18
app/jobs/job_logger.rb Normal file
View File

@@ -0,0 +1,18 @@
# frozen_string_literal: false
module JobLogger
class Formatter < ::Logger::Formatter
def call(_severity, timestamp, _progname, msg)
time = timestamp.strftime('%FT%T%z')
"#{time}: #{msg.is_a?(String) ? msg : msg.inspect}\n"
end
end
def self.logger
@logger ||= begin
logger = Delayed::Worker.logger.clone
logger.formatter = Formatter.new
logger
end
end
end

View File

@@ -24,7 +24,7 @@ class SubscriptionConfirmJob
# Confirm these proxy orders
ProxyOrder.where(id: unconfirmed_proxy_orders_ids).each do |proxy_order|
Rails.logger.info "Confirming Order for Proxy Order #{proxy_order.id}"
JobLogger.logger.info "Confirming Order for Proxy Order #{proxy_order.id}"
confirm_order!(proxy_order.order)
end

View File

@@ -28,7 +28,7 @@ class SubscriptionPlacementJob
end
def place_order_for(proxy_order)
Rails.logger.info "Placing Order for Proxy Order #{proxy_order.id}"
JobLogger.logger.info("Placing Order for Proxy Order #{proxy_order.id}")
proxy_order.initialise_order!
place_order(proxy_order.order)
end

View File

@@ -0,0 +1,21 @@
# frozen_string_literal: true
# API controllers inherit from ActionController::Metal to keep them slim and fast.
# This concern adds the minimum requirements needed to use Action Caching in the API.
module ApiActionCaching
extend ActiveSupport::Concern
included do
include ActionController::Caching
include ActionController::Caching::Actions
include AbstractController::Layouts
# These configs are not assigned to the controller automatically with ActionController::Metal
self.cache_store = Rails.configuration.cache_store
self.perform_caching = true
# ActionController::Caching asks for a controller's layout, but they're not used in the API
layout false
end
end

View File

@@ -1,5 +1,5 @@
class DistributorShippingMethod < ActiveRecord::Base
self.table_name = "distributors_shipping_methods"
belongs_to :shipping_method, class_name: Spree::ShippingMethod
belongs_to :shipping_method, class_name: Spree::ShippingMethod, touch: true
belongs_to :distributor, class_name: Enterprise, touch: true
end

View File

@@ -16,7 +16,8 @@ class OrderCycle < ActiveRecord::Base
has_many :suppliers, -> { uniq }, source: :sender, through: :cached_incoming_exchanges
has_many :distributors, -> { uniq }, source: :receiver, through: :cached_outgoing_exchanges
has_and_belongs_to_many :schedules, join_table: 'order_cycle_schedules'
has_many :schedules, through: :order_cycle_schedules
has_many :order_cycle_schedules
has_paper_trail meta: { custom_data: proc { |order_cycle| order_cycle.schedule_ids.to_s } }
attr_accessor :incoming_exchanges, :outgoing_exchanges

View File

@@ -0,0 +1,6 @@
# frozen_string_literal: true
class OrderCycleSchedule < ActiveRecord::Base
belongs_to :schedule
belongs_to :order_cycle
end

View File

@@ -1,7 +1,8 @@
class Schedule < ActiveRecord::Base
has_and_belongs_to_many :order_cycles, join_table: 'order_cycle_schedules'
has_paper_trail meta: { custom_data: proc { |schedule| schedule.order_cycle_ids.to_s } }
has_many :order_cycles, through: :order_cycle_schedules
has_many :order_cycle_schedules, dependent: :destroy
has_many :coordinators, -> { uniq }, through: :order_cycles
validates :order_cycles, presence: true

View File

@@ -1,5 +1,6 @@
Spree::Classification.class_eval do
belongs_to :product, class_name: "Spree::Product", touch: true
belongs_to :taxon, class_name: "Spree::Taxon", touch: true
before_destroy :dont_destroy_if_primary_taxon

View File

@@ -12,7 +12,7 @@ Spree::Product.class_eval do
has_many :option_types, through: :product_option_types, dependent: :destroy
belongs_to :supplier, class_name: 'Enterprise', touch: true
belongs_to :primary_taxon, class_name: 'Spree::Taxon'
belongs_to :primary_taxon, class_name: 'Spree::Taxon', touch: true
delegate_belongs_to :master, :unit_value, :unit_description
delegate :images_attributes=, :display_as=, to: :master

View File

@@ -11,6 +11,10 @@ module Spree
end
def total_on_hand
# Associated stock_items no longer exist if the variant has been soft-deleted. A variant
# may still be in an active cart after it's deleted, so this will mark it as out of stock.
return 0 if @variant.deleted?
stock_items.sum(&:count_on_hand)
end

View File

@@ -3,7 +3,7 @@ class Api::Admin::OrderSerializer < ActiveModel::Serializer
:edit_path, :state, :payment_state, :shipment_state,
:payments_path, :ready_to_ship, :ready_to_capture, :created_at,
:distributor_name, :special_instructions,
:item_total, :adjustment_total, :payment_total, :total
:item_total, :adjustment_total, :payment_total, :total, :display_outstanding_balance
has_one :distributor, serializer: Api::Admin::IdSerializer
has_one :order_cycle, serializer: Api::Admin::IdSerializer
@@ -16,6 +16,12 @@ class Api::Admin::OrderSerializer < ActiveModel::Serializer
object.distributor.andand.name
end
def display_outstanding_balance
return "" if object.outstanding_balance.zero?
object.display_outstanding_balance.to_s
end
def edit_path
return '' unless object.id

View File

@@ -62,10 +62,14 @@ module Api
end
def supplied_taxons
return [] unless enterprise.is_primary_producer
ids_to_objs data.supplied_taxons[enterprise.id]
end
def supplied_properties
return [] unless enterprise.is_primary_producer
(product_properties + producer_properties).uniq do |property_object|
property_object.property.presentation
end
@@ -113,7 +117,7 @@ module Api
end
def active
data.active_distributor_ids.andand.include? enterprise.id
@active ||= data.active_distributor_ids.andand.include? enterprise.id
end
# Map svg icons.

View File

@@ -18,7 +18,8 @@ module Api
end
def active
enterprise.ready_for_checkout? && OrderCycle.active.with_distributor(enterprise).exists?
@active ||=
enterprise.ready_for_checkout? && OrderCycle.active.with_distributor(enterprise).exists?
end
def pickup
@@ -73,12 +74,16 @@ module Api
end
def supplied_taxons
return [] unless enterprise.is_primary_producer
ActiveModel::ArraySerializer.new(
enterprise.supplied_taxons, each_serializer: Api::TaxonSerializer
)
end
def supplied_properties
return [] unless enterprise.is_primary_producer
(product_properties + producer_properties).uniq do |property_object|
property_object.property.presentation
end
@@ -118,7 +123,7 @@ module Api
private
def product_properties
enterprise.supplied_products.flat_map(&:properties)
enterprise.supplied_products.includes(:properties).flat_map(&:properties)
end
def producer_properties

View File

@@ -0,0 +1,62 @@
# frozen_string_literal: true
class CacheService
HOME_STATS_EXPIRY = 1.day.freeze
FILTERS_EXPIRY = 30.seconds.freeze
SHOPS_EXPIRY = 15.seconds.freeze
def self.cache(cache_key, options = {})
Rails.cache.fetch cache_key.to_s, options do
yield
end
end
# Yields a cached query, expired by the most recently updated record for a given class.
# E.g: if *any* Spree::Taxon record is updated, all keys based on Spree::Taxon will auto-expire.
def self.cached_data_by_class(cache_key, cached_class)
Rails.cache.fetch "#{cache_key}-#{cached_class}-#{latest_timestamp_by_class(cached_class)}" do
yield
end
end
# Gets the :updated_at value of the most recently updated record for a given class, and returns
# it as a timestamp, eg: `1583836069`.
def self.latest_timestamp_by_class(cached_class)
cached_class.maximum(:updated_at).to_i
end
def self.home_stats(statistic)
Rails.cache.fetch("home_stats_count_#{statistic}",
expires_in: HOME_STATS_EXPIRY,
race_condition_ttl: 10) do
yield
end
end
module FragmentCaching
# Rails' caching in views is called "Fragment Caching" and uses some slightly different logic.
# Note: keys supplied here are actually prepended with "views/" under the hood.
def self.ams_all_taxons_key
"inject-all-taxons-#{CacheService.latest_timestamp_by_class(Spree::Taxon)}"
end
def self.ams_all_properties_key
"inject-all-properties-#{CacheService.latest_timestamp_by_class(Spree::Property)}"
end
def self.ams_shops
[
"shops/index/inject_enterprises",
{ expires_in: SHOPS_EXPIRY }
]
end
def self.ams_shop(enterprise)
[
"enterprises/shop/inject_enterprise_shopfront-#{enterprise.id}",
{ expires_in: SHOPS_EXPIRY }
]
end
end
end

View File

@@ -29,11 +29,15 @@ class CartService
variants_data.each do |variant_data|
loaded_variant = loaded_variants[variant_data[:variant_id].to_i]
if loaded_variant.deleted?
remove_deleted_variant(loaded_variant)
next
end
next unless varies_from_cart(variant_data, loaded_variant)
attempt_cart_add(
loaded_variant, variant_data[:quantity], variant_data[:max_quantity]
)
attempt_cart_add(loaded_variant, variant_data[:quantity], variant_data[:max_quantity])
end
end
@@ -41,12 +45,16 @@ class CartService
@indexed_variants ||= begin
variant_ids_in_data = variants_data.map{ |v| v[:variant_id] }
Spree::Variant.where(id: variant_ids_in_data).
Spree::Variant.with_deleted.where(id: variant_ids_in_data).
includes(:default_price, :stock_items, :product).
index_by(&:id)
end
end
def remove_deleted_variant(variant)
line_item_for_variant(variant).andand.destroy
end
def attempt_cart_add(variant, quantity, max_quantity = nil)
quantity = quantity.to_i
max_quantity = max_quantity.to_i if max_quantity

View File

@@ -8,7 +8,7 @@ class VariantsStockLevels
variant_stock_levels = variant_stock_levels(order.line_items.includes(variant: :stock_items))
order_variant_ids = variant_stock_levels.keys
missing_variants = Spree::Variant.includes(:stock_items).
missing_variants = Spree::Variant.with_deleted.includes(:stock_items).
where(id: (requested_variant_ids - order_variant_ids))
missing_variants.each do |missing_variant|

View File

@@ -0,0 +1,24 @@
%div{"ng-controller" => "OrderCycleChangeCtrl", "ng-cloak" => true}
%closing
%div{"ng-if" => "OrderCycle.selected()"}
= t :enterprises_next_closing
%strong {{ OrderCycle.orders_close_at() | date_in_words }}
%div{"ng-if" => "!OrderCycle.selected()"}
= t :enterprises_choose
.order-cycle-select
.select-label
%span= t :enterprises_ready_for
- if oc_select_options.count == 1
%p
= oc_select_options.first[:time]
- else
%select.select2.avenir#order_cycle_id{"ng-model" => "order_cycle.order_cycle_id",
"ofn-change-order-cycle" => true,
"disabled" => require_customer?,
"ng-options" => "oc.id as oc.time for oc in #{oc_select_options.to_json}"}
- if oc_select_options.count > 1
%option{value: "", disabled: "", selected: ""}= t :shopping_oc_select

View File

@@ -6,7 +6,8 @@
= current_distributor.logo.url
- content_for :injection_data do
= inject_enterprise_shopfront(@enterprise)
- cache(*CacheService::FragmentCaching.ams_shop(@enterprise)) do
= inject_enterprise_shopfront(@enterprise)
%shop.darkswarm
- if @shopfront_layout == 'embedded'
@@ -17,26 +18,7 @@
%a.close{ ng: { click: "alert.close()" } } &times;
- content_for :order_cycle_form do
%div{"ng-controller" => "OrderCycleChangeCtrl", "ng-cloak" => true}
%closing
%div{"ng-if" => "OrderCycle.selected()"}
= t :enterprises_next_closing
%strong {{ OrderCycle.orders_close_at() | date_in_words }}
%div{"ng-if" => "!OrderCycle.selected()"}
= t :enterprises_choose
.order-cycle-select
.select-label
%span= t :enterprises_ready_for
%select.select2.avenir#order_cycle_id{"ng-model" => "order_cycle.order_cycle_id",
"ofn-change-order-cycle" => true,
"disabled" => require_customer?,
"ng-options" => "oc.id as oc.time for oc in #{oc_select_options.to_json}"}
- if oc_select_options.count > 1
%option{value: "", disabled: "", selected: ""}= t :shopping_oc_select
= render partial: "change_order_cycle"
- content_for :ordercycle_sidebar do
.show-for-large-up.large-4.columns

View File

@@ -48,8 +48,10 @@
= inject_current_hub
= inject_current_user
= inject_rails_flash
= inject_taxons
= inject_properties
- cache CacheService::FragmentCaching.ams_all_taxons_key do
= inject_taxons
- cache CacheService::FragmentCaching.ams_all_properties_key do
= inject_properties
= inject_current_order
= inject_currency_config
= yield :injection_data

View File

@@ -1,4 +1,4 @@
.joyride-tip-guide{"ng-class" => "{ in: open }", "ng-show" => "open"}
.cart-dropdown.joyride-tip-guide{"ng-class" => "{ in: open }", "ng-show" => "open"}
%span.joyride-nub.top
.joyride-content-wrapper
%h5

View File

@@ -1,21 +0,0 @@
- if require_customer?
.row.footer-pad
.small-12.columns
.shopfront_hidden_message
= t '.require_customer_login'
- if spree_current_user.nil?
= t '.require_login_html',
{login: ('<a auth="login">' + t('.login') + '</a>').html_safe,
signup: ('<a auth="signup">' + t('.signup') + '</a>').html_safe,
contact: link_to(t('.contact'), '#contact'),
enterprise: current_distributor.name}
- else
= t '.require_customer_html',
{contact: link_to(t('.contact'), '#contact'),
enterprise: current_distributor.name}
- elsif current_distributor.preferred_shopfront_message.present?
.row
.small-12.columns
.shopfront-message
= current_distributor.preferred_shopfront_message.html_safe

View File

@@ -0,0 +1,21 @@
.row.closed-shop-header
.small-12.columns
.content{ "darker-background" => true }
%h4
.warning-sign
.rectangle
%strong !
.message
= t :shopping_oc_closed
%p
= render partial: "shopping_shared/next_order_cycle"
= render partial: "shopping_shared/last_order_cycle"
.row
.small-12.columns
.content
.shopfront_closed_message
- if shopfront_closed_message?
= current_distributor.preferred_shopfront_closed_message.html_safe
- else
= t :shopping_oc_closed_description

View File

@@ -0,0 +1,19 @@
.content{ "darker-background" => true }
.row.footer-pad
.small-12.columns
%strong
= t '.require_customer_login'
%p
- if spree_current_user.nil?
%p
= t '.require_login_html',
{login: ('<a auth="login">' + t('.login') + '</a>').html_safe,
signup: ('<a auth="signup">' + t('.signup') + '</a>').html_safe}
%p
= t '.require_login_2_html',
{contact: link_to(t('.contact'), '#contact'),
enterprise: current_distributor.name}
- else
= t '.require_customer_html',
{contact: link_to(t('.contact'), '#contact'),
enterprise: current_distributor.name}

View File

@@ -0,0 +1,5 @@
.content
.row
.small-12.columns
.open-shop-message
= current_distributor.preferred_shopfront_message.html_safe

View File

@@ -0,0 +1,3 @@
.content.footer-pad{ "darker-background" => true, "ng-controller" => "ProductsCtrl", "ng-show" => "order_cycle.order_cycle_id == null" }
.select-oc-message
= t '.select_oc_html'

View File

@@ -0,0 +1,9 @@
%span{ "ng-show" => "query && ( appliedPropertiesList() || appliedTaxonsList() )" }
= t :products_filters_in
%span.applied-properties{'ng-bind-html' => 'appliedPropertiesList()'}
%span{ "ng-show" => "appliedPropertiesList() && appliedTaxonsList()" }
= t :products_and
%span.applied-taxons{'ng-bind-html' => 'appliedTaxonsList()'}

View File

@@ -1,5 +1,5 @@
.filter-shopfront.taxon-selectors.text-right{ng: {show: 'supplied_taxons != null'}}
%single-line-selectors{ selectors: "taxonSelectors", objects: "supplied_taxons", "active-selectors" => "activeTaxons"}
.filter-shopfront.taxon-selectors{ng: {show: 'supplied_taxons != null'}}
%filter-selector{ 'selector-set' => "taxonSelectors", objects: "supplied_taxons", "active-selectors" => "activeTaxons"}
.filter-shopfront.property-selectors.text-right{ng: {show: 'supplied_properties != null'}}
%single-line-selectors{ selectors: "propertySelectors", objects: "supplied_properties", "active-selectors" => "activeProperties"}
.filter-shopfront.property-selectors{ng: {show: 'supplied_properties != null'}}
%filter-selector{ 'selector-set' => "propertySelectors", objects: "supplied_properties", "active-selectors" => "activeProperties"}

View File

@@ -1,54 +1,48 @@
.footer-pad.small-12.columns
%form{action: main_app.cart_path}
%products{"ng-controller" => "ProductsCtrl", "ng-init" => "refreshStaleData()", "ng-show" => "order_cycle.order_cycle_id != null", "ng-cloak" => true }
// TODO: Needs an ng-show to slide content down
.row.animate-slide{ "ng-show" => "query || appliedPropertiesList() || appliedTaxonsList()" }
= render partial: "shop/products/searchbar"
.row
.small-12.columns
.alert-box.search-alert.ng-scope
%a.right{"ng-click" => "clearAll()"}
= t :products_clear_all
%i.ofn-i_009-close
%span.filter-label
= t :products_showing
%span.applied-properties
{{ appliedPropertiesList() }}
%span.applied-taxons
{{ appliedTaxonsList() }}
%span{ ng: { hide: "!query"} }
%span{ "ng-show" => "appliedPropertiesList() || appliedTaxonsList()" }
= t :products_with
%span.applied-search "{{ query }}"
.row
.small-12.medium-6.large-5.columns
%input#search.text{"ng-model" => "query",
placeholder: t(:products_search),
"ng-debounce" => "200",
"ofn-disable-enter" => true}
.small-12.medium-6.large-6.large-offset-1.columns
= render partial: "shop/products/filters"
.row
%div.pad-top{ "infinite-scroll" => "loadMore()", "infinite-scroll-distance" => "1", "infinite-scroll-disabled" => 'Products.loading' }
%product.animate-repeat{"ng-controller" => "ProductNodeCtrl", "ng-repeat" => "product in Products.products track by product.id", "id" => "product-{{ product.id }}"}
= render "shop/products/summary"
%shop-variant{variant: 'variant', "ng-repeat" => "variant in product.variants | orderBy: ['name_to_display','unit_value'] track by variant.id", "id" => "variant-{{ variant.id }}", "ng-class" => "{'out-of-stock': !variant.on_demand && variant.on_hand == 0}"}
%product{"ng-show" => "Products.loading"}
.row.summary
.small-12.columns.text-center
= t :products_loading
.footer-pad.small-12.columns.no-gutter
.row
.small-12.columns.text-center
%img.spinner{ src: "/assets/spinning-circles.svg" }
.medium-12.large-10.columns
= render partial: "shop/products/search_feedback"
%div{"ng-show" => "Products.products.length == 0 && !Products.loading"}
.row.summary
.small-12.columns
%p.no-results
= t :search_no_results_html, query: "<strong>{{query}}</strong>".html_safe
.row
.small-12.columns
%form{action: main_app.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 ? '#{t(:products_updating_cart)}' : (Cart.empty() ? '#{t(:products_cart_empty)}' : '#{t(:products_edit_cart)}' ) }}", "ng-disabled" => "Cart.dirty || Cart.empty()", "ng-class" => "{ dirty: Cart.dirty }" }
%div.pad-top{ "infinite-scroll" => "loadMore()", "infinite-scroll-distance" => "1", "infinite-scroll-disabled" => 'Products.loading' }
%product.animate-repeat{"ng-controller" => "ProductNodeCtrl", "ng-repeat" => "product in Products.products track by product.id", "id" => "product-{{ product.id }}"}
= render "shop/products/summary"
%shop-variant{variant: 'variant', "ng-repeat" => "variant in product.variants | orderBy: ['name_to_display','unit_value'] track by variant.id", "id" => "variant-{{ variant.id }}", "ng-class" => "{'out-of-stock': !variant.on_demand && variant.on_hand == 0}"}
%product{"ng-show" => "Products.loading"}
.row.summary
.small-12.columns.text-center
= t :products_loading
.row
.small-12.columns.text-center
%img.spinner{ src: "/assets/spinning-circles.svg" }
.hide-for-medium-down.large-2.columns
%h5
= t(:products_filter_by)
%span{ng: {show: 'filtersCount()' }}
= "({{ filtersCount() }} #{t(:products_filter_selected)})"
= render partial: "shop/products/filters"
.shop-filters-sidebar.hide-for-large-up{ng: {show: 'showFilterSidebar', class: "{'shown': showFilterSidebar}"}}
.background{ng: {click: 'toggleFilterSidebar()'}}
.sidebar
%h5
= t(:products_filter_by)
%span{ng: {show: 'filtersCount()' }}
= "({{ filtersCount() }} #{t(:products_filter_selected)})"
= render partial: "shop/products/filters"
.sidebar-footer
%button.large.dark.left{type: 'button', ng: {click: 'clearFilters()'}}
= t(:products_filter_clear)
%button.large.bright.right{type: 'button', ng: {click: 'toggleFilterSidebar()'}}
= t(:products_filter_done)

View File

@@ -0,0 +1,24 @@
.row.animate-slide{ "ng-show" => "query || appliedPropertiesList() || appliedTaxonsList()" }
.small-12.columns
.alert-box.search-alert.ng-scope
%div{"ng-show" => "Products.products.length > 0"}
%a.clear-all.right{"ng-click" => "clearAll()"}
= t :products_clear
%i.ofn-i_009-close
%span.filter-label
= t :products_results_for
%span{ ng: { hide: "!query"} }
%span.applied-search
{{ query }}
= render partial: 'shop/products/applied_filters_feedback'
%div.no-results-bar{"ng-show" => "Products.products.length == 0 && !Products.loading"}
.row.summary
.small-12.columns
%p.no-results
= t :products_no_results_html, query: "<span class='applied-search'>{{query}}</span>".html_safe
= render partial: 'shop/products/applied_filters_feedback'
%button.clear-search{type: 'button', ng: {click: 'clearAll()'}}
= t :products_clear_search

View File

@@ -0,0 +1,17 @@
.shop-searchbar
.row
.small-12.large-5.columns.flex
%div.search-wrap
%input#search.text{"ng-model" => "query",
type: 'search',
placeholder: t(:products_search),
"ng-debounce" => "200",
"ofn-disable-enter" => true}
%a.clear{type: 'button', ng: {show: 'query', click: 'clearQuery()'}, 'focus-search' => true}
%img{ src: "/assets/icn-close.png" }
.hide-for-large-up
%button{type: 'button', ng: {click: 'toggleFilterSidebar()'}}
= t(:products_filter_heading)
%span{ng: {show: 'filtersCount()' }}
({{ filtersCount() }})

View File

@@ -1,4 +1,2 @@
- if most_recently_closed = OrderCycle.most_recently_closed_for(@distributor)
(
= t :shopping_oc_last_closed, distance_of_time: distance_of_time_in_words_to_now(most_recently_closed.orders_close_at)
)

View File

@@ -1,4 +1,2 @@
- if next_oc = OrderCycle.first_opening_for(@distributor)
(
= t :shopping_oc_next_open, distance_of_time: distance_of_time_in_words_to_now(next_oc.orders_open_at)
)

View File

@@ -1,7 +1,7 @@
- content_for :injection_data do
= inject_current_order_cycle
- unless no_open_order_cycles?
- unless no_open_order_cycles? || require_customer?
%ordercycle{"ng-controller" => "OrderCycleCtrl", "ng-cloak" => true,
"ng-class" => "{'requires-selection': !OrderCycle.selected()}"}
%form.custom

View File

@@ -2,5 +2,12 @@
.order-cycle-bar.hide-for-large-up
= render partial: "shopping_shared/order_cycles"
.content
= render partial: 'shop/messages'
- if require_customer?
= render partial: "shop/messages/customer_required"
- else
- if no_open_order_cycles?
= render partial: "shop/messages/closed_shop"
- else
= render partial: "shop/messages/open_shop"

View File

@@ -2,23 +2,9 @@
.order-cycle-bar.hide-for-large-up
= render partial: "shopping_shared/order_cycles"
.row
.small-12.columns
- if no_open_order_cycles?
.content
%h4
%i.ofn-i_012-warning
= t :shopping_oc_closed
%small
%em
= render partial: "shopping_shared/next_order_cycle"
= render partial: "shopping_shared/last_order_cycle"
%p
= t :shopping_oc_closed_description
- if no_open_order_cycles?
= render partial: "shop/messages/closed_shop"
- if shopfront_closed_message?
.shopfront_closed_message
= current_distributor.preferred_shopfront_closed_message.html_safe
- unless require_customer?
= render partial: "shop/products/form"
- else
= render partial: "shop/messages/select_oc"
= render partial: "shop/products/form"

View File

@@ -2,7 +2,8 @@
= t :shops_title
- content_for :injection_data do
= inject_enterprises(@enterprises)
- cache(*CacheService::FragmentCaching.ams_shops) do
= inject_enterprises(@enterprises)
#panes
#shops.pane

View File

@@ -1,26 +1,26 @@
- shipment.manifest.each do |item|
- line_item = order.find_line_item_by_variant(item.variant)
- break if line_item.blank?
%tr.stock-item{ "data-item-quantity" => "#{item.quantity}" }
%td.item-image
= mini_image(item.variant)
%td.item-name
= item.variant.product_and_full_name
%td.item-price.align-center
= line_item.single_money.to_html
%td.item-qty-show.align-center
- item.states.each do |state,count|
= "#{count} x #{t(state.humanize.downcase, scope: [:spree, :shipment_states], default: [:missing, "none"])}"
- unless shipment.shipped?
%td.item-qty-edit.hidden
= number_field_tag :quantity, item.quantity, :min => 0, :class => "line_item_quantity", :size => 5
%td.item-total.align-center
= line_item_shipment_price(line_item, item.quantity)
- if line_item.present?
%tr.stock-item{ "data-item-quantity" => "#{item.quantity}" }
%td.item-image
= mini_image(item.variant)
%td.item-name
= item.variant.product_and_full_name
%td.item-price.align-center
= line_item.single_money.to_html
%td.item-qty-show.align-center
- item.states.each do |state,count|
= "#{count} x #{t(state.humanize.downcase, scope: [:spree, :shipment_states], default: [:missing, "none"])}"
- unless shipment.shipped?
%td.item-qty-edit.hidden
= number_field_tag :quantity, item.quantity, :min => 0, :class => "line_item_quantity", :size => 5
%td.item-total.align-center
= line_item_shipment_price(line_item, item.quantity)
%td.cart-item-delete.actions{ "data-hook" => "cart_item_delete" }
- if !shipment.shipped? && can?(:update, shipment)
= link_to '', '#', :class => 'save-item icon_link icon-ok no-text with-tip', :data => {'shipment-number' => shipment.number, 'variant-id' => item.variant.id, :action => 'save'}, :title => t('actions.save'), :style => 'display: none'
= link_to '', '#', :class => 'cancel-item icon_link icon-cancel no-text with-tip', :data => {:action => 'cancel'}, :title => t('actions.cancel'), :style => 'display: none'
= link_to '', '#', :class => 'edit-item icon_link icon-edit no-text with-tip', :data => {:action => 'edit'}, :title => t('actions.edit')
= link_to '', '#', :class => 'delete-item icon-trash no-text with-tip', :data => {'shipment-number' => shipment.number, 'variant-id' => item.variant.id, :action => 'remove', :confirm => t(:are_you_sure)}, :title => t('actions.delete')
%td.cart-item-delete.actions{ "data-hook" => "cart_item_delete" }
- if !shipment.shipped? && can?(:update, shipment)
= link_to '', '#', :class => 'save-item icon_link icon-ok no-text with-tip', :data => {'shipment-number' => shipment.number, 'variant-id' => item.variant.id, :action => 'save'}, :title => t('actions.save'), :style => 'display: none'
= link_to '', '#', :class => 'cancel-item icon_link icon-cancel no-text with-tip', :data => {:action => 'cancel'}, :title => t('actions.cancel'), :style => 'display: none'
= link_to '', '#', :class => 'edit-item icon_link icon-edit no-text with-tip', :data => {:action => 'edit'}, :title => t('actions.edit')
= link_to '', '#', :class => 'delete-item icon-trash no-text with-tip', :data => {'shipment-number' => shipment.number, 'variant-id' => item.variant.id, :action => 'remove', :confirm => t(:are_you_sure)}, :title => t('actions.delete')

View File

@@ -68,6 +68,8 @@
%span.state{'ng-class' => 'order.payment_state', 'ng-if' => 'order.payment_state'}
%a{'ng-href' => '{{order.payments_path}}' }
{{'js.admin.orders.payment_states.' + order.payment_state | t}}
%span{'ng-if' => 'order.display_outstanding_balance'}
({{order.display_outstanding_balance}})
%td.align-center
%span.state{'ng-class' => 'order.shipment_state', 'ng-if' => 'order.shipment_state'}
{{'js.admin.orders.shipment_states.' + order.shipment_state | t}}

View File

@@ -856,6 +856,10 @@ ar:
save_and_back_to_list: "حفظ والعودة إلى القائمة"
choose_products_from: "اختر المنتجات من:"
incoming:
incoming: "الوارد"
supplier: "المورد"
products: "منتجات"
fees: "رسوم"
save: "حفظ"
save_and_next: "حفظ والتالي"
next: "التالى"
@@ -2869,8 +2873,9 @@ ar:
blank: "لا يمكن أن تكون فارغة"
layouts:
admin:
header:
store: متجر
login_nav:
header:
store: متجر
admin:
tab:
dashboard: "لوحة العرض"

View File

@@ -865,6 +865,10 @@ ca:
save_and_back_to_list: "Desa i torna a la llista"
choose_products_from: "Trieu Productes des de:"
incoming:
incoming: "Entrant"
supplier: "Proveïdora"
products: "Productes "
fees: "Comissions"
save: "Desa"
save_and_next: "Desa i següent"
next: "Següent"
@@ -2912,8 +2916,9 @@ ca:
blank: "no es pot deixar en blanc"
layouts:
admin:
header:
store: Botiga
login_nav:
header:
store: Botiga
admin:
tab:
dashboard: "Panell"

View File

@@ -863,6 +863,10 @@ de_DE:
save_and_back_to_list: "Speichern und zurück zur Liste"
choose_products_from: "Wählen Sie Produkte von:"
incoming:
incoming: "Eingehend"
supplier: "Anbieter"
products: "Produkte"
fees: "Gebühren"
save: "Speichern"
save_and_next: "Speichern und weiter"
next: "Weiter"
@@ -1210,7 +1214,7 @@ de_DE:
menu_5_title: "Über Uns"
menu_5_url: "https://wp.openfoodnetwork.de/"
menu_6_title: "Verbinden"
menu_6_url: "https://openfoodnetwork.org/au/connect/"
menu_6_url: "https://wp.openfoodnetwork.de/support/"
menu_7_title: "Mehr Erfahren"
menu_7_url: "https://openfoodnetwork.org/au/learn/"
logo: "Logo (640x130)"
@@ -2906,8 +2910,9 @@ de_DE:
blank: "kann nicht leer sein"
layouts:
admin:
header:
store: openfoodnetwork.de
login_nav:
header:
store: openfoodnetwork.de
admin:
tab:
dashboard: "Übersicht"

View File

@@ -915,6 +915,11 @@ en:
save_and_back_to_list: "Save and Back to List"
choose_products_from: "Choose Products From:"
incoming:
incoming: "Incoming"
supplier: "Supplier"
products: "Products"
receival_details: "Receival Details"
fees: "Fees"
save: "Save"
save_and_next: "Save and Next"
next: "Next"
@@ -1234,12 +1239,16 @@ en:
footer_data_cookies_policy: "cookies policy"
shop:
messages:
login: "login"
signup: "signup"
contact: "contact"
require_customer_login: "Only approved customers can access this shop."
require_login_html: "If you're already an approved customer, %{login} or %{signup} to proceed. Want to start shopping here? Please %{contact} %{enterprise} and ask about joining."
require_customer_html: "If you'd like to start shopping here, please %{contact} %{enterprise} to ask about joining."
customer_required:
login: "login"
signup: "signup"
contact: "contact"
require_customer_login: "Only approved customers can access this shop."
require_login_html: "If you're already an approved customer, %{login} or %{signup} to proceed."
require_login_2_html: "Want to start shopping here? Please %{contact} %{enterprise} and ask about joining."
require_customer_html: "If you'd like to start shopping here, please %{contact} %{enterprise} to ask about joining."
select_oc:
select_oc_html: "Please <span class='highlighted'>choose when you want your order</span>, to see what products are available."
# Front-end controller translations
card_could_not_be_updated: Card could not be updated
@@ -1639,11 +1648,19 @@ See the %{link} to find out more about %{sitename}'s features and to start using
other: You have <a href='%{path}' target='_blank'>%{count} orders with %{shop}</a> currently open for review. You can make changes until %{oc_close}.
orders_changeable_orders_alert_html: This order has been confirmed, but you can make changes until <strong>%{oc_close}</strong>.
products_clear_all: Clear all
products_clear: Clear
products_showing: "Showing:"
products_or: "OR"
products_results_for: "Results for"
products_or: "or"
products_and: "and"
products_filters_in: "in"
products_with: with
products_search: "Search by product or producer"
products_search: "Search..."
products_filter_by: "Filter by"
products_filter_selected: "selected"
products_filter_heading: "Filters"
products_filter_clear: "Clear"
products_filter_done: "Done"
products_loading: "Loading products..."
products_updating_cart: "Updating cart..."
products_cart_empty: "Cart empty"
@@ -1654,6 +1671,8 @@ See the %{link} to find out more about %{sitename}'s features and to start using
products_update_error_msg: "Saving failed."
products_update_error_data: "Save failed due to invalid data:"
products_changes_saved: "Changes saved."
products_no_results_html: "Sorry, no results found for %{query}"
products_clear_search: "Clear search"
search_no_results_html: "Sorry, no results found for %{query}. Try another search?"
@@ -2438,6 +2457,10 @@ See the %{link} to find out more about %{sitename}'s features and to start using
resolve_errors: Please resolve the following errors
more_items: "+ %{count} More"
default_card_updated: Default Card Updated
cart:
add_to_cart_failed: >
There was a problem adding this product to the cart.
Perhaps it has become unavailable or the shop is closing.
admin:
enterprise_limit_reached: "You have reached the standard limit of enterprises per account. Write to %{contact_email} if you need to increase it."
modals:
@@ -3071,8 +3094,9 @@ See the %{link} to find out more about %{sitename}'s features and to start using
blank: "can't be blank"
layouts:
admin:
header:
store: Store
login_nav:
header:
store: Store
admin:
tab:
dashboard: "Dashboard"

Some files were not shown because too many files have changed in this diff Show More