⬆ Bump python deps and upgrade everything
Replace gulp with webpack Switch to npm from yarn Add eslint and prettier
This commit is contained in:
1 parent
45621280bc
commit
be9f84d6dd
33 files changed
+11851
-26393
No files matched your search
+2
-1
@@ -6,4 +6,5 @@ db.sqlite3
|
||||
venv
|
||||
node_modules
|
||||
bower_components
|
||||
staticfiles
|
||||
staticfiles
|
||||
static/dist/
|
||||
@@ -1,17 +1,26 @@
|
||||
Kassa helps you register purchased membership cards and memberships. It's a tool for volunteers and bar employees at The Norwegian Student Society.
|
||||
|
||||
## TODO
|
||||
* replace nunjucks with something that is maintained
|
||||
* autoprefixer
|
||||
* bootstrap 4.x
|
||||
* Loading state on submit buttons
|
||||
* Galtinn/LDAP auth
|
||||
|
||||
## Installation
|
||||
apt install libldap2-dev libsasl2-dev # pyldap deps
|
||||
python3 -m venv venv
|
||||
. venv/bin/activate
|
||||
pip install -U pip wheel
|
||||
pip install -r requirements.txt
|
||||
python manage.py migrate
|
||||
```shell script
|
||||
apt install libldap2-dev libsasl2-dev # pyldap deps
|
||||
python3 -m venv venv
|
||||
. venv/bin/activate
|
||||
pip install -U pip wheel
|
||||
pip install -r requirements.txt
|
||||
python manage.py migrate
|
||||
```
|
||||
|
||||
### Development tasks
|
||||
```shell script
|
||||
npm run build:templates # regenerate nunjucks templates
|
||||
```
|
||||
## Configuration
|
||||
Create a user in Galtinn with the following permissions:
|
||||
* galtinn | User | Can view User
|
||||
@@ -22,14 +31,20 @@ Create a user in Galtinn with the following permissions:
|
||||
* galtinn | Order | Can view Order
|
||||
|
||||
Fetch GALTINN_API_KEY:
|
||||
```shell script
|
||||
curl -H "Content-Type: application/json" -X POST -d '{"username":"apiuser","password":"test"}' GALTINN_API_URL/auth/obtain-token/
|
||||
```
|
||||
|
||||
If developing you can disable calls to Tekstmelding in local_settings.py:
|
||||
TEKSTMELDING_ENABLED = False
|
||||
```python
|
||||
TEKSTMELDING_ENABLED = False
|
||||
```
|
||||
|
||||
## LDAP
|
||||
sudo docker run -e LDAP_DOMAIN=neuf.no -e LDAP_ORGANISATION="Neuf" -e LDAP_ADMIN_PASSWORD="toor" -p 389:389 -d nikolaik/openldap
|
||||
ldapadd -D "cn=admin,dc=neuf,dc=no" -w "toor" -f test/testdata.ldif # Testdata
|
||||
# Verify import
|
||||
ldapsearch -x -b dc=neuf,dc=no
|
||||
# Login with test@example.com:test
|
||||
```shell script
|
||||
sudo docker run -e LDAP_DOMAIN=neuf.no -e LDAP_ORGANISATION="Neuf" -e LDAP_ADMIN_PASSWORD="toor" -p 389:389 -d nikolaik/openldap
|
||||
ldapadd -D "cn=admin,dc=neuf,dc=no" -w "toor" -f test/testdata.ldif # Testdata
|
||||
# Verify import
|
||||
ldapsearch -x -b dc=neuf,dc=no
|
||||
# Login with test@example.com:test
|
||||
```
|
||||
@@ -0,0 +1,5 @@
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
def sentry(request):
|
||||
return {'SENTRY_DSN': settings.SENTRY_DSN or '', 'SENTRY_ENVIRONMENT': settings.SENTRY_ENVIRONMENT}
|
||||
@@ -1,18 +1,14 @@
|
||||
{% load staticfiles raven %}
|
||||
{% load static %}
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head lang="en">
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{% block title %}Registrer medlemskap{% endblock %}</title>
|
||||
<link rel="shortcut icon" href="{% static "dist/images/favicon.ico" %}">
|
||||
<!--<link rel="stylesheet" href="{% static "dist/styles/vendor.css" %}">-->
|
||||
<link rel="stylesheet" href="{% static "dist/styles/app.css" %}">
|
||||
<script src="{% static "dist/scripts/vendor.js" %}"></script>
|
||||
<script src="{% static "dist/scripts/templates.js" %}"></script>
|
||||
<script src="{% static "dist/scripts/app.js" %}"></script>
|
||||
<script src="https://cdn.ravenjs.com/3.5.1/raven.min.js"></script>
|
||||
<link rel="stylesheet" href="{% static "dist/main.css" %}">
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<div class="toast-wrap"></div>
|
||||
@@ -24,11 +20,15 @@
|
||||
</div>
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<div class="credits"><p><a href="https://studentersamfundet.no" title="Det Norske Studentersamfund"><img src="{% static "dist/images/logo.png" %}" width="70" height="70"></a></p>
|
||||
<div class="credits"><p><a href="https://studentersamfundet.no" title="Det Norske Studentersamfund"><img src="{% static "dist/images/logo.png" %}" width="70" height="70" alt="DNS logo"></a></p>
|
||||
Laget med <span class="love" title="kærlighed">♥</span> av <a href="http://kak.studentersamfundet.no/" title="Kommunikasjonsavdelingen">KAK</a></div>
|
||||
|
||||
</div>
|
||||
</footer>
|
||||
<script>Raven.config('{% sentry_public_dsn %}').install()</script>
|
||||
{% if SENTRY_DSN %}
|
||||
<script src="https://browser.sentry-cdn.com/5.10.2/bundle.min.js" integrity="sha384-ssBfXiBvlVC7bdA/VX03S88B5MwXQWdnpJRbUYFPgswlOBwETwTp6F3SMUNpo9M9" crossorigin="anonymous"></script>
|
||||
<script>Sentry.init({ dsn: '{{ SENTRY_DSN }}', environment: '{{ SENTRY_ENVIRONMENT }}' });</script>
|
||||
{% endif %}
|
||||
<script src="{% static "dist/main.js" %}"></script>
|
||||
</body>
|
||||
</html>
|
||||
+8
-7
@@ -1,12 +1,13 @@
|
||||
from django.conf.urls import url
|
||||
from django.urls import path, re_path
|
||||
|
||||
from apps.kassa.views import (register, user_search, check_card, check_phone_number, register_card_and_membership,
|
||||
renew_membership)
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^$', register, name='register'),
|
||||
url(r'^user-search/$', user_search, name='user-search'),
|
||||
url(r'^check-phone-number/$', check_phone_number, name='check-phone-number'),
|
||||
url(r'^check-card/$', check_card, name='check-card'),
|
||||
url(r'^register-card-membership/$', register_card_and_membership, name='register-card-and-membership'),
|
||||
url(r'^renew-membership/$', renew_membership, name='renew-membership'),
|
||||
re_path(r'^$', register, name='register'),
|
||||
path('user-search/', user_search, name='user-search'),
|
||||
path('check-phone-number/', check_phone_number, name='check-phone-number'),
|
||||
path('check-card/', check_card, name='check-card'),
|
||||
path('register-card-membership/', register_card_and_membership, name='register-card-and-membership'),
|
||||
path('renew-membership/', renew_membership, name='renew-membership'),
|
||||
]
|
||||
Vendored
+28
-20
@@ -1,25 +1,33 @@
|
||||
from contextlib import contextmanager as _contextmanager
|
||||
from fabric.api import run, sudo, env, cd, prefix
|
||||
import os
|
||||
|
||||
env.use_ssh_config = True
|
||||
env.hosts = ['dreamcast.neuf.no']
|
||||
env.project_path = '/var/www/neuf.no/kassa'
|
||||
env.user = 'gitdeploy'
|
||||
env.activate = 'source {}/venv/bin/activate'.format(env.project_path)
|
||||
from fabric import Connection
|
||||
from invoke import task
|
||||
|
||||
|
||||
@_contextmanager
|
||||
def virtualenv():
|
||||
with cd(env.project_path), prefix(env.activate):
|
||||
yield
|
||||
@task()
|
||||
def deploy(c):
|
||||
"""Make sure proxy_user is set to your neuf username."""
|
||||
project_path = '/var/www/neuf.no/kassa'
|
||||
proxy_user = os.getenv('DEPLOY_USER', os.getenv('USER'))
|
||||
|
||||
c = Connection(host='gitdeploy@dreamcast.neuf.no', gateway=Connection('login.neuf.no', user=proxy_user))
|
||||
|
||||
with c.cd(project_path), c.prefix('source {}/venv/bin/activate'.format(project_path)):
|
||||
c.run('git pull') # Get source
|
||||
c.run('pip install -U pip')
|
||||
c.run('pip install -r requirements.txt') # install deps in virtualenv
|
||||
with c.cd('static'): # install and compile frontend deps
|
||||
c.run('npm i')
|
||||
c.run('npm run build')
|
||||
c.run('python manage.py collectstatic --noinput -i node_modules') # Collect static
|
||||
c.run('python manage.py migrate') # Run DB migrations
|
||||
|
||||
# Reload gunicorn
|
||||
c.sudo('/usr/bin/supervisorctl pid kassa.neuf.no | xargs kill -HUP', shell=False)
|
||||
|
||||
|
||||
def deploy():
|
||||
with virtualenv():
|
||||
run('git pull') # Get source
|
||||
run('pip install -r requirements.txt') # install deps in virtualenv
|
||||
run('umask 022; python manage.py collectstatic --noinput') # Collect static
|
||||
run('python manage.py migrate') # Run DB migrations
|
||||
|
||||
# Reload
|
||||
sudo('/usr/bin/supervisorctl pid kassa.neuf.no | xargs kill -HUP', shell=False)
|
||||
@task
|
||||
def build(c):
|
||||
with c.cd('static'):
|
||||
c.run('npm i')
|
||||
c.run('npm run build')
|
||||
+4
-29
@@ -11,8 +11,6 @@ https://docs.djangoproject.com/en/1.8/ref/settings/
|
||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||
import os
|
||||
|
||||
import raven
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
|
||||
@@ -41,20 +39,18 @@ INSTALLED_APPS = (
|
||||
'bootstrapform',
|
||||
'django_extensions',
|
||||
'corsheaders',
|
||||
'raven.contrib.django.raven_compat',
|
||||
)
|
||||
|
||||
LOCAL_APPS = ('apps.kassa',)
|
||||
|
||||
INSTALLED_APPS += LOCAL_APPS
|
||||
|
||||
MIDDLEWARE_CLASSES = (
|
||||
MIDDLEWARE = (
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'corsheaders.middleware.CorsMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
@@ -75,6 +71,7 @@ TEMPLATES = [
|
||||
'django.template.context_processors.request',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
'apps.kassa.context_processors.sentry',
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -124,11 +121,6 @@ TEKSTMELDING_ACTIVATION_SMS_TEMPLATE = 'Velkommen som medlem! Registrer deg her:
|
||||
|
||||
CORS_ORIGIN_ALLOW_ALL = True
|
||||
|
||||
RAVEN_CONFIG = {
|
||||
'dsn': os.getenv('RAVEN_DSN'),
|
||||
'release': raven.fetch_git_sha(BASE_DIR)
|
||||
}
|
||||
|
||||
LOGGING = {
|
||||
'version': 1,
|
||||
'disable_existing_loggers': True,
|
||||
@@ -139,10 +131,6 @@ LOGGING = {
|
||||
},
|
||||
},
|
||||
'handlers': {
|
||||
'sentry': {
|
||||
'level': 'WARNING', # To capture more than ERROR, change to WARNING, INFO, etc.
|
||||
'class': 'raven.contrib.django.raven_compat.handlers.SentryHandler',
|
||||
},
|
||||
'console': {
|
||||
'level': 'DEBUG',
|
||||
'class': 'logging.StreamHandler',
|
||||
@@ -152,31 +140,18 @@ LOGGING = {
|
||||
'loggers': {
|
||||
'root': {
|
||||
'level': 'WARNING',
|
||||
'handlers': ['sentry', 'console'],
|
||||
},
|
||||
'django': {
|
||||
'level': 'DEBUG',
|
||||
'handlers': ['console'],
|
||||
'propagate': True,
|
||||
},
|
||||
'django.db.backends': {
|
||||
'level': 'ERROR',
|
||||
'handlers': ['console'],
|
||||
'propagate': False,
|
||||
},
|
||||
'raven': {
|
||||
'level': 'DEBUG',
|
||||
'handlers': ['console'],
|
||||
'propagate': False,
|
||||
},
|
||||
'sentry.errors': {
|
||||
'level': 'DEBUG',
|
||||
'handlers': ['console'],
|
||||
'propagate': False,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
SENTRY_DSN = None
|
||||
SENTRY_ENVIRONMENT = 'dev'
|
||||
|
||||
# Local settings
|
||||
try:
|
||||
|
||||
+6
-2
@@ -1,9 +1,13 @@
|
||||
from django.conf.urls import include, url
|
||||
from django.contrib import admin
|
||||
from django.urls import path
|
||||
|
||||
from django.contrib.auth import urls as auth_urls
|
||||
|
||||
from apps.kassa import urls as kassa_urls
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^admin/', include(admin.site.urls)),
|
||||
path('admin/', admin.site.urls),
|
||||
url('^', include(kassa_urls)),
|
||||
url('^accounts/', include('django.contrib.auth.urls'))
|
||||
path('accounts/', include(auth_urls))
|
||||
]
|
||||
+11
-10
@@ -1,11 +1,12 @@
|
||||
django-auth-ldap==1.7.0
|
||||
django-auth-ldap==2.1.*
|
||||
django-bootstrap-form==3.4
|
||||
django-cors-headers==2.4.0
|
||||
django-extensions==2.1.0
|
||||
django-phonenumber-field==2.0.0
|
||||
django==1.11.17
|
||||
gunicorn==19.9.0
|
||||
mysqlclient==1.3.13
|
||||
phonenumbers==8.9.10
|
||||
raven==6.9.0
|
||||
requests==2.21.0
|
||||
django-cors-headers==3.2.*
|
||||
django-extensions==2.2.*
|
||||
django-phonenumber-field==4.0.*
|
||||
django==2.2.*
|
||||
fabric==2.5.*
|
||||
gunicorn==20.0.*
|
||||
mysqlclient==1.4.6
|
||||
phonenumbers==8.11.*
|
||||
requests==2.22.*
|
||||
sentry-sdk==0.13.*
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"presets": ["@babel/preset-env"],
|
||||
"plugins": ["@babel/plugin-transform-runtime"]
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true
|
||||
},
|
||||
"extends": [
|
||||
"airbnb-base",
|
||||
"plugin:prettier/recommended",
|
||||
"plugin:import/errors",
|
||||
"plugin:import/warnings"
|
||||
],
|
||||
"rules": {
|
||||
"no-plusplus": "off",
|
||||
"no-console": "off",
|
||||
"no-underscore-dangle": "off",
|
||||
"no-param-reassign": "off",
|
||||
"no-restricted-syntax": "off",
|
||||
},
|
||||
"settings": {
|
||||
"import/resolver": {
|
||||
"node": {}, // https://github.com/benmosher/eslint-plugin-import/issues/1396#issuecomment-509384041
|
||||
"webpack": {
|
||||
"config": "webpack.config.js"
|
||||
}
|
||||
}
|
||||
}
|
||||
,
|
||||
"plugins": ["prettier"],
|
||||
"parser": "babel-eslint"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"printWidth": 120,
|
||||
"arrowParens": "always"
|
||||
}
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 106 KiB |
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,652 @@
|
||||
import * as $ from 'jquery';
|
||||
import moment from 'moment';
|
||||
import nunjucks from 'nunjucks';
|
||||
import _ from 'lodash';
|
||||
|
||||
//
|
||||
import 'bootstrap-sass/assets/javascripts/bootstrap/tooltip';
|
||||
import 'bootstrap-sass/assets/javascripts/bootstrap/popover';
|
||||
|
||||
import './templates';
|
||||
import './index.scss';
|
||||
|
||||
/* Init and global vars */
|
||||
let _dom;
|
||||
let users;
|
||||
let searching; /* search ajax request */
|
||||
let selectedUser = null;
|
||||
let membershipOrder = null; /* selected order object */
|
||||
const cardForm = {
|
||||
fields: {
|
||||
cardNumber: false,
|
||||
phoneNumber: false
|
||||
}
|
||||
};
|
||||
const STATE_ICON = {
|
||||
success: 'ok',
|
||||
warning: 'warning-sign',
|
||||
error: 'remove'
|
||||
};
|
||||
// TODO: replace with lodash template
|
||||
const nunjucksEnv = new nunjucks.Environment();
|
||||
|
||||
const urls = {
|
||||
userSearch: '/user-search/',
|
||||
checkPhoneNumber: '/check-phone-number/',
|
||||
checkCard: '/check-card/',
|
||||
registerCardAndMembership: '/register-card-membership/',
|
||||
renewMembership: '/renew-membership/'
|
||||
};
|
||||
|
||||
function getCookie(name) {
|
||||
let cookieValue = null;
|
||||
if (document.cookie && document.cookie !== '') {
|
||||
const cookies = document.cookie.split(';');
|
||||
for (let i = 0; i < cookies.length; i++) {
|
||||
const cookie = $.trim(cookies[i]);
|
||||
// Does this cookie string begin with the name we want?
|
||||
if (cookie.substring(0, name.length + 1) === `${name}=`) {
|
||||
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return cookieValue;
|
||||
}
|
||||
|
||||
function getParameterByName(name) {
|
||||
return new URLSearchParams(window.location.search).get(name) || '';
|
||||
}
|
||||
|
||||
function formatPhoneNumber(str) {
|
||||
if (!str || str.length <= 1) {
|
||||
return 'No phone number';
|
||||
}
|
||||
if (str.length === 11) {
|
||||
return `${str.slice(0, 3)} ${str.slice(3, 6)} ${str.slice(6, 8)} ${str.slice(8)}`;
|
||||
}
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
function sortSearchResults(results) {
|
||||
return _.sortBy(results, (x) => {
|
||||
if (x.last_membership) {
|
||||
if (x.last_membership.end_date) {
|
||||
return 0 - moment(x.last_membership.end_date).unix();
|
||||
}
|
||||
return 0 - moment(x.last_membership.start_date).unix();
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
function formatResults(results) {
|
||||
return nunjucksEnv.render('search_results.html', { results });
|
||||
}
|
||||
|
||||
function renderSelectedUser(userData) {
|
||||
let context;
|
||||
if (!userData) {
|
||||
context = { placeholder: true }; // dummy card
|
||||
} else {
|
||||
context = { res: userData, checked: true, no_user_actions: true };
|
||||
}
|
||||
const selectedUserHtml = nunjucksEnv.render('search_result.html', context);
|
||||
_dom.selectedEntityWrap.html(selectedUserHtml);
|
||||
}
|
||||
|
||||
function renderSelectedOrder(order) {
|
||||
if (!order) {
|
||||
return;
|
||||
}
|
||||
const context = { order, checked: true, no_user_actions: true };
|
||||
const selectedOrderHtml = nunjucksEnv.render('selected_order.html', context);
|
||||
_dom.selectedEntityWrap.html(selectedOrderHtml);
|
||||
}
|
||||
|
||||
function setToast(message, messageType) {
|
||||
const context = {
|
||||
message,
|
||||
message_type: messageType,
|
||||
icon: STATE_ICON[messageType]
|
||||
};
|
||||
const html = nunjucksEnv.render('toast.html', context);
|
||||
_dom.toastWrap.html(html);
|
||||
_dom.toastWrap.addClass('visible');
|
||||
setTimeout(() => {
|
||||
_dom.toastWrap.removeClass('visible');
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
function updateMemberShipButton(user, order) {
|
||||
const todayPlusThirtyDays = moment()
|
||||
.add(30, 'days')
|
||||
.format('YYYY-MM-DD');
|
||||
/* User with valid membership */
|
||||
if (user && user.is_member) {
|
||||
/* ...which is lifelong cannot renew */
|
||||
if (user.last_membership.membership_type === 'lifelong') {
|
||||
_dom.membershipSubmitButton.prop('disabled', true);
|
||||
}
|
||||
/* ...or expiring soon and the user has a card */
|
||||
// else if (user.last_membership.end_date <= todayPlusThirtyDays && user.active_member_card) {
|
||||
/* ...with a membership that expires soon can renew */
|
||||
else if (user.last_membership.end_date <= todayPlusThirtyDays) {
|
||||
_dom.membershipSubmitButton.prop('disabled', false);
|
||||
}
|
||||
}
|
||||
/* User with expired membership can renew */
|
||||
// else if (user && !user.is_member && user.active_member_card) {
|
||||
else if (user && !user.is_member) {
|
||||
_dom.membershipSubmitButton.prop('disabled', false);
|
||||
} else if (order) {
|
||||
/* Order */
|
||||
/* If the membership has expired it can be renewed */
|
||||
if (!order.product.is_valid && order.product.end_date <= todayPlusThirtyDays) {
|
||||
_dom.membershipSubmitButton.prop('disabled', false);
|
||||
}
|
||||
} else {
|
||||
_dom.membershipSubmitButton.prop('disabled', true);
|
||||
}
|
||||
}
|
||||
|
||||
function setSelectedUser(user, updateSearchResult) {
|
||||
selectedUser = user;
|
||||
updateMemberShipButton(user, null);
|
||||
|
||||
_dom.userIdField.val((user && user.id) || '').trigger('change');
|
||||
if (updateSearchResult) {
|
||||
const staleSearchResult = _dom.results.find('.search-result').removeClass('selected');
|
||||
staleSearchResult.find('input').prop('checked', false);
|
||||
/* User is still in search result, mark selected */
|
||||
if (user && _.findWhere(users, { id: user.id })) {
|
||||
const searchResult = _dom.results.find(`[data-user-id="${user.id}"]`).toggleClass('selected');
|
||||
searchResult.find('input').prop('checked', true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setSelectedOrder(order) {
|
||||
membershipOrder = order;
|
||||
if (order) {
|
||||
updateMemberShipButton(null, order);
|
||||
}
|
||||
_dom.orderUuidField.val((order && order.uuid) || '').trigger('change');
|
||||
}
|
||||
|
||||
function setFieldState(fieldElement, state, helpText) {
|
||||
if (!helpText) {
|
||||
helpText = '';
|
||||
}
|
||||
const helpSpan = `<span class="help-block ">${helpText}</span>`;
|
||||
const feedbackSpan = `<span class="glyphicon glyphicon-${STATE_ICON[state]} form-control-feedback" aria-hidden="true"></span>`;
|
||||
const cssClass = `has-${state}`;
|
||||
const formGroup = fieldElement.parent().parent();
|
||||
formGroup.removeClass('has-success has-warning has-error has-feedback'); // reset
|
||||
if (['success', 'warning', 'error'].indexOf(state) !== -1) {
|
||||
formGroup.addClass(`${cssClass} has-feedback`);
|
||||
/* Feedback icon */
|
||||
const feedbackEl = formGroup.find('.form-control-feedback');
|
||||
if (!feedbackEl.length) {
|
||||
// add
|
||||
formGroup.append(feedbackSpan);
|
||||
} else {
|
||||
// Update
|
||||
feedbackEl.removeClass('glyphicon-ok glypicon-warning-sign glyphicon-remove');
|
||||
feedbackEl.addClass(`glyphicon-${STATE_ICON[state]}`);
|
||||
}
|
||||
/* Help text */
|
||||
const helpTextEl = formGroup.find('.help-block');
|
||||
if (!helpTextEl.length) {
|
||||
formGroup.append(helpSpan);
|
||||
} else {
|
||||
helpTextEl.text(helpText);
|
||||
}
|
||||
} else {
|
||||
formGroup.find('.form-control-feedback').remove();
|
||||
formGroup.find('.help-block').remove();
|
||||
}
|
||||
}
|
||||
function resetCardForm(resetNative) {
|
||||
if (resetNative) {
|
||||
_dom.registerCardForm.get(0).reset();
|
||||
}
|
||||
/* Clear field states */
|
||||
setFieldState(_dom.phoneNumberField, '');
|
||||
cardForm.fields.phoneNumber = false;
|
||||
setFieldState(_dom.cardNumberField, '');
|
||||
cardForm.fields.cardNumber = false;
|
||||
selectedUser = null;
|
||||
membershipOrder = null;
|
||||
_dom.userIdField.val('').trigger('change');
|
||||
_dom.membershipTrialCheckBox.prop('checked', false);
|
||||
|
||||
/* Disable submit buttons */
|
||||
_dom.registerSubmitButton.prop('disabled', true);
|
||||
_dom.membershipSubmitButton.prop('disabled', true);
|
||||
}
|
||||
function resetSearchForm(resetNative) {
|
||||
if (resetNative) {
|
||||
_dom.searchResetButton.get(0).reset();
|
||||
}
|
||||
/* Clear search results */
|
||||
users = null;
|
||||
_dom.results.html('');
|
||||
}
|
||||
|
||||
function cardFormIsValid() {
|
||||
return _.values(cardForm.fields).every((v) => v);
|
||||
}
|
||||
|
||||
function updateSubmitButton() {
|
||||
if (cardFormIsValid()) {
|
||||
_dom.registerSubmitButton.prop('disabled', false);
|
||||
return;
|
||||
}
|
||||
_dom.registerSubmitButton.prop('disabled', true);
|
||||
}
|
||||
function validatePhoneNumber(val) {
|
||||
if (val.length === 0) {
|
||||
cardForm.fields.phoneNumber = false;
|
||||
updateSubmitButton();
|
||||
return 'Phone number should not be empty.';
|
||||
}
|
||||
cardForm.fields.phoneNumber = true;
|
||||
updateSubmitButton();
|
||||
return '';
|
||||
}
|
||||
|
||||
function validateCardNumber(val) {
|
||||
if (val.length === 0) {
|
||||
cardForm.fields.cardNumber = false;
|
||||
updateSubmitButton();
|
||||
return 'Card number should not be empty.';
|
||||
}
|
||||
if (val.length > 0 && val[0] !== '1') {
|
||||
cardForm.fields.cardNumber = false;
|
||||
updateSubmitButton();
|
||||
return 'Card number should start with 1.';
|
||||
}
|
||||
if (val.length !== 9) {
|
||||
cardForm.fields.cardNumber = false;
|
||||
updateSubmitButton();
|
||||
return 'Card number should be 9 digits long.';
|
||||
}
|
||||
cardForm.fields.cardNumber = true;
|
||||
updateSubmitButton();
|
||||
return '';
|
||||
}
|
||||
|
||||
function getFormData(formElement) {
|
||||
let formData = formElement.serializeArray();
|
||||
formData = _.object(
|
||||
_.map(formData, (x) => {
|
||||
return [x.name, x.value || null];
|
||||
})
|
||||
);
|
||||
/* no need for radio button state */
|
||||
delete formData.user;
|
||||
|
||||
return formData;
|
||||
}
|
||||
|
||||
/* Phone number as you type */
|
||||
function checkPhoneNumber() {
|
||||
const val = _dom.phoneNumberField.val().trim();
|
||||
$.getJSON(urls.checkPhoneNumber, { phone_number: val }, (data) => {
|
||||
/* Invalid phone number? */
|
||||
if (data.error) {
|
||||
setSelectedUser(null, true);
|
||||
setSelectedOrder(null);
|
||||
setFieldState(_dom.phoneNumberField, 'error', data.error);
|
||||
cardForm.fields.phoneNumber = false;
|
||||
updateSubmitButton();
|
||||
return;
|
||||
}
|
||||
|
||||
const { user } = data;
|
||||
const { order } = data;
|
||||
let message = '';
|
||||
|
||||
/* Has valid card membership */
|
||||
if (!user && order && order.member_card && order.product.is_valid) {
|
||||
message = `Phone number is already tied to card ${order.member_card} and is valid until ${order.product.end_date}.`;
|
||||
setFieldState(_dom.phoneNumberField, 'error', message);
|
||||
cardForm.fields.phoneNumber = false;
|
||||
setSelectedOrder(order);
|
||||
updateSubmitButton();
|
||||
return;
|
||||
}
|
||||
|
||||
if (user) {
|
||||
/* Existing user */
|
||||
message = 'Phone number belongs to existing user.';
|
||||
setFieldState(_dom.phoneNumberField, 'success', message);
|
||||
setSelectedUser(user, true);
|
||||
setSelectedOrder(null);
|
||||
cardForm.fields.phoneNumber = true;
|
||||
updateSubmitButton();
|
||||
} else if (order) {
|
||||
setSelectedOrder(order);
|
||||
/* Valid membership, no card associated */
|
||||
if (order.product.is_valid) {
|
||||
message = 'Phone number has a valid membership.';
|
||||
} else {
|
||||
message = 'Phone number has an expired membership.';
|
||||
}
|
||||
} else {
|
||||
/* Valid phone number associated user or order */
|
||||
setSelectedOrder(null);
|
||||
setSelectedUser(null);
|
||||
}
|
||||
|
||||
setFieldState(_dom.phoneNumberField, 'success', message);
|
||||
cardForm.fields.phoneNumber = true;
|
||||
updateSubmitButton();
|
||||
});
|
||||
}
|
||||
|
||||
/* Add filter |phoneNumber */
|
||||
nunjucksEnv.addFilter('phoneNumber', formatPhoneNumber);
|
||||
|
||||
/* On document ready */
|
||||
$(document).ready(function() {
|
||||
_dom = {
|
||||
results: $('.results-wrap'),
|
||||
query: $('#id_query'),
|
||||
phoneNumberLabel: $('[for=id_phone_number]'),
|
||||
phoneNumberField: $('#id_phone_number'),
|
||||
cardNumberField: $('#id_card_number'),
|
||||
registerCardForm: $('.register-card-form'),
|
||||
registerSubmitButton: $('#register-submit-btn'),
|
||||
usernameField: $('#id_username'),
|
||||
userIdField: $('#id_user_id'),
|
||||
orderUuidField: $('#id_order_uuid'),
|
||||
selectedEntityWrap: $('.register-card-form--selected-entity-wrap'),
|
||||
toastWrap: $('.toast-wrap'),
|
||||
registerResetButton: $('.register-reset-btn'),
|
||||
searchResetButton: $('.search-reset-btn'),
|
||||
membershipSubmitButton: $('#membership-submit-btn'),
|
||||
membershipTrialCheckBox: $('#id_membership_trial')
|
||||
};
|
||||
|
||||
const lazyCheckPhoneNumber = _.debounce(checkPhoneNumber, 250);
|
||||
_dom.phoneNumberField.on('input', (e) => {
|
||||
const val = e.target.value.trim();
|
||||
const validationMsg = validatePhoneNumber(val);
|
||||
if (validationMsg !== '') {
|
||||
setSelectedUser(null, true);
|
||||
setFieldState(_dom.phoneNumberField, 'error', validationMsg);
|
||||
return;
|
||||
}
|
||||
lazyCheckPhoneNumber();
|
||||
});
|
||||
/* Card number as you type */
|
||||
_dom.cardNumberField.on('input', (e) => {
|
||||
/* Card number should exist in the database and not tied to existing user */
|
||||
const val = e.target.value.trim();
|
||||
const validationMsg = validateCardNumber(val);
|
||||
if (validationMsg !== '') {
|
||||
setFieldState(_dom.cardNumberField, 'error', validationMsg);
|
||||
return;
|
||||
}
|
||||
$.getJSON(urls.checkCard, { card_number: val }, (data) => {
|
||||
if (data.error) {
|
||||
updateSubmitButton();
|
||||
cardForm.fields.cardNumber = false;
|
||||
setFieldState(_dom.cardNumberField, 'error', data.error);
|
||||
return;
|
||||
}
|
||||
|
||||
const card = data;
|
||||
const { user } = data;
|
||||
const { order } = data;
|
||||
// const todayPlusThirtyDays = moment().add(30, 'days');
|
||||
|
||||
if (!card) {
|
||||
cardForm.fields.cardNumber = false;
|
||||
setFieldState(_dom.cardNumberField, 'error', 'Cannot find card number in database.');
|
||||
} else if (card && card.registered !== null && user === null && order && !order.product.is_valid) {
|
||||
/* An already registered card (with no user) can repurchase membership if expired */
|
||||
setSelectedOrder(order);
|
||||
cardForm.fields.cardNumber = true;
|
||||
setFieldState(
|
||||
_dom.cardNumberField,
|
||||
'success',
|
||||
`Card number is in use (not activated) and belongs to ${formatPhoneNumber(order.phone_number)}.`
|
||||
);
|
||||
} else if (card && card.registered !== null) {
|
||||
let ownerString = '???';
|
||||
if (user) {
|
||||
ownerString = `existing user: ${user.first_name} ${user.last_name} (${formatPhoneNumber(user.phone_number)})`;
|
||||
} else {
|
||||
setSelectedOrder(order);
|
||||
ownerString = `phone number: ${formatPhoneNumber(order.phone_number)}`;
|
||||
}
|
||||
cardForm.fields.cardNumber = false;
|
||||
setFieldState(_dom.cardNumberField, 'error', `Card number is in use and belongs to ${ownerString}.`);
|
||||
} else if (card && user === null) {
|
||||
cardForm.fields.cardNumber = true;
|
||||
setFieldState(_dom.cardNumberField, 'success');
|
||||
} else {
|
||||
cardForm.fields.cardNumber = true;
|
||||
setFieldState(_dom.cardNumberField, '');
|
||||
}
|
||||
updateSubmitButton();
|
||||
}).fail(() => {
|
||||
cardForm.fields.cardNumber = false;
|
||||
setFieldState(_dom.cardNumberField, 'error', 'Cannot find card number in database.');
|
||||
});
|
||||
});
|
||||
|
||||
/* User search as you type */
|
||||
_dom.query.on('input', () => {
|
||||
const val = _dom.query.val().trim();
|
||||
if (val.length <= 2) {
|
||||
_dom.results.html('');
|
||||
return;
|
||||
}
|
||||
if (searching) {
|
||||
searching.abort();
|
||||
}
|
||||
searching = $.getJSON(urls.userSearch, { search: val }, (data) => {
|
||||
if (data.results && data.results.length > 0) {
|
||||
const sortedResults = sortSearchResults(data.results);
|
||||
_dom.results.html(formatResults(sortedResults));
|
||||
users = data.results;
|
||||
} else if (data.error) {
|
||||
_dom.results.html(data.error);
|
||||
} else if (val !== '') {
|
||||
_dom.results.html(`Found no user matching: '${val}'`);
|
||||
} else {
|
||||
_dom.results.html('');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/* On search result click */
|
||||
_dom.results.on('click', '.search-result input', () => {
|
||||
const label = $(this).parent();
|
||||
let number = label.attr('data-phone-number');
|
||||
const userId = label.attr('data-user-id');
|
||||
|
||||
/* Update selected state */
|
||||
$('.search-result').removeClass('selected');
|
||||
label.toggleClass('selected');
|
||||
|
||||
setSelectedUser(_.findWhere(users, { id: userId }));
|
||||
|
||||
/* Update phone number field */
|
||||
if (number === '-') {
|
||||
number = '';
|
||||
}
|
||||
_dom.phoneNumberField.val(number).trigger('input');
|
||||
});
|
||||
/* On submit button click */
|
||||
_dom.registerSubmitButton.on('click', (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!cardFormIsValid()) {
|
||||
setToast('Either phone number or card number is not valid', 'error');
|
||||
return;
|
||||
}
|
||||
const payload = getFormData(_dom.registerCardForm);
|
||||
|
||||
/* Register type */
|
||||
// FIXME: get from form
|
||||
// action: 'new_card_membership', 'update_card', 'add_or_renew', 'sms_card_notify'
|
||||
if (selectedUser === null && membershipOrder === null) {
|
||||
// Unknown phone / card, new membership
|
||||
payload.action = 'new_card_membership';
|
||||
} else if (selectedUser === null && membershipOrder !== null) {
|
||||
if (membershipOrder.member_card === null && membershipOrder.product.is_valid) {
|
||||
// Associate member card with a valid membership order
|
||||
payload.action = 'sms_card_notify';
|
||||
payload.order_uuid = membershipOrder.uuid;
|
||||
} else {
|
||||
// New card for expired order -- treat as new card membership
|
||||
payload.action = 'new_card_membership';
|
||||
}
|
||||
} else if (selectedUser !== null && selectedUser.is_member) {
|
||||
// Add member card to a user
|
||||
payload.action = 'update_card';
|
||||
} else {
|
||||
// New order for user
|
||||
payload.action = 'add_or_renew'; // Note: could also update card number
|
||||
}
|
||||
$.ajax(urls.registerCardAndMembership, {
|
||||
data: JSON.stringify(payload),
|
||||
contentType: 'application/json',
|
||||
dataType: 'json',
|
||||
type: 'post',
|
||||
headers: { 'X-CSRFToken': getCookie('csrftoken') }
|
||||
})
|
||||
.done(() => {
|
||||
let successMessage = `New card registered ${payload.card_number}`;
|
||||
if (payload.action === 'add_or_renew') {
|
||||
const fullName = `${selectedUser.first_name} ${selectedUser.last_name}`;
|
||||
successMessage = `New membership and card number ${payload.card_number} registered to ${fullName} `;
|
||||
} else if (payload.action === 'new_card_membership') {
|
||||
successMessage = `New membership and card registered to ${formatPhoneNumber(
|
||||
payload.phone_number
|
||||
)}. Activation SMS sent `;
|
||||
} else if (payload.action === 'sms_card_notify') {
|
||||
successMessage = `New card registered to ${formatPhoneNumber(
|
||||
payload.phone_number
|
||||
)}. Membership already paid ${membershipOrder.product.start_date}. Activation SMS sent `;
|
||||
}
|
||||
setToast(`${successMessage} :-)`, 'success');
|
||||
resetCardForm(true);
|
||||
})
|
||||
.fail((data) => {
|
||||
console.log('failed', data);
|
||||
let errorText = data.responseText;
|
||||
if (data.responseJSON) {
|
||||
if (data.responseJSON.non_field_errors) {
|
||||
errorText = data.responseJSON.non_field_errors.join('<br>');
|
||||
} else {
|
||||
errorText = 'Not sure why. :-(';
|
||||
}
|
||||
}
|
||||
setToast(`Failed! ${errorText}`, 'error');
|
||||
});
|
||||
});
|
||||
|
||||
/* Membership renewal button */
|
||||
_dom.membershipSubmitButton.on('click', (e) => {
|
||||
e.preventDefault();
|
||||
if (!selectedUser && !membershipOrder) {
|
||||
setToast('Computer says no.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = getFormData(_dom.registerCardForm);
|
||||
|
||||
/* If order, make sure to bring along both card number and phone number */
|
||||
if (membershipOrder) {
|
||||
payload.card_number = membershipOrder.member_card;
|
||||
payload.phone_number = membershipOrder.phone_number;
|
||||
}
|
||||
|
||||
$.ajax(urls.renewMembership, {
|
||||
data: JSON.stringify(payload),
|
||||
contentType: 'application/json',
|
||||
dataType: 'json',
|
||||
type: 'post',
|
||||
headers: { 'X-CSRFToken': getCookie('csrftoken') }
|
||||
})
|
||||
.done((data) => {
|
||||
let name = 'unknown';
|
||||
if (data.user) {
|
||||
name = `${selectedUser.first_name} ${selectedUser.last_name}`;
|
||||
} else {
|
||||
name = `phone number ${data.phone_number}`;
|
||||
}
|
||||
const successMessage = `Membership renewed for ${name} :-)`;
|
||||
setToast(successMessage, 'success');
|
||||
resetCardForm(true);
|
||||
})
|
||||
.fail((data) => {
|
||||
console.log('failed', data);
|
||||
let errorText = data.responseText;
|
||||
if (data.responseJSON) {
|
||||
if (data.responseJSON.non_field_errors) {
|
||||
errorText = data.responseJSON.non_field_errors.join('<br>');
|
||||
} else {
|
||||
errorText = 'Not sure why. :-(';
|
||||
}
|
||||
}
|
||||
setToast(`Failed! ${errorText}`, 'error');
|
||||
});
|
||||
});
|
||||
|
||||
/* On user id change */
|
||||
_dom.userIdField.on('change', function() {
|
||||
/* Render selected user in search form */
|
||||
renderSelectedUser(selectedUser);
|
||||
});
|
||||
/* On order uuid change */
|
||||
_dom.orderUuidField.on('change', function() {
|
||||
/* Render selected order */
|
||||
renderSelectedOrder(membershipOrder);
|
||||
});
|
||||
/* Reset buttons */
|
||||
_dom.registerResetButton.on('click', function() {
|
||||
resetCardForm();
|
||||
});
|
||||
_dom.searchResetButton.on('click', function() {
|
||||
resetSearchForm();
|
||||
});
|
||||
|
||||
/* Ninja add some icons to bootstrap form */
|
||||
const iconMobile =
|
||||
'<span class="input-group-addon"><span class="glyphicon glyphicon-phone glyphicon-phone-large"></span></span>';
|
||||
_dom.phoneNumberField.parent().addClass('input-group');
|
||||
_dom.phoneNumberField.before(iconMobile);
|
||||
|
||||
/* Render initial placeholder user */
|
||||
const selectedUserHtml = nunjucksEnv.render('search_result.html', { placeholder: true });
|
||||
_dom.selectedEntityWrap.html(selectedUserHtml);
|
||||
|
||||
/* Initial focus */
|
||||
if (_dom.usernameField) {
|
||||
_dom.usernameField.focus();
|
||||
}
|
||||
|
||||
/* Load query params */
|
||||
if (getParameterByName('user_id')) {
|
||||
_dom.userIdField.val(getParameterByName('user_id')).trigger('change');
|
||||
}
|
||||
if (getParameterByName('phone_number')) {
|
||||
_dom.phoneNumberField.val(getParameterByName('phone_number')).trigger('input');
|
||||
}
|
||||
if (getParameterByName('card_number')) {
|
||||
_dom.cardNumberField.val(getParameterByName('card_number')).trigger('input');
|
||||
}
|
||||
|
||||
$('[data-toggle="popover"]').popover({
|
||||
html: true,
|
||||
trigger: 'click'
|
||||
});
|
||||
});
|
||||
@@ -1,11 +1,11 @@
|
||||
$icon-font-path: '../fonts/';
|
||||
$icon-font-path: './fonts/';
|
||||
$green-color: #5cb85c;
|
||||
$red-color: #ff433d;
|
||||
$yellow-color: #f8ff5b;
|
||||
$active-color: darkred;
|
||||
|
||||
/* Vendor styles */
|
||||
@import "../../bower_components/bootstrap-sass/assets/stylesheets/_bootstrap.scss";
|
||||
@import "../node_modules/bootstrap-sass/assets/stylesheets/_bootstrap.scss";
|
||||
|
||||
/* Global styles */
|
||||
body {
|
||||
@@ -1,635 +0,0 @@
|
||||
function getCookie(name) {
|
||||
var cookieValue = null;
|
||||
if (document.cookie && document.cookie !== '') {
|
||||
var cookies = document.cookie.split(';');
|
||||
for (var i = 0; i < cookies.length; i++) {
|
||||
var cookie = jQuery.trim(cookies[i]);
|
||||
// Does this cookie string begin with the name we want?
|
||||
if (cookie.substring(0, name.length + 1) == (name + '=')) {
|
||||
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return cookieValue;
|
||||
}
|
||||
|
||||
function getParameterByName(name) {
|
||||
name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]");
|
||||
var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"),
|
||||
results = regex.exec(location.search);
|
||||
return results === null ? "" : decodeURIComponent(results[1].replace(/\+/g, " "));
|
||||
}
|
||||
|
||||
|
||||
function format_phone_number(str) {
|
||||
if(!str || str.length <= 1) {
|
||||
return 'No phone number';
|
||||
}
|
||||
if(str.length == 11) {
|
||||
return str.slice(0, 3) + " " + str.slice(3, 6) + " " + str.slice(6, 8) + " " + str.slice(8);
|
||||
}
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
function sort_search_results(results) {
|
||||
return _.sortBy(results, function (x) {
|
||||
if (x.last_membership) {
|
||||
if (x.last_membership.end_date) {
|
||||
return 0 - moment(x.last_membership.end_date).unix();
|
||||
}
|
||||
return 0 - moment(x.last_membership.start_date).unix();
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
function format_results(results) {
|
||||
return nunjucksEnv.render('search_results.html', {'results': results});
|
||||
}
|
||||
function render_selected_user(user_data) {
|
||||
var context;
|
||||
if (!user_data) {
|
||||
context = {placeholder: true}; // dummy card
|
||||
} else {
|
||||
context = {res: user_data, checked: true, no_user_actions: true};
|
||||
}
|
||||
var selected_user_html = nunjucksEnv.render('search_result.html', context);
|
||||
_dom.selectedEntityWrap.html(selected_user_html);
|
||||
}
|
||||
function render_selected_order(order) {
|
||||
if (!order) { return; }
|
||||
var context = {order: order, checked: true, no_user_actions: true};
|
||||
var selected_order_html = nunjucksEnv.render('selected_order.html', context);
|
||||
_dom.selectedEntityWrap.html(selected_order_html);
|
||||
}
|
||||
function set_toast(message, message_type) {
|
||||
var context = {
|
||||
message: message,
|
||||
message_type: message_type,
|
||||
icon: state_to_icon[message_type]
|
||||
};
|
||||
var html = nunjucksEnv.render('toast.html', context);
|
||||
_dom.toastWrap.html(html);
|
||||
_dom.toastWrap.addClass('visible');
|
||||
setTimeout(function() {
|
||||
_dom.toastWrap.removeClass('visible');
|
||||
}, 5000);
|
||||
}
|
||||
function set_selected_user(user, update_search_result) {
|
||||
selectedUser = user;
|
||||
updateMemberShipButton(user, null);
|
||||
|
||||
_dom.userIdField.val((user && user.id) || '').trigger('change');
|
||||
if(update_search_result) {
|
||||
var stale_search_result = _dom.results.find('.search-result').removeClass('selected');
|
||||
stale_search_result.find('input').prop('checked', false);
|
||||
/* User is still in search result, mark selected */
|
||||
if( user && _.findWhere(users, {id:user.id}) ) {
|
||||
var search_result = _dom.results.find('[data-user-id="'+ user.id +'"]').toggleClass('selected');
|
||||
search_result.find('input').prop('checked', true);
|
||||
}
|
||||
}
|
||||
}
|
||||
function set_selected_order(order) {
|
||||
membershipOrder = order;
|
||||
if (order) {
|
||||
updateMemberShipButton(null, order);
|
||||
}
|
||||
_dom.orderUuidField.val((order && order.uuid) || '').trigger('change');
|
||||
}
|
||||
|
||||
function set_field_state(field_element, state, help_text) {
|
||||
if(!help_text) {
|
||||
help_text = '';
|
||||
}
|
||||
var help_span = '<span class="help-block ">'+ help_text +'</span>';
|
||||
var feedback_span = '<span class="glyphicon glyphicon-'+ state_to_icon[state] +' form-control-feedback" aria-hidden="true"></span>';
|
||||
var css_class = 'has-'+state;
|
||||
var form_group = field_element.parent().parent();
|
||||
form_group.removeClass('has-success has-warning has-error has-feedback'); // reset
|
||||
if(['success', 'warning', 'error'].indexOf(state) !== -1) {
|
||||
form_group.addClass(css_class +' has-feedback');
|
||||
/* Feedback icon */
|
||||
var feedback_el = form_group.find('.form-control-feedback');
|
||||
if (!feedback_el.length) {
|
||||
// add
|
||||
form_group.append(feedback_span);
|
||||
} else {
|
||||
// Update
|
||||
feedback_el.removeClass('glyphicon-ok glypicon-warning-sign glyphicon-remove');
|
||||
feedback_el.addClass('glyphicon-'+state_to_icon[state]);
|
||||
}
|
||||
/* Help text */
|
||||
var help_text_el = form_group.find('.help-block');
|
||||
if(!help_text_el.length) {
|
||||
form_group.append(help_span);
|
||||
} else {
|
||||
help_text_el.text(help_text);
|
||||
}
|
||||
}
|
||||
else {
|
||||
form_group.find('.form-control-feedback').remove();
|
||||
form_group.find('.help-block').remove();
|
||||
}
|
||||
}
|
||||
function resetCardForm(reset_native) {
|
||||
if(reset_native) {
|
||||
_dom.registerCardForm.get(0).reset();
|
||||
}
|
||||
/* Clear field states */
|
||||
set_field_state(_dom.phoneNumberField, '');
|
||||
cardForm.fields.phoneNumber = false;
|
||||
set_field_state(_dom.cardNumberField, '');
|
||||
cardForm.fields.cardNumber = false;
|
||||
selectedUser = null;
|
||||
membershipOrder = null;
|
||||
_dom.userIdField.val('').trigger('change');
|
||||
_dom.membershipTrialCheckBox.prop('checked', false);
|
||||
|
||||
/* Disable submit buttons */
|
||||
_dom.registerSubmitButton.prop('disabled', true);
|
||||
_dom.membershipSubmitButton.prop('disabled', true);
|
||||
}
|
||||
function resetSearchForm(reset_native) {
|
||||
if(reset_native) {
|
||||
_dom.searchResetButton.get(0).reset();
|
||||
}
|
||||
/* Clear search results */
|
||||
users = null;
|
||||
_dom.results.html('');
|
||||
}
|
||||
|
||||
function cardFormIsValid() {
|
||||
return _.all(_.values(cardForm.fields));
|
||||
}
|
||||
|
||||
function update_submit_button() {
|
||||
if( cardFormIsValid() ) {
|
||||
_dom.registerSubmitButton.prop('disabled', false);
|
||||
return;
|
||||
}
|
||||
_dom.registerSubmitButton.prop('disabled', true);
|
||||
}
|
||||
function updateMemberShipButton(user, order) {
|
||||
var today_plus_thirty_days = moment().add(30, 'days').format('YYYY-MM-DD');
|
||||
/* User with valid membership */
|
||||
if (user && user.is_member) {
|
||||
/* ...which is lifelong cannot renew */
|
||||
if (user.last_membership.membership_type == 'lifelong') {
|
||||
_dom.membershipSubmitButton.prop('disabled', true);
|
||||
}
|
||||
/* ...or expiring soon and the user has a card */
|
||||
//else if (user.last_membership.end_date <= today_plus_thirty_days && user.active_member_card) {
|
||||
/* ...with a membership that expires soon can renew */
|
||||
else if (user.last_membership.end_date <= today_plus_thirty_days) {
|
||||
_dom.membershipSubmitButton.prop('disabled', false);
|
||||
}
|
||||
}
|
||||
/* User with expired membership can renew */
|
||||
// else if (user && !user.is_member && user.active_member_card) {
|
||||
else if (user && !user.is_member) {
|
||||
_dom.membershipSubmitButton.prop('disabled', false);
|
||||
}
|
||||
/* Order */
|
||||
else if (order) {
|
||||
/* If the membership has expired it can be renewed */
|
||||
if (!order.product.is_valid && order.product.end_date <= today_plus_thirty_days) {
|
||||
_dom.membershipSubmitButton.prop('disabled', false);
|
||||
}
|
||||
}
|
||||
else {
|
||||
_dom.membershipSubmitButton.prop('disabled', true);
|
||||
}
|
||||
}
|
||||
function validatePhoneNumber(val) {
|
||||
if(val.length === 0) {
|
||||
cardForm.fields.phoneNumber = false;
|
||||
update_submit_button();
|
||||
return 'Phone number should not be empty.';
|
||||
}
|
||||
cardForm.fields.phoneNumber = true;
|
||||
update_submit_button();
|
||||
return '';
|
||||
}
|
||||
|
||||
function validateCardNumber(val) {
|
||||
if(val.length === 0) {
|
||||
cardForm.fields.cardNumber = false;
|
||||
update_submit_button();
|
||||
return 'Card number should not be empty.';
|
||||
}
|
||||
if(val.length > 0 && val[0] != "1") {
|
||||
cardForm.fields.cardNumber = false;
|
||||
update_submit_button();
|
||||
return 'Card number should start with 1.';
|
||||
}
|
||||
if(val.length !== 9) {
|
||||
cardForm.fields.cardNumber = false;
|
||||
update_submit_button();
|
||||
return 'Card number should be 9 digits long.';
|
||||
}
|
||||
cardForm.fields.cardNumber = true;
|
||||
update_submit_button();
|
||||
return '';
|
||||
}
|
||||
|
||||
function getFormData(formElement) {
|
||||
var formData = formElement.serializeArray();
|
||||
formData = _.object(_.map(formData, function (x) {
|
||||
return [x.name, x.value || null];
|
||||
}));
|
||||
/* no need for radio button state */
|
||||
delete formData.user;
|
||||
|
||||
return formData;
|
||||
}
|
||||
|
||||
/* Phone number as you type */
|
||||
function checkPhoneNumber() {
|
||||
var val = _dom.phoneNumberField.val().trim();
|
||||
$.getJSON(urls.checkPhoneNumber, {phone_number: val}, function(data) {
|
||||
/* Invalid phone number?*/
|
||||
if(data.error) {
|
||||
set_selected_user(null, true);
|
||||
set_selected_order(null);
|
||||
set_field_state(_dom.phoneNumberField, 'error', data.error);
|
||||
cardForm.fields.phoneNumber = false;
|
||||
update_submit_button();
|
||||
return;
|
||||
}
|
||||
|
||||
var user = data.user;
|
||||
var order = data.order;
|
||||
var message = '';
|
||||
|
||||
/* Has valid card membership */
|
||||
if (!user && order && order.member_card && order.product.is_valid) {
|
||||
message = 'Phone number is already tied to card '+ order.member_card + ' and is valid until '+ order.product.end_date +'.';
|
||||
set_field_state(_dom.phoneNumberField, 'error', message);
|
||||
cardForm.fields.phoneNumber = false;
|
||||
set_selected_order(order);
|
||||
update_submit_button();
|
||||
return;
|
||||
}
|
||||
|
||||
if (user) {
|
||||
/* Existing user */
|
||||
message = 'Phone number belongs to existing user.';
|
||||
set_field_state(_dom.phoneNumberField, 'success', message);
|
||||
set_selected_user(user, true);
|
||||
set_selected_order(null);
|
||||
cardForm.fields.phoneNumber = true;
|
||||
update_submit_button();
|
||||
} else if (order) {
|
||||
set_selected_order(order);
|
||||
/* Valid membership, no card associated */
|
||||
if (order.product.is_valid) {
|
||||
message = 'Phone number has a valid membership.';
|
||||
} else {
|
||||
message = 'Phone number has an expired membership.';
|
||||
}
|
||||
} else {
|
||||
/* Valid phone number associated user or order */
|
||||
set_selected_order(null);
|
||||
set_selected_user(null);
|
||||
}
|
||||
|
||||
set_field_state(_dom.phoneNumberField, 'success', message);
|
||||
cardForm.fields.phoneNumber = true;
|
||||
update_submit_button();
|
||||
});
|
||||
}
|
||||
|
||||
/* Init and global vars */
|
||||
var _dom;
|
||||
var users;
|
||||
var searching; /* search ajax request */
|
||||
var selectedUser = null;
|
||||
var membershipOrder = null; /* selected order object */
|
||||
var cardForm = {
|
||||
fields: {
|
||||
cardNumber: false,
|
||||
phoneNumber: false
|
||||
}
|
||||
};
|
||||
var state_to_icon = {
|
||||
success: 'ok',
|
||||
warning: 'warning-sign',
|
||||
error: 'remove'
|
||||
};
|
||||
var nunjucksEnv = new nunjucks.Environment();
|
||||
/* Add filter |phoneNumber */
|
||||
nunjucksEnv.addFilter('phoneNumber', format_phone_number);
|
||||
|
||||
var urls = {
|
||||
userSearch: '/user-search/',
|
||||
checkPhoneNumber: '/check-phone-number/',
|
||||
checkCard: '/check-card/',
|
||||
registerCardAndMembership: '/register-card-membership/',
|
||||
renewMembership: '/renew-membership/'
|
||||
};
|
||||
|
||||
|
||||
|
||||
$(document).ready(function(){
|
||||
_dom = {
|
||||
results: $('.results-wrap'),
|
||||
query: $('#id_query'),
|
||||
phoneNumberLabel: $('[for=id_phone_number]'),
|
||||
phoneNumberField: $('#id_phone_number'),
|
||||
cardNumberField: $('#id_card_number'),
|
||||
registerCardForm: $('.register-card-form'),
|
||||
registerSubmitButton: $('#register-submit-btn'),
|
||||
usernameField: $('#id_username'),
|
||||
userIdField: $('#id_user_id'),
|
||||
orderUuidField: $('#id_order_uuid'),
|
||||
selectedEntityWrap: $('.register-card-form--selected-entity-wrap'),
|
||||
toastWrap: $('.toast-wrap'),
|
||||
registerResetButton: $('.register-reset-btn'),
|
||||
searchResetButton: $('.search-reset-btn'),
|
||||
membershipSubmitButton: $('#membership-submit-btn'),
|
||||
membershipTrialCheckBox: $('#id_membership_trial')
|
||||
};
|
||||
|
||||
var lazyCheckPhoneNumber = _.debounce(checkPhoneNumber, 250);
|
||||
_dom.phoneNumberField.on('input', function() {
|
||||
var val = _dom.phoneNumberField.val().trim();
|
||||
var validation_msg = validatePhoneNumber(val);
|
||||
if( validation_msg !== '') {
|
||||
set_selected_user(null, true);
|
||||
set_field_state(_dom.phoneNumberField, 'error', validation_msg);
|
||||
return;
|
||||
}
|
||||
lazyCheckPhoneNumber();
|
||||
});
|
||||
/* Card number as you type */
|
||||
_dom.cardNumberField.on('input', function() {
|
||||
/* Card number should exist in the database and not tied to existing user */
|
||||
var val = _dom.cardNumberField.val().trim();
|
||||
var validation_msg = validateCardNumber(val);
|
||||
if (validation_msg !== '') {
|
||||
set_field_state(_dom.cardNumberField, 'error', validation_msg);
|
||||
return;
|
||||
}
|
||||
$.getJSON(urls.checkCard, {card_number: val}, function(data) {
|
||||
if (data.error) {
|
||||
update_submit_button();
|
||||
cardForm.fields.cardNumber = false;
|
||||
set_field_state(_dom.cardNumberField, 'error', data.error);
|
||||
return;
|
||||
}
|
||||
|
||||
var card = data;
|
||||
var user = data.user;
|
||||
var order = data.order;
|
||||
var today_plus_thirty_days = moment().add(30, 'days');
|
||||
|
||||
if (!card) {
|
||||
cardForm.fields.cardNumber = false;
|
||||
set_field_state(_dom.cardNumberField, 'error',
|
||||
'Cannot find card number in database.');
|
||||
}
|
||||
else if (card && card.registered !== null && user === null && order && !order.product.is_valid) {
|
||||
/* An already registered card (with no user) can repurchase membership if expired */
|
||||
set_selected_order(order);
|
||||
cardForm.fields.cardNumber = true;
|
||||
set_field_state(_dom.cardNumberField, 'success',
|
||||
'Card number is in use (not activated) and belongs to ' + format_phone_number(order.phone_number) + '.');
|
||||
}
|
||||
else if (card && card.registered !== null) {
|
||||
var owner_string = '???';
|
||||
if (user) {
|
||||
owner_string = 'existing user: ' + user.first_name + ' ' + user.last_name + ' (' + format_phone_number(user.phone_number) + ')';
|
||||
} else {
|
||||
set_selected_order(order);
|
||||
owner_string = 'phone number: '+ format_phone_number(order.phone_number);
|
||||
}
|
||||
cardForm.fields.cardNumber = false;
|
||||
set_field_state(_dom.cardNumberField, 'error',
|
||||
'Card number is in use and belongs to '+ owner_string +'.');
|
||||
}
|
||||
else if (card && user === null) {
|
||||
cardForm.fields.cardNumber = true;
|
||||
set_field_state(_dom.cardNumberField, 'success');
|
||||
} else {
|
||||
cardForm.fields.cardNumber = true;
|
||||
set_field_state(_dom.cardNumberField, '');
|
||||
}
|
||||
update_submit_button();
|
||||
}).fail(function(data) {
|
||||
cardForm.fields.cardNumber = false;
|
||||
set_field_state(_dom.cardNumberField, 'error',
|
||||
'Cannot find card number in database.');
|
||||
});
|
||||
});
|
||||
|
||||
/* User search as you type */
|
||||
_dom.query.on('input', function() {
|
||||
var val = _dom.query.val().trim();
|
||||
if(val.length <= 2) {
|
||||
_dom.results.html('');
|
||||
return;
|
||||
}
|
||||
if (searching) {
|
||||
searching.abort();
|
||||
}
|
||||
searching = $.getJSON(urls.userSearch, {search: val}, function(data) {
|
||||
if (data.results && data.results.length > 0) {
|
||||
sorted_results = sort_search_results(data.results);
|
||||
_dom.results.html(format_results(sorted_results));
|
||||
users = data.results;
|
||||
}
|
||||
else if (data.error) {
|
||||
_dom.results.html(data.error);
|
||||
}
|
||||
else {
|
||||
if(val !== "") {
|
||||
_dom.results.html("Found no user matching: '"+ val +"'");
|
||||
} else {
|
||||
_dom.results.html('');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/* On search result click */
|
||||
_dom.results.on('click', '.search-result input', function(e){
|
||||
var label = $(this).parent();
|
||||
var number = label.attr('data-phone-number');
|
||||
var user_id = label.attr('data-user-id');
|
||||
|
||||
/* Update selected state */
|
||||
$('.search-result').removeClass('selected');
|
||||
label.toggleClass('selected');
|
||||
|
||||
set_selected_user(_.findWhere(users, {id: user_id}));
|
||||
|
||||
/* Update phone number field */
|
||||
if (number === '-') {
|
||||
number = '';
|
||||
}
|
||||
_dom.phoneNumberField.val(number).trigger('input');
|
||||
});
|
||||
/* On submit button click */
|
||||
_dom.registerSubmitButton.on('click', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if( !cardFormIsValid() ) {
|
||||
set_toast('Either phone number or card number is not valid', 'error');
|
||||
return;
|
||||
}
|
||||
var payload = getFormData(_dom.registerCardForm);
|
||||
|
||||
/* Register type */
|
||||
// FIXME: get from form
|
||||
// action: 'new_card_membership', 'update_card', 'add_or_renew', 'sms_card_notify'
|
||||
if (selectedUser === null && membershipOrder === null) {
|
||||
// Unknown phone / card, new membership
|
||||
payload.action = 'new_card_membership';
|
||||
} else if (selectedUser === null && membershipOrder !== null) {
|
||||
if (membershipOrder.member_card === null && membershipOrder.product.is_valid) {
|
||||
// Associate member card with a valid membership order
|
||||
payload.action = 'sms_card_notify';
|
||||
payload.order_uuid = membershipOrder.uuid;
|
||||
} else {
|
||||
// New card for expired order -- treat as new card membership
|
||||
payload.action = 'new_card_membership';
|
||||
}
|
||||
} else if (selectedUser !== null && selectedUser.is_member) {
|
||||
// Add member card to a user
|
||||
payload.action = 'update_card';
|
||||
} else {
|
||||
// New order for user
|
||||
payload.action = 'add_or_renew'; // Note: could also update card number
|
||||
}
|
||||
$.ajax(urls.registerCardAndMembership, {
|
||||
data: JSON.stringify(payload),
|
||||
contentType: 'application/json',
|
||||
dataType: 'json',
|
||||
type: 'post',
|
||||
headers: {'X-CSRFToken': getCookie('csrftoken')}
|
||||
}).done(function(data) {
|
||||
var success_message = 'New card registered ' + payload.card_number;
|
||||
if (payload.action === 'add_or_renew') {
|
||||
var full_name = selectedUser.first_name + ' ' + selectedUser.last_name;
|
||||
success_message = 'New membership and card number '+ payload.card_number + ' registered to ' + full_name + ' ';
|
||||
}
|
||||
else if (payload.action ==='new_card_membership') {
|
||||
success_message = 'New membership and card registered to ' + format_phone_number(payload.phone_number) + '. Activation SMS sent ';
|
||||
}
|
||||
else if (payload.action === 'sms_card_notify') {
|
||||
success_message = 'New card registered to ' + format_phone_number(payload.phone_number) + '. Membership already paid '+ membershipOrder.product.start_date +'. Activation SMS sent ';
|
||||
}
|
||||
set_toast(success_message + ' :-)', 'success');
|
||||
resetCardForm(true);
|
||||
|
||||
}).fail(function(data) {
|
||||
console.log("failed", data);
|
||||
var error_text = data.responseText;
|
||||
if (data.responseJSON) {
|
||||
if (data.responseJSON.non_field_errors) {
|
||||
error_text = data.responseJSON.non_field_errors.join('<br>');
|
||||
} else {
|
||||
error_text = 'Not sure why. :-(';
|
||||
}
|
||||
}
|
||||
set_toast('Failed! '+ error_text, 'error');
|
||||
});
|
||||
});
|
||||
|
||||
/* Membership renewal button */
|
||||
_dom.membershipSubmitButton.on('click', function(e) {
|
||||
e.preventDefault();
|
||||
if (!selectedUser && !membershipOrder) {
|
||||
set_toast('Computer says no.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
var payload = getFormData(_dom.registerCardForm);
|
||||
|
||||
/* If order, make sure to bring along both card number and phone number */
|
||||
if (membershipOrder) {
|
||||
payload.card_number = membershipOrder.member_card;
|
||||
payload.phone_number = membershipOrder.phone_number;
|
||||
}
|
||||
|
||||
$.ajax(urls.renewMembership, {
|
||||
data: JSON.stringify(payload),
|
||||
contentType: 'application/json',
|
||||
dataType: 'json',
|
||||
type: 'post',
|
||||
headers: {'X-CSRFToken': getCookie('csrftoken')}
|
||||
}).done(function(data) {
|
||||
var name = 'unknown';
|
||||
if (data.user) {
|
||||
name = selectedUser.first_name + ' ' + selectedUser.last_name;
|
||||
} else {
|
||||
name = 'phone number ' + data.phone_number;
|
||||
}
|
||||
var success_message = 'Membership renewed for ' + name + ' :-)';
|
||||
set_toast(success_message, 'success');
|
||||
resetCardForm(true);
|
||||
}).fail(function(data) {
|
||||
console.log("failed", data);
|
||||
var error_text = data.responseText;
|
||||
if (data.responseJSON) {
|
||||
if (data.responseJSON.non_field_errors) {
|
||||
error_text = data.responseJSON.non_field_errors.join('<br>');
|
||||
} else {
|
||||
error_text = 'Not sure why. :-(';
|
||||
}
|
||||
}
|
||||
set_toast('Failed! '+ error_text, 'error');
|
||||
});
|
||||
});
|
||||
|
||||
/* On user id change */
|
||||
_dom.userIdField.on('change', function() {
|
||||
/* Render selected user in search form */
|
||||
render_selected_user(selectedUser);
|
||||
});
|
||||
/* On order uuid change */
|
||||
_dom.orderUuidField.on('change', function() {
|
||||
/* Render selected order */
|
||||
render_selected_order(membershipOrder);
|
||||
});
|
||||
/* Reset buttons */
|
||||
_dom.registerResetButton.on('click', function() {
|
||||
resetCardForm();
|
||||
});
|
||||
_dom.searchResetButton.on('click', function() {
|
||||
resetSearchForm();
|
||||
});
|
||||
|
||||
/* Ninja add some icons to bootstrap form */
|
||||
var iconMobile = '<span class="input-group-addon"><span class="glyphicon glyphicon-phone glyphicon-phone-large"></span></span>';
|
||||
_dom.phoneNumberField.parent().addClass('input-group');
|
||||
_dom.phoneNumberField.before(iconMobile);
|
||||
|
||||
/* Render initial placeholder user */
|
||||
var selected_user_html = nunjucksEnv.render('search_result.html', {placeholder: true});
|
||||
_dom.selectedEntityWrap.html(selected_user_html);
|
||||
|
||||
/* Initial focus */
|
||||
if(_dom.usernameField) {
|
||||
_dom.usernameField.focus();
|
||||
}
|
||||
|
||||
/* Load query params */
|
||||
if( getParameterByName('user_id') ) {
|
||||
_dom.userIdField.val(getParameterByName('user_id')).trigger('change');
|
||||
}
|
||||
if( getParameterByName('phone_number') ) {
|
||||
_dom.phoneNumberField.val(getParameterByName('phone_number')).trigger('input');
|
||||
}
|
||||
if( getParameterByName('card_number') ) {
|
||||
_dom.cardNumberField.val(getParameterByName('card_number')).trigger('input');
|
||||
}
|
||||
|
||||
$('[data-toggle="popover"]').popover({
|
||||
html: true,
|
||||
trigger: 'click'
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,8 @@
|
||||
/* eslint-disable */
|
||||
(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["search_result.html"] = (function() {
|
||||
function root(env, context, frame, runtime, cb) {
|
||||
var lineno = null;
|
||||
var colno = null;
|
||||
var lineno = 0;
|
||||
var colno = 0;
|
||||
var output = "";
|
||||
try {
|
||||
var parentTemplate = null;
|
||||
@@ -42,7 +43,7 @@ output += "</span>\n ";
|
||||
;
|
||||
}
|
||||
else {
|
||||
if(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "res")),"last_membership") == runtime.contextOrFrameLookup(context, frame, "null")) {
|
||||
if(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "res")),"last_membership") == null) {
|
||||
output += "\n <span class=\"label label-default\">Registered</span>\n ";
|
||||
;
|
||||
}
|
||||
@@ -107,18 +108,18 @@ root: root
|
||||
|
||||
})();
|
||||
})();
|
||||
|
||||
(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["search_results.html"] = (function() {
|
||||
function root(env, context, frame, runtime, cb) {
|
||||
var lineno = null;
|
||||
var colno = null;
|
||||
var lineno = 0;
|
||||
var colno = 0;
|
||||
var output = "";
|
||||
try {
|
||||
var parentTemplate = null;
|
||||
output += "<div class=\"list-group\">\n ";
|
||||
frame = frame.push();
|
||||
var t_3 = runtime.contextOrFrameLookup(context, frame, "results");
|
||||
if(t_3) {var t_2 = t_3.length;
|
||||
if(t_3) {t_3 = runtime.fromIterator(t_3);
|
||||
var t_2 = t_3.length;
|
||||
for(var t_1=0; t_1 < t_3.length; t_1++) {
|
||||
var t_4 = t_3[t_1];
|
||||
frame.set("res", t_4);
|
||||
@@ -133,15 +134,15 @@ output += "\n ";
|
||||
var tasks = [];
|
||||
tasks.push(
|
||||
function(callback) {
|
||||
env.getTemplate("search_result.html", false, "search_results.html", null, function(t_7,t_5) {
|
||||
if(t_7) { cb(t_7); return; }
|
||||
env.getTemplate("search_result.html", false, "search_results.html", false, function(t_6,t_5) {
|
||||
if(t_6) { cb(t_6); return; }
|
||||
callback(null,t_5);});
|
||||
});
|
||||
tasks.push(
|
||||
function(template, callback){
|
||||
template.render(context.getVariables(), frame, function(t_8,t_6) {
|
||||
template.render(context.getVariables(), frame, function(t_8,t_7) {
|
||||
if(t_8) { cb(t_8); return; }
|
||||
callback(null,t_6);});
|
||||
callback(null,t_7);});
|
||||
});
|
||||
tasks.push(
|
||||
function(result, callback){
|
||||
@@ -171,11 +172,10 @@ root: root
|
||||
|
||||
})();
|
||||
})();
|
||||
|
||||
(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["selected_order.html"] = (function() {
|
||||
function root(env, context, frame, runtime, cb) {
|
||||
var lineno = null;
|
||||
var colno = null;
|
||||
var lineno = 0;
|
||||
var colno = 0;
|
||||
var output = "";
|
||||
try {
|
||||
var parentTemplate = null;
|
||||
@@ -222,11 +222,10 @@ root: root
|
||||
|
||||
})();
|
||||
})();
|
||||
|
||||
(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["toast.html"] = (function() {
|
||||
function root(env, context, frame, runtime, cb) {
|
||||
var lineno = null;
|
||||
var colno = null;
|
||||
var lineno = 0;
|
||||
var colno = 0;
|
||||
var output = "";
|
||||
try {
|
||||
var parentTemplate = null;
|
||||
@@ -253,3 +252,4 @@ root: root
|
||||
|
||||
})();
|
||||
})();
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export const identity = (v) => v;
|
||||
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"name": "kassa",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"jquery": "3.2.1",
|
||||
"underscore": "1.8.3",
|
||||
"list.js": "1.5.0",
|
||||
"bootstrap-sass": "3.3.7",
|
||||
"nunjucks": "~3.0.1",
|
||||
"moment": "~2.18.1"
|
||||
},
|
||||
"devDependencies": {},
|
||||
"resolutions": {
|
||||
"jquery": "3.2.1"
|
||||
},
|
||||
"overrides": {
|
||||
"bootstrap-sass": {
|
||||
"main": [
|
||||
"assets/stylesheets/_bootstrap.scss",
|
||||
"assets/fonts/bootstrap/*",
|
||||
"assets/javascripts/bootstrap.js"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 117 KiB |
Vendored
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 6.7 KiB |
Vendored
-635
@@ -1,635 +0,0 @@
|
||||
function getCookie(name) {
|
||||
var cookieValue = null;
|
||||
if (document.cookie && document.cookie !== '') {
|
||||
var cookies = document.cookie.split(';');
|
||||
for (var i = 0; i < cookies.length; i++) {
|
||||
var cookie = jQuery.trim(cookies[i]);
|
||||
// Does this cookie string begin with the name we want?
|
||||
if (cookie.substring(0, name.length + 1) == (name + '=')) {
|
||||
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return cookieValue;
|
||||
}
|
||||
|
||||
function getParameterByName(name) {
|
||||
name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]");
|
||||
var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"),
|
||||
results = regex.exec(location.search);
|
||||
return results === null ? "" : decodeURIComponent(results[1].replace(/\+/g, " "));
|
||||
}
|
||||
|
||||
|
||||
function format_phone_number(str) {
|
||||
if(!str || str.length <= 1) {
|
||||
return 'No phone number';
|
||||
}
|
||||
if(str.length == 11) {
|
||||
return str.slice(0, 3) + " " + str.slice(3, 6) + " " + str.slice(6, 8) + " " + str.slice(8);
|
||||
}
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
function sort_search_results(results) {
|
||||
return _.sortBy(results, function (x) {
|
||||
if (x.last_membership) {
|
||||
if (x.last_membership.end_date) {
|
||||
return 0 - moment(x.last_membership.end_date).unix();
|
||||
}
|
||||
return 0 - moment(x.last_membership.start_date).unix();
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
function format_results(results) {
|
||||
return nunjucksEnv.render('search_results.html', {'results': results});
|
||||
}
|
||||
function render_selected_user(user_data) {
|
||||
var context;
|
||||
if (!user_data) {
|
||||
context = {placeholder: true}; // dummy card
|
||||
} else {
|
||||
context = {res: user_data, checked: true, no_user_actions: true};
|
||||
}
|
||||
var selected_user_html = nunjucksEnv.render('search_result.html', context);
|
||||
_dom.selectedEntityWrap.html(selected_user_html);
|
||||
}
|
||||
function render_selected_order(order) {
|
||||
if (!order) { return; }
|
||||
var context = {order: order, checked: true, no_user_actions: true};
|
||||
var selected_order_html = nunjucksEnv.render('selected_order.html', context);
|
||||
_dom.selectedEntityWrap.html(selected_order_html);
|
||||
}
|
||||
function set_toast(message, message_type) {
|
||||
var context = {
|
||||
message: message,
|
||||
message_type: message_type,
|
||||
icon: state_to_icon[message_type]
|
||||
};
|
||||
var html = nunjucksEnv.render('toast.html', context);
|
||||
_dom.toastWrap.html(html);
|
||||
_dom.toastWrap.addClass('visible');
|
||||
setTimeout(function() {
|
||||
_dom.toastWrap.removeClass('visible');
|
||||
}, 5000);
|
||||
}
|
||||
function set_selected_user(user, update_search_result) {
|
||||
selectedUser = user;
|
||||
updateMemberShipButton(user, null);
|
||||
|
||||
_dom.userIdField.val((user && user.id) || '').trigger('change');
|
||||
if(update_search_result) {
|
||||
var stale_search_result = _dom.results.find('.search-result').removeClass('selected');
|
||||
stale_search_result.find('input').prop('checked', false);
|
||||
/* User is still in search result, mark selected */
|
||||
if( user && _.findWhere(users, {id:user.id}) ) {
|
||||
var search_result = _dom.results.find('[data-user-id="'+ user.id +'"]').toggleClass('selected');
|
||||
search_result.find('input').prop('checked', true);
|
||||
}
|
||||
}
|
||||
}
|
||||
function set_selected_order(order) {
|
||||
membershipOrder = order;
|
||||
if (order) {
|
||||
updateMemberShipButton(null, order);
|
||||
}
|
||||
_dom.orderUuidField.val((order && order.uuid) || '').trigger('change');
|
||||
}
|
||||
|
||||
function set_field_state(field_element, state, help_text) {
|
||||
if(!help_text) {
|
||||
help_text = '';
|
||||
}
|
||||
var help_span = '<span class="help-block ">'+ help_text +'</span>';
|
||||
var feedback_span = '<span class="glyphicon glyphicon-'+ state_to_icon[state] +' form-control-feedback" aria-hidden="true"></span>';
|
||||
var css_class = 'has-'+state;
|
||||
var form_group = field_element.parent().parent();
|
||||
form_group.removeClass('has-success has-warning has-error has-feedback'); // reset
|
||||
if(['success', 'warning', 'error'].indexOf(state) !== -1) {
|
||||
form_group.addClass(css_class +' has-feedback');
|
||||
/* Feedback icon */
|
||||
var feedback_el = form_group.find('.form-control-feedback');
|
||||
if (!feedback_el.length) {
|
||||
// add
|
||||
form_group.append(feedback_span);
|
||||
} else {
|
||||
// Update
|
||||
feedback_el.removeClass('glyphicon-ok glypicon-warning-sign glyphicon-remove');
|
||||
feedback_el.addClass('glyphicon-'+state_to_icon[state]);
|
||||
}
|
||||
/* Help text */
|
||||
var help_text_el = form_group.find('.help-block');
|
||||
if(!help_text_el.length) {
|
||||
form_group.append(help_span);
|
||||
} else {
|
||||
help_text_el.text(help_text);
|
||||
}
|
||||
}
|
||||
else {
|
||||
form_group.find('.form-control-feedback').remove();
|
||||
form_group.find('.help-block').remove();
|
||||
}
|
||||
}
|
||||
function resetCardForm(reset_native) {
|
||||
if(reset_native) {
|
||||
_dom.registerCardForm.get(0).reset();
|
||||
}
|
||||
/* Clear field states */
|
||||
set_field_state(_dom.phoneNumberField, '');
|
||||
cardForm.fields.phoneNumber = false;
|
||||
set_field_state(_dom.cardNumberField, '');
|
||||
cardForm.fields.cardNumber = false;
|
||||
selectedUser = null;
|
||||
membershipOrder = null;
|
||||
_dom.userIdField.val('').trigger('change');
|
||||
_dom.membershipTrialCheckBox.prop('checked', false);
|
||||
|
||||
/* Disable submit buttons */
|
||||
_dom.registerSubmitButton.prop('disabled', true);
|
||||
_dom.membershipSubmitButton.prop('disabled', true);
|
||||
}
|
||||
function resetSearchForm(reset_native) {
|
||||
if(reset_native) {
|
||||
_dom.searchResetButton.get(0).reset();
|
||||
}
|
||||
/* Clear search results */
|
||||
users = null;
|
||||
_dom.results.html('');
|
||||
}
|
||||
|
||||
function cardFormIsValid() {
|
||||
return _.all(_.values(cardForm.fields));
|
||||
}
|
||||
|
||||
function update_submit_button() {
|
||||
if( cardFormIsValid() ) {
|
||||
_dom.registerSubmitButton.prop('disabled', false);
|
||||
return;
|
||||
}
|
||||
_dom.registerSubmitButton.prop('disabled', true);
|
||||
}
|
||||
function updateMemberShipButton(user, order) {
|
||||
var today_plus_thirty_days = moment().add(30, 'days').format('YYYY-MM-DD');
|
||||
/* User with valid membership */
|
||||
if (user && user.is_member) {
|
||||
/* ...which is lifelong cannot renew */
|
||||
if (user.last_membership.membership_type == 'lifelong') {
|
||||
_dom.membershipSubmitButton.prop('disabled', true);
|
||||
}
|
||||
/* ...or expiring soon and the user has a card */
|
||||
//else if (user.last_membership.end_date <= today_plus_thirty_days && user.active_member_card) {
|
||||
/* ...with a membership that expires soon can renew */
|
||||
else if (user.last_membership.end_date <= today_plus_thirty_days) {
|
||||
_dom.membershipSubmitButton.prop('disabled', false);
|
||||
}
|
||||
}
|
||||
/* User with expired membership can renew */
|
||||
// else if (user && !user.is_member && user.active_member_card) {
|
||||
else if (user && !user.is_member) {
|
||||
_dom.membershipSubmitButton.prop('disabled', false);
|
||||
}
|
||||
/* Order */
|
||||
else if (order) {
|
||||
/* If the membership has expired it can be renewed */
|
||||
if (!order.product.is_valid && order.product.end_date <= today_plus_thirty_days) {
|
||||
_dom.membershipSubmitButton.prop('disabled', false);
|
||||
}
|
||||
}
|
||||
else {
|
||||
_dom.membershipSubmitButton.prop('disabled', true);
|
||||
}
|
||||
}
|
||||
function validatePhoneNumber(val) {
|
||||
if(val.length === 0) {
|
||||
cardForm.fields.phoneNumber = false;
|
||||
update_submit_button();
|
||||
return 'Phone number should not be empty.';
|
||||
}
|
||||
cardForm.fields.phoneNumber = true;
|
||||
update_submit_button();
|
||||
return '';
|
||||
}
|
||||
|
||||
function validateCardNumber(val) {
|
||||
if(val.length === 0) {
|
||||
cardForm.fields.cardNumber = false;
|
||||
update_submit_button();
|
||||
return 'Card number should not be empty.';
|
||||
}
|
||||
if(val.length > 0 && val[0] != "1") {
|
||||
cardForm.fields.cardNumber = false;
|
||||
update_submit_button();
|
||||
return 'Card number should start with 1.';
|
||||
}
|
||||
if(val.length !== 9) {
|
||||
cardForm.fields.cardNumber = false;
|
||||
update_submit_button();
|
||||
return 'Card number should be 9 digits long.';
|
||||
}
|
||||
cardForm.fields.cardNumber = true;
|
||||
update_submit_button();
|
||||
return '';
|
||||
}
|
||||
|
||||
function getFormData(formElement) {
|
||||
var formData = formElement.serializeArray();
|
||||
formData = _.object(_.map(formData, function (x) {
|
||||
return [x.name, x.value || null];
|
||||
}));
|
||||
/* no need for radio button state */
|
||||
delete formData.user;
|
||||
|
||||
return formData;
|
||||
}
|
||||
|
||||
/* Phone number as you type */
|
||||
function checkPhoneNumber() {
|
||||
var val = _dom.phoneNumberField.val().trim();
|
||||
$.getJSON(urls.checkPhoneNumber, {phone_number: val}, function(data) {
|
||||
/* Invalid phone number?*/
|
||||
if(data.error) {
|
||||
set_selected_user(null, true);
|
||||
set_selected_order(null);
|
||||
set_field_state(_dom.phoneNumberField, 'error', data.error);
|
||||
cardForm.fields.phoneNumber = false;
|
||||
update_submit_button();
|
||||
return;
|
||||
}
|
||||
|
||||
var user = data.user;
|
||||
var order = data.order;
|
||||
var message = '';
|
||||
|
||||
/* Has valid card membership */
|
||||
if (!user && order && order.member_card && order.product.is_valid) {
|
||||
message = 'Phone number is already tied to card '+ order.member_card + ' and is valid until '+ order.product.end_date +'.';
|
||||
set_field_state(_dom.phoneNumberField, 'error', message);
|
||||
cardForm.fields.phoneNumber = false;
|
||||
set_selected_order(order);
|
||||
update_submit_button();
|
||||
return;
|
||||
}
|
||||
|
||||
if (user) {
|
||||
/* Existing user */
|
||||
message = 'Phone number belongs to existing user.';
|
||||
set_field_state(_dom.phoneNumberField, 'success', message);
|
||||
set_selected_user(user, true);
|
||||
set_selected_order(null);
|
||||
cardForm.fields.phoneNumber = true;
|
||||
update_submit_button();
|
||||
} else if (order) {
|
||||
set_selected_order(order);
|
||||
/* Valid membership, no card associated */
|
||||
if (order.product.is_valid) {
|
||||
message = 'Phone number has a valid membership.';
|
||||
} else {
|
||||
message = 'Phone number has an expired membership.';
|
||||
}
|
||||
} else {
|
||||
/* Valid phone number associated user or order */
|
||||
set_selected_order(null);
|
||||
set_selected_user(null);
|
||||
}
|
||||
|
||||
set_field_state(_dom.phoneNumberField, 'success', message);
|
||||
cardForm.fields.phoneNumber = true;
|
||||
update_submit_button();
|
||||
});
|
||||
}
|
||||
|
||||
/* Init and global vars */
|
||||
var _dom;
|
||||
var users;
|
||||
var searching; /* search ajax request */
|
||||
var selectedUser = null;
|
||||
var membershipOrder = null; /* selected order object */
|
||||
var cardForm = {
|
||||
fields: {
|
||||
cardNumber: false,
|
||||
phoneNumber: false
|
||||
}
|
||||
};
|
||||
var state_to_icon = {
|
||||
success: 'ok',
|
||||
warning: 'warning-sign',
|
||||
error: 'remove'
|
||||
};
|
||||
var nunjucksEnv = new nunjucks.Environment();
|
||||
/* Add filter |phoneNumber */
|
||||
nunjucksEnv.addFilter('phoneNumber', format_phone_number);
|
||||
|
||||
var urls = {
|
||||
userSearch: '/user-search/',
|
||||
checkPhoneNumber: '/check-phone-number/',
|
||||
checkCard: '/check-card/',
|
||||
registerCardAndMembership: '/register-card-membership/',
|
||||
renewMembership: '/renew-membership/'
|
||||
};
|
||||
|
||||
|
||||
|
||||
$(document).ready(function(){
|
||||
_dom = {
|
||||
results: $('.results-wrap'),
|
||||
query: $('#id_query'),
|
||||
phoneNumberLabel: $('[for=id_phone_number]'),
|
||||
phoneNumberField: $('#id_phone_number'),
|
||||
cardNumberField: $('#id_card_number'),
|
||||
registerCardForm: $('.register-card-form'),
|
||||
registerSubmitButton: $('#register-submit-btn'),
|
||||
usernameField: $('#id_username'),
|
||||
userIdField: $('#id_user_id'),
|
||||
orderUuidField: $('#id_order_uuid'),
|
||||
selectedEntityWrap: $('.register-card-form--selected-entity-wrap'),
|
||||
toastWrap: $('.toast-wrap'),
|
||||
registerResetButton: $('.register-reset-btn'),
|
||||
searchResetButton: $('.search-reset-btn'),
|
||||
membershipSubmitButton: $('#membership-submit-btn'),
|
||||
membershipTrialCheckBox: $('#id_membership_trial')
|
||||
};
|
||||
|
||||
var lazyCheckPhoneNumber = _.debounce(checkPhoneNumber, 250);
|
||||
_dom.phoneNumberField.on('input', function() {
|
||||
var val = _dom.phoneNumberField.val().trim();
|
||||
var validation_msg = validatePhoneNumber(val);
|
||||
if( validation_msg !== '') {
|
||||
set_selected_user(null, true);
|
||||
set_field_state(_dom.phoneNumberField, 'error', validation_msg);
|
||||
return;
|
||||
}
|
||||
lazyCheckPhoneNumber();
|
||||
});
|
||||
/* Card number as you type */
|
||||
_dom.cardNumberField.on('input', function() {
|
||||
/* Card number should exist in the database and not tied to existing user */
|
||||
var val = _dom.cardNumberField.val().trim();
|
||||
var validation_msg = validateCardNumber(val);
|
||||
if (validation_msg !== '') {
|
||||
set_field_state(_dom.cardNumberField, 'error', validation_msg);
|
||||
return;
|
||||
}
|
||||
$.getJSON(urls.checkCard, {card_number: val}, function(data) {
|
||||
if (data.error) {
|
||||
update_submit_button();
|
||||
cardForm.fields.cardNumber = false;
|
||||
set_field_state(_dom.cardNumberField, 'error', data.error);
|
||||
return;
|
||||
}
|
||||
|
||||
var card = data;
|
||||
var user = data.user;
|
||||
var order = data.order;
|
||||
var today_plus_thirty_days = moment().add(30, 'days');
|
||||
|
||||
if (!card) {
|
||||
cardForm.fields.cardNumber = false;
|
||||
set_field_state(_dom.cardNumberField, 'error',
|
||||
'Cannot find card number in database.');
|
||||
}
|
||||
else if (card && card.registered !== null && user === null && order && !order.product.is_valid) {
|
||||
/* An already registered card (with no user) can repurchase membership if expired */
|
||||
set_selected_order(order);
|
||||
cardForm.fields.cardNumber = true;
|
||||
set_field_state(_dom.cardNumberField, 'success',
|
||||
'Card number is in use (not activated) and belongs to ' + format_phone_number(order.phone_number) + '.');
|
||||
}
|
||||
else if (card && card.registered !== null) {
|
||||
var owner_string = '???';
|
||||
if (user) {
|
||||
owner_string = 'existing user: ' + user.first_name + ' ' + user.last_name + ' (' + format_phone_number(user.phone_number) + ')';
|
||||
} else {
|
||||
set_selected_order(order);
|
||||
owner_string = 'phone number: '+ format_phone_number(order.phone_number);
|
||||
}
|
||||
cardForm.fields.cardNumber = false;
|
||||
set_field_state(_dom.cardNumberField, 'error',
|
||||
'Card number is in use and belongs to '+ owner_string +'.');
|
||||
}
|
||||
else if (card && user === null) {
|
||||
cardForm.fields.cardNumber = true;
|
||||
set_field_state(_dom.cardNumberField, 'success');
|
||||
} else {
|
||||
cardForm.fields.cardNumber = true;
|
||||
set_field_state(_dom.cardNumberField, '');
|
||||
}
|
||||
update_submit_button();
|
||||
}).fail(function(data) {
|
||||
cardForm.fields.cardNumber = false;
|
||||
set_field_state(_dom.cardNumberField, 'error',
|
||||
'Cannot find card number in database.');
|
||||
});
|
||||
});
|
||||
|
||||
/* User search as you type */
|
||||
_dom.query.on('input', function() {
|
||||
var val = _dom.query.val().trim();
|
||||
if(val.length <= 2) {
|
||||
_dom.results.html('');
|
||||
return;
|
||||
}
|
||||
if (searching) {
|
||||
searching.abort();
|
||||
}
|
||||
searching = $.getJSON(urls.userSearch, {search: val}, function(data) {
|
||||
if (data.results && data.results.length > 0) {
|
||||
sorted_results = sort_search_results(data.results);
|
||||
_dom.results.html(format_results(sorted_results));
|
||||
users = data.results;
|
||||
}
|
||||
else if (data.error) {
|
||||
_dom.results.html(data.error);
|
||||
}
|
||||
else {
|
||||
if(val !== "") {
|
||||
_dom.results.html("Found no user matching: '"+ val +"'");
|
||||
} else {
|
||||
_dom.results.html('');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/* On search result click */
|
||||
_dom.results.on('click', '.search-result input', function(e){
|
||||
var label = $(this).parent();
|
||||
var number = label.attr('data-phone-number');
|
||||
var user_id = label.attr('data-user-id');
|
||||
|
||||
/* Update selected state */
|
||||
$('.search-result').removeClass('selected');
|
||||
label.toggleClass('selected');
|
||||
|
||||
set_selected_user(_.findWhere(users, {id: user_id}));
|
||||
|
||||
/* Update phone number field */
|
||||
if (number === '-') {
|
||||
number = '';
|
||||
}
|
||||
_dom.phoneNumberField.val(number).trigger('input');
|
||||
});
|
||||
/* On submit button click */
|
||||
_dom.registerSubmitButton.on('click', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if( !cardFormIsValid() ) {
|
||||
set_toast('Either phone number or card number is not valid', 'error');
|
||||
return;
|
||||
}
|
||||
var payload = getFormData(_dom.registerCardForm);
|
||||
|
||||
/* Register type */
|
||||
// FIXME: get from form
|
||||
// action: 'new_card_membership', 'update_card', 'add_or_renew', 'sms_card_notify'
|
||||
if (selectedUser === null && membershipOrder === null) {
|
||||
// Unknown phone / card, new membership
|
||||
payload.action = 'new_card_membership';
|
||||
} else if (selectedUser === null && membershipOrder !== null) {
|
||||
if (membershipOrder.member_card === null && membershipOrder.product.is_valid) {
|
||||
// Associate member card with a valid membership order
|
||||
payload.action = 'sms_card_notify';
|
||||
payload.order_uuid = membershipOrder.uuid;
|
||||
} else {
|
||||
// New card for expired order -- treat as new card membership
|
||||
payload.action = 'new_card_membership';
|
||||
}
|
||||
} else if (selectedUser !== null && selectedUser.is_member) {
|
||||
// Add member card to a user
|
||||
payload.action = 'update_card';
|
||||
} else {
|
||||
// New order for user or non-user
|
||||
payload.action = 'add_or_renew'; // Note: could also update card number
|
||||
}
|
||||
$.ajax(urls.registerCardAndMembership, {
|
||||
data: JSON.stringify(payload),
|
||||
contentType: 'application/json',
|
||||
dataType: 'json',
|
||||
type: 'post',
|
||||
headers: {'X-CSRFToken': getCookie('csrftoken')}
|
||||
}).done(function(data) {
|
||||
var success_message = 'New card registered ' + payload.card_number;
|
||||
if (payload.action === 'add_or_renew') {
|
||||
var full_name = selectedUser.first_name + ' ' + selectedUser.last_name;
|
||||
success_message = 'New membership and card number '+ payload.card_number + ' registered to ' + full_name + ' ';
|
||||
}
|
||||
else if (payload.action ==='new_card_membership') {
|
||||
success_message = 'New membership and card registered to ' + format_phone_number(payload.phone_number) + '. Activation SMS sent ';
|
||||
}
|
||||
else if (payload.action === 'sms_card_notify') {
|
||||
success_message = 'New card registered to ' + format_phone_number(payload.phone_number) + '. Membership already paid '+ membershipOrder.product.start_date +'. Activation SMS sent ';
|
||||
}
|
||||
set_toast(success_message + ' :-)', 'success');
|
||||
resetCardForm(true);
|
||||
|
||||
}).fail(function(data) {
|
||||
console.log("failed", data);
|
||||
var error_text = data.responseText;
|
||||
if (data.responseJSON) {
|
||||
if (data.responseJSON.non_field_errors) {
|
||||
error_text = data.responseJSON.non_field_errors.join('<br>');
|
||||
} else {
|
||||
error_text = 'Not sure why. :-(';
|
||||
}
|
||||
}
|
||||
set_toast('Failed! '+ error_text, 'error');
|
||||
});
|
||||
});
|
||||
|
||||
/* Membership renewal button */
|
||||
_dom.membershipSubmitButton.on('click', function(e) {
|
||||
e.preventDefault();
|
||||
if (!selectedUser && !membershipOrder) {
|
||||
set_toast('Computer says no.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
var payload = getFormData(_dom.registerCardForm);
|
||||
|
||||
/* If order, make sure to bring along both card number and phone number */
|
||||
if (membershipOrder) {
|
||||
payload.card_number = membershipOrder.member_card;
|
||||
payload.phone_number = membershipOrder.phone_number;
|
||||
}
|
||||
|
||||
$.ajax(urls.renewMembership, {
|
||||
data: JSON.stringify(payload),
|
||||
contentType: 'application/json',
|
||||
dataType: 'json',
|
||||
type: 'post',
|
||||
headers: {'X-CSRFToken': getCookie('csrftoken')}
|
||||
}).done(function(data) {
|
||||
var name = 'unknown';
|
||||
if (data.user) {
|
||||
name = selectedUser.first_name + ' ' + selectedUser.last_name;
|
||||
} else {
|
||||
name = 'phone number ' + data.phone_number;
|
||||
}
|
||||
var success_message = 'Membership renewed for ' + name + ' :-)';
|
||||
set_toast(success_message, 'success');
|
||||
resetCardForm(true);
|
||||
}).fail(function(data) {
|
||||
console.log("failed", data);
|
||||
var error_text = data.responseText;
|
||||
if (data.responseJSON) {
|
||||
if (data.responseJSON.non_field_errors) {
|
||||
error_text = data.responseJSON.non_field_errors.join('<br>');
|
||||
} else {
|
||||
error_text = 'Not sure why. :-(';
|
||||
}
|
||||
}
|
||||
set_toast('Failed! '+ error_text, 'error');
|
||||
});
|
||||
});
|
||||
|
||||
/* On user id change */
|
||||
_dom.userIdField.on('change', function() {
|
||||
/* Render selected user in search form */
|
||||
render_selected_user(selectedUser);
|
||||
});
|
||||
/* On order uuid change */
|
||||
_dom.orderUuidField.on('change', function() {
|
||||
/* Render selected order */
|
||||
render_selected_order(membershipOrder);
|
||||
});
|
||||
/* Reset buttons */
|
||||
_dom.registerResetButton.on('click', function() {
|
||||
resetCardForm();
|
||||
});
|
||||
_dom.searchResetButton.on('click', function() {
|
||||
resetSearchForm();
|
||||
});
|
||||
|
||||
/* Ninja add some icons to bootstrap form */
|
||||
var iconMobile = '<span class="input-group-addon"><span class="glyphicon glyphicon-phone glyphicon-phone-large"></span></span>';
|
||||
_dom.phoneNumberField.parent().addClass('input-group');
|
||||
_dom.phoneNumberField.before(iconMobile);
|
||||
|
||||
/* Render initial placeholder user */
|
||||
var selected_user_html = nunjucksEnv.render('search_result.html', {placeholder: true});
|
||||
_dom.selectedEntityWrap.html(selected_user_html);
|
||||
|
||||
/* Initial focus */
|
||||
if(_dom.usernameField) {
|
||||
_dom.usernameField.focus();
|
||||
}
|
||||
|
||||
/* Load query params */
|
||||
if( getParameterByName('user_id') ) {
|
||||
_dom.userIdField.val(getParameterByName('user_id')).trigger('change');
|
||||
}
|
||||
if( getParameterByName('phone_number') ) {
|
||||
_dom.phoneNumberField.val(getParameterByName('phone_number')).trigger('input');
|
||||
}
|
||||
if( getParameterByName('card_number') ) {
|
||||
_dom.cardNumberField.val(getParameterByName('card_number')).trigger('input');
|
||||
}
|
||||
|
||||
$('[data-toggle="popover"]').popover({
|
||||
html: true,
|
||||
trigger: 'click'
|
||||
});
|
||||
});
|
||||
Vendored
-20406
File diff suppressed because it is too large.
Load diff
Vendored
-10
@@ -1,10 +0,0 @@
|
||||
@charset "UTF-8";
|
||||
/*!
|
||||
* Bootstrap v3.3.7 (http://getbootstrap.com)
|
||||
* Copyright 2011-2016 Twitter, Inc.
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
|
||||
*/
|
||||
/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */
|
||||
html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{font-size:2em;margin:.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-appearance:textfield;box-sizing:content-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{border:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-collapse:collapse;border-spacing:0}legend,td,th{padding:0}
|
||||
/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */
|
||||
@media print{*,:after,:before{background:0 0!important;color:#000!important;box-shadow:none!important;text-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="#"]:after,a[href^="javascript:"]:after{content:""}blockquote,pre{border:1px solid #999}thead{display:table-header-group}blockquote,img,pre,tr{page-break-inside:avoid}img{max-width:100%!important}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}.navbar{display:none}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000!important}.label{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #ddd!important}}@font-face{font-family:'Glyphicons Halflings';src:url(../fonts/glyphicons-halflings-regular.eot);src:url(../fonts/glyphicons-halflings-regular.eot?#iefix) format("embedded-opentype"),url(../fonts/glyphicons-halflings-regular.woff2) format("woff2"),url(../fonts/glyphicons-halflings-regular.woff) format("woff"),url(../fonts/glyphicons-halflings-regular.ttf) format("truetype"),url(../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular) format("svg")}.glyphicon{position:relative;top:1px;display:inline-block;font-family:'Glyphicons Halflings';font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.glyphicon-asterisk:before{content:"\002a"}.glyphicon-plus:before{content:"\002b"}.glyphicon-eur:before,.glyphicon-euro:before{content:"\20ac"}.glyphicon-minus:before{content:"\2212"}.glyphicon-cloud:before{content:"\2601"}.glyphicon-envelope:before{content:"\2709"}.glyphicon-pencil:before{content:"\270f"}.glyphicon-glass:before{content:"\e001"}.glyphicon-music:before{content:"\e002"}.glyphicon-search:before{content:"\e003"}.glyphicon-heart:before{content:"\e005"}.glyphicon-star:before{content:"\e006"}.glyphicon-star-empty:before{content:"\e007"}.glyphicon-user:before{content:"\e008"}.glyphicon-film:before{content:"\e009"}.glyphicon-th-large:before{content:"\e010"}.glyphicon-th:before{content:"\e011"}.glyphicon-th-list:before{content:"\e012"}.glyphicon-ok:before{content:"\e013"}.glyphicon-remove:before{content:"\e014"}.glyphicon-zoom-in:before{content:"\e015"}.glyphicon-zoom-out:before{content:"\e016"}.glyphicon-off:before{content:"\e017"}.glyphicon-signal:before{content:"\e018"}.glyphicon-cog:before{content:"\e019"}.glyphicon-trash:before{content:"\e020"}.glyphicon-home:before{content:"\e021"}.glyphicon-file:before{content:"\e022"}.glyphicon-time:before{content:"\e023"}.glyphicon-road:before{content:"\e024"}.glyphicon-download-alt:before{content:"\e025"}.glyphicon-download:before{content:"\e026"}.glyphicon-upload:before{content:"\e027"}.glyphicon-inbox:before{content:"\e028"}.glyphicon-play-circle:before{content:"\e029"}.glyphicon-repeat:before{content:"\e030"}.glyphicon-refresh:before{content:"\e031"}.glyphicon-list-alt:before{content:"\e032"}.glyphicon-lock:before{content:"\e033"}.glyphicon-flag:before{content:"\e034"}.glyphicon-headphones:before{content:"\e035"}.glyphicon-volume-off:before{content:"\e036"}.glyphicon-volume-down:before{content:"\e037"}.glyphicon-volume-up:before{content:"\e038"}.glyphicon-qrcode:before{content:"\e039"}.glyphicon-barcode:before{content:"\e040"}.glyphicon-tag:before{content:"\e041"}.glyphicon-tags:before{content:"\e042"}.glyphicon-book:before{content:"\e043"}.glyphicon-bookmark:before{content:"\e044"}.glyphicon-print:before{content:"\e045"}.glyphicon-camera:before{content:"\e046"}.glyphicon-font:before{content:"\e047"}.glyphicon-bold:before{content:"\e048"}.glyphicon-italic:before{content:"\e049"}.glyphicon-text-height:before{content:"\e050"}.glyphicon-text-width:before{content:"\e051"}.glyphicon-align-left:before{content:"\e052"}.glyphicon-align-center:before{content:"\e053"}.glyphicon-align-right:before{content:"\e054"}.glyphicon-align-justify:before{content:"\e055"}.glyphicon-list:before{content:"\e056"}.glyphicon-indent-left:before{content:"\e057"}.glyphicon-indent-right:before{content:"\e058"}.glyphicon-facetime-video:before{content:"\e059"}.glyphicon-picture:before{content:"\e060"}.glyphicon-map-marker:before{content:"\e062"}.glyphicon-adjust:before{content:"\e063"}.glyphicon-tint:before{content:"\e064"}.glyphicon-edit:before{content:"\e065"}.glyphicon-share:before{content:"\e066"}.glyphicon-check:before{content:"\e067"}.glyphicon-move:before{content:"\e068"}.glyphicon-step-backward:before{content:"\e069"}.glyphicon-fast-backward:before{content:"\e070"}.glyphicon-backward:before{content:"\e071"}.glyphicon-play:before{content:"\e072"}.glyphicon-pause:before{content:"\e073"}.glyphicon-stop:before{content:"\e074"}.glyphicon-forward:before{content:"\e075"}.glyphicon-fast-forward:before{content:"\e076"}.glyphicon-step-forward:before{content:"\e077"}.glyphicon-eject:before{content:"\e078"}.glyphicon-chevron-left:before{content:"\Line truncated
|
||||
Generated
+10903
File diff suppressed because it is too large.
Load diff
+40
-25
@@ -1,35 +1,50 @@
|
||||
{
|
||||
"name": "kassa",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "webpack -p --env production --display-error-details --mode production",
|
||||
"build:templates": "echo '/* eslint-disable */' > app/templates.js && nunjucks-precompile app/templates >> app/templates.js",
|
||||
"start": "webpack-dev-server --hot --env development --mode development",
|
||||
"format": "eslint --fix \"app/**/*.js\"",
|
||||
"lint": "eslint --quiet \"app/**/*.js\""
|
||||
},
|
||||
"dependencies": {
|
||||
"jshint": "^2.9.5"
|
||||
"bootstrap-sass": "^3.4.1",
|
||||
"jquery": "^3.4.1",
|
||||
"list.js": "^1.5.0",
|
||||
"lodash": "^4.17.15",
|
||||
"moment": "^2.24.0",
|
||||
"nunjucks": "^3.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"browser-sync": "^2.7.6",
|
||||
"del": "^3.0.0",
|
||||
"gulp": "^3.8.11",
|
||||
"gulp-autoprefixer": "^4.0.0",
|
||||
"gulp-concat": "~2.6.1",
|
||||
"gulp-csso": "^3.0.0",
|
||||
"gulp-filter": "^5.0.0",
|
||||
"gulp-flatten": "^0.3.1",
|
||||
"gulp-imagemin": "^3.3.0",
|
||||
"gulp-jshint": "^2.0.4",
|
||||
"gulp-load-plugins": "^1.5.0",
|
||||
"gulp-nunjucks": "^3.0.0",
|
||||
"gulp-plumber": "~1.1.0",
|
||||
"gulp-rename": "~1.2.2",
|
||||
"gulp-sass": "~3.1.0",
|
||||
"gulp-size": "^2.1.0",
|
||||
"gulp-uglify": "^3.0.0",
|
||||
"gulp-useref": "^3.1.2",
|
||||
"gulp-util": "~3.0.4",
|
||||
"jshint-stylish": "^2.0.0",
|
||||
"main-bower-files": "^2.8.0",
|
||||
"opn": "^5.1.0"
|
||||
"@babel/core": "^7.7.7",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.7.4",
|
||||
"@babel/plugin-transform-runtime": "^7.7.6",
|
||||
"@babel/preset-env": "^7.7.7",
|
||||
"@babel/preset-react": "^7.7.4",
|
||||
"babel-eslint": "^10.0.3",
|
||||
"babel-loader": "^8.0.6",
|
||||
"clean-webpack-plugin": "^3.0.0",
|
||||
"css-loader": "^3.4.0",
|
||||
"eslint": "^6.8.0",
|
||||
"eslint-config-airbnb-base": "^14.0.0",
|
||||
"eslint-config-prettier": "^6.7.0",
|
||||
"eslint-import-resolver-babel-module": "^5.1.0",
|
||||
"eslint-plugin-import": "^2.19.1",
|
||||
"eslint-plugin-prettier": "^3.1.2",
|
||||
"file-loader": "^5.0.2",
|
||||
"html-webpack-plugin": "^3.2.0",
|
||||
"mini-css-extract-plugin": "^0.9.0",
|
||||
"node-sass": "^4.13.0",
|
||||
"prettier": "^1.19.1",
|
||||
"sass-loader": "^8.0.0",
|
||||
"style-loader": "^1.1.2",
|
||||
"url-loader": "^3.0.0",
|
||||
"webpack": "^4.41.5",
|
||||
"webpack-cli": "^3.3.10",
|
||||
"webpack-dev-server": "^3.10.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
"node": "12"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
const path = require('path');
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||
const webpack = require('webpack');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
|
||||
const baseStyleLoader = (env) => (env === 'production' ? MiniCssExtractPlugin.loader : require.resolve('style-loader'));
|
||||
|
||||
module.exports = (env, argv) => {
|
||||
return {
|
||||
entry: './app/index.js',
|
||||
output: {
|
||||
filename: '[name].js',
|
||||
path: path.resolve(__dirname, 'dist')
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.jsx', '.js', '.json', '.scss', '.css'],
|
||||
modules: ['node_modules']
|
||||
},
|
||||
plugins: [
|
||||
new MiniCssExtractPlugin(),
|
||||
env === 'production' ? new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/) : null,
|
||||
env !== 'production' ? new HtmlWebpackPlugin({ template: './app/index.html' }) : null,
|
||||
new webpack.ProvidePlugin({
|
||||
jQuery: 'jquery'
|
||||
})
|
||||
].filter(Boolean),
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.(png|jpg|gif|svg)$/,
|
||||
use: [
|
||||
{
|
||||
loader: require.resolve('file-loader'),
|
||||
options: {}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /\.(woff|woff2|eot|ttf|otf)$/,
|
||||
use: require.resolve('file-loader')
|
||||
},
|
||||
{
|
||||
test: /\.(js|jsx)$/,
|
||||
exclude: /node_modules/,
|
||||
loader: require.resolve('babel-loader')
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
loaders: [baseStyleLoader(env), require.resolve('css-loader')]
|
||||
},
|
||||
{
|
||||
test: /\.scss$/,
|
||||
loaders: [
|
||||
baseStyleLoader(env),
|
||||
{
|
||||
loader: require.resolve('css-loader'),
|
||||
options: {
|
||||
importLoaders: 1,
|
||||
modules: false,
|
||||
sourceMap: true
|
||||
}
|
||||
},
|
||||
{
|
||||
loader: require.resolve('sass-loader'),
|
||||
options: {
|
||||
sassOptions: {
|
||||
precision: 8
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
devServer: {
|
||||
contentBase: path.resolve(__dirname, 'dist'),
|
||||
historyApiFallback: true,
|
||||
overlay: {
|
||||
errors: true,
|
||||
warnings: false
|
||||
},
|
||||
port: 8001,
|
||||
publicPath: '/',
|
||||
proxy: {
|
||||
'/api': 'http://localhost:8000'
|
||||
// TODO: get urls
|
||||
},
|
||||
stats: {
|
||||
assets: false,
|
||||
modules: false,
|
||||
chunks: false
|
||||
}
|
||||
},
|
||||
devtool: env === 'production' ? 'source-map' : 'cheap-module-eval-source-map'
|
||||
};
|
||||
};
|
||||
-4546
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user