Переглянути джерело

lead local storage feature added

robert2206 8 роки тому
батько
коміт
f5416645ba

+ 5 - 4
www/index.html

@@ -34,10 +34,11 @@
     <!-- Factories -->
     <script src="js/factories/utils.factory.js"></script>
     <script src="js/factories/odoo.factory.js"></script>
-    <script src="js/factories/storage.user.factory.js"></script>
-    <script src="js/factories/sales/storage.customer.factory.js"></script>
-    <script src="js/factories/sales/sync.customer.factory.js"></script>
-    <script src="js/factories/sales/sync.lead.factory.js"></script>
+    <script src="js/factories/user.storage.factory.js"></script>
+    <script src="js/factories/sales/customer.storage.factory.js"></script>
+    <script src="js/factories/sales/customer.sync.factory.js"></script>
+    <script src="js/factories/sales/lead.storage.factory.js"></script>
+    <script src="js/factories/sales/lead.sync.factory.js"></script>
 
     <!-- 3rd Party -->
     <script src="lib/angular-xmlrpc/xmlrpc.js"></script>

+ 2 - 2
www/js/controllers/configuration.controller.js

@@ -6,7 +6,7 @@ angular.module('odoo')
     $ionicLoading,
     $ionicPopup,
     odoo,
-    sql,
+    sqlFactory,
     userStorage
 ) {
     $scope.config = { host: '192.168.2.103', port: 8069, database: 'odoo', username: 'admin', password: 'admin' }
@@ -15,7 +15,7 @@ angular.module('odoo')
     $scope.configure = function () {
         $ionicLoading.show();
 
-        sql.count('user', function (total) {
+        sqlFactory.count('user', function (total) {
             if (total == 0) {
                 odoo.auth($scope.config, function (user) {
                     userStorage.save(

+ 2 - 3
www/js/controllers/customer.controller.js

@@ -8,7 +8,7 @@ angular.module('odoo')
     $ionicPopup,
     customers,
     odoo,
-    sql,
+    sqlFactory,
     customerStorage,
     camera,
     contact
@@ -62,8 +62,7 @@ angular.module('odoo')
 
     // Get filtered customers for correct data visualization
     $scope.getCustomers = function (success, error) {
-        sql.selectByConstraint('partner', 'customer = 1 AND modified != 2', function (customers) {
-            console.log(customers);
+        sqlFactory.selectByConstraint('partner', 'customer = 1 AND modified != 2', function (customers) {
             $scope.customers = [];
 
             for (var i = 0; i < customers.length; i++) {

+ 48 - 7
www/js/controllers/lead.controller.js

@@ -6,7 +6,9 @@ angular.module('odoo')
     $ionicActionSheet,
     $ionicFilterBar,
     $ionicPopup,
-    leads
+    leadsRemoteFactory,
+    leadsStorageFactory,
+    sqlFactory
 ) {
     $scope.leads = [];
     $scope.lead = {};
@@ -32,11 +34,27 @@ angular.module('odoo')
     });
 
     $scope.fill = function () {
-        leads.read(function (leads) {
-            console.log(leads);
-            $scope.leads = leads;
+        leadsRemoteFactory.sync(function () {
+            console.log('ok');
         }, function (error) {
-            console.log(error);
+            console.log('error');
+        });
+    }
+
+    $scope.getLeads = function (success, error) {
+        sqlFactory.selectByConstraint('lead', "type = 'lead' AND modified != 2", function (leads) {
+            $scope.leads = [];
+
+            for (var i = 0; i < leads.length; i++) {
+                $scope.leads.push(leads.item(i));
+            }
+
+            console.log($scope.leads);
+
+            $scope.$apply();
+            success();
+        }, function (sqlFactoryError) {
+            error(sqlFactoryError);
         });
     }
 
@@ -45,7 +63,19 @@ angular.module('odoo')
     }
 
     $scope.save = function () {
+        leadsStorageFactory.save($scope.lead, function (leadId) {
+            if (!$scope.lead.id) {
+                $scope.lead.id = leadId;
+                $scope.leads.push($scope.lead);
+            }
 
+            $scope.lead = {};
+            $scope.leadModal.hide();
+            console.log('Lead saved');
+        }, function (error) {
+            $ionicPopup.alert({ title: 'No se ha podido guardar la iniciativa', template: JSON.stringify(error) });
+            console.log(JSON.stringify(error));
+        });
     }
 
     $scope.delete = function () {
@@ -54,7 +84,18 @@ angular.module('odoo')
             template: 'Estás seguro que quieres eliminar esta iniciativa?'
         }).then(function (confirmation) {
             if(confirmation) {
-                console.log(confirmation);
+                leadsStorageFactory.remove($scope.lead, function (affected) {
+                    if (affected != 0) {
+                        var index = $scope.leads.indexOf($scope.customer);
+                        $scope.leads.splice(index, 1);
+                        $scope.lead = {};
+
+                        $scope.$apply();
+                    }
+                }, function (error) {
+                    $ionicPopup.alert({ title: 'No se puedo eliminar la iniciativa', template: JSON.stringify(error) });
+                    console.log(JSON.stringify(error));
+                });
             }
         });
     }
@@ -88,7 +129,7 @@ angular.module('odoo')
             destructiveText: '<i class="icon ion-trash-a assertive"></i> Eliminar',
             cancel: function() {
                 $scope.lead = {};
-                $log.info('ActionSheet canceled');
+                console.log('ActionSheet canceled');
             },
             buttonClicked: function(index) {
                 switch (index) {

+ 13 - 0
www/js/directives/onerrorsrc.directive.js

@@ -0,0 +1,13 @@
+angular.module('odoo')
+
+.directive('onErrorSrc', function() {
+    return {
+        link: function(scope, element, attrs) {
+            element.bind('error', function() {
+                if (attrs.src != attrs.onErrorSrc) {
+                    attrs.$set('src', attrs.onErrorSrc);
+                }
+            });
+        }
+    }
+})

+ 66 - 0
www/js/factories/sales/customer.storage.factory.js

@@ -0,0 +1,66 @@
+angular.module('odoo')
+
+/**
+ * -----------------------------------------------------------------------------
+ *  Description:    Local storage manager for customers data
+ * -----------------------------------------------------------------------------
+ */
+.factory('customerStorage', function () {
+    
+    // Save customer data to local storage
+    var save = function (customer, success, error) {
+        var sql = '';
+
+        if (customer.id) {
+            sql = `UPDATE partner SET remote_id = ${ customer.remote_id ? customer.remote_id : 0  }, modified = 1, modifiedDate = CURRENT_TIMESTAMP, name = ${ customer.name ? '"' + customer.name + '"' : null }, city = ${ customer.city ? '"' + customer.city + '"' : null }, mobile = ${ customer.mobile ? '"' + customer.mobile + '"' : null }, phone = ${ customer.phone ? '"' + customer.phone + '"' : null }, fax = ${ customer.fax ? '"' + customer.fax + '"' : null }, email = ${ customer.email ? '"' + customer.email + '"' : null }, street = ${ customer.street ? '"' + customer.street + '"' : null }, street2 = ${ customer.street2 ? '"' + customer.street2 + '"' : null }, image_medium = ${ customer.image_medium ? '"' + customer.image_medium + '"' : null }, image_small = ${ customer.image_small ? '"' + customer.image_small + '"' : null }, comment = ${ customer.comment ? '"' + customer.comment + '"' : null }, customer = ${ customer.customer ? customer.customer : 1 }, employee = ${ customer.employee ? customer.employee : 0 }, is_company = ${ customer.is_company ? customer.is_company : 0 }, debit = ${ customer.debit ? customer.debit : 0 }, debit_limit = ${ customer.debit_limit ? customer.debit_limit : 0 }, opportunity_count = ${ customer.opportunity_count ? customer.opportunity_count : 0 }, contracts_count = ${ customer.contracts_count ? customer.contracts_count : 0 }, journal_item_count = ${ customer.journal_item_count ? customer.journal_item_count : 0 }, meeting_count = ${ customer.meeting_count ? customer.meeting_count : 0 }, phonecall_count = ${ customer.phonecall_count ? customer.phonecall_count : 0 }, sale_order_count = ${ customer.sale_order_count ? customer.sale_order_count : 0 }, total_invoiced = ${ customer.total_invoiced ? customer.total_invoiced : 0 } WHERE id = ${ customer.id }`;
+        } else {
+            sql = `INSERT INTO partner(remote_id, name, city, mobile, phone, fax, email, street, street2, image_medium, image_small, comment, customer, employee, is_company, debit, debit_limit, opportunity_count, contracts_count, journal_item_count, meeting_count, phonecall_count, sale_order_count, total_invoiced) VALUES (${ customer.remote_id ? customer.remote_id : 0 }, ${ customer.name ? '"' + customer.name + '"' : null }, ${ customer.city ? '"' + customer.city + '"' : null }, ${ customer.mobile ? '"' + customer.mobile + '"' : null }, ${ customer.phone ? '"' + customer.phone + '"' : null }, ${ customer.fax ? '"' + customer.fax + '"' : null }, ${ customer.email ? '"' + customer.email + '"' : null }, ${ customer.street ? '"' + customer.street + '"' : null }, ${ customer.street2 ? '"' + customer.street2 + '"' : null }, ${ customer.image_medium ? '"' + customer.image_medium + '"' : null }, ${ customer.image_small ? '"' + customer.image_small + '"' : null }, ${ customer.comment ? '"' + customer.comment + '"' : null }, 1, 0, 0, ${ customer.debit ? customer.debit : 0 }, ${ customer.debit_limit ? customer.debit_limit : 0 }, ${ customer.opportunity_count ? customer.opportunity_count : 0 }, ${ customer.contracts_count ? customer.contracts_count : 0 }, ${ customer.journal_item_count ? customer.journal_item_count : 0 }, ${ customer.meeting_count ? customer.meeting_count : 0 }, ${ customer.phonecall_count ? customer.phonecall_count : 0 }, ${ customer.sale_order_count ? customer.sale_order_count : 0 }, ${ customer.total_invoiced ? customer.total_invoiced : 0 })`;
+        }
+
+        db.executeSql(sql, [], function(result) {
+            success(sql.startsWith('INSERT') ? result.insertId : customer.id);
+        }, function(err) {
+            error(err);
+        });
+    };
+
+    // Delete customer from local storage
+    var remove = function (customer, success, error) {
+        if (!customer.id) {
+            error('Customer cannot delete without provide an id');
+        }
+
+        var sql = '';
+
+        if (customer.remote_id) {
+            sql = `UPDATE partner SET modified = 2 WHERE id = ${ customer.id }`;
+        } else {
+            sql = `DELETE FROM partner WHERE id = ${ customer.id }`;
+        }
+
+        console.log(sql);
+
+        db.executeSql(sql, [], function(result) {
+            success(result.rowsAffected);
+        }, function(err) {
+            error(err);
+        });
+    };
+
+    // Delete all customers from local storage
+    var removeAll = function (success, error) {
+        var sql = 'DELETE FROM partner WHERE customer = 1';
+
+        db.executeSql(sql, [], function(result) {
+            success(result.rowsAffected);
+        }, function(err) {
+            error(err);
+        });
+    };
+
+    return {
+        save: save,
+        remove: remove,
+        removeAll: removeAll
+    }
+});

+ 127 - 0
www/js/factories/sales/customer.sync.factory.js

@@ -0,0 +1,127 @@
+angular.module('odoo')
+
+.factory('customers', function (sqlFactory, customerStorage, asyncLoop, odoo) {
+    return {
+        sync: function (success, error) {
+            // Get current user saved on mobile
+            sqlFactory.select('user', function (users) {
+                if (users.length == 1) {
+                    var userConfig = users.item(0);
+
+                    // 1. Transfer all new data from mobile to server
+                    sqlFactory.selectByConstraint('partner', 'remote_id = 0 AND customer = 1', function(newPartners) {
+                        asyncLoop(newPartners.length, function (loop) {
+                            var data = newPartners.item(loop.iteration());
+
+                            // Avoid odoo server warning message
+                            delete data.id;
+                            delete data.remote_id;
+                            delete data.modified;
+                            delete data.modifiedDate;
+
+                            odoo.create('res.partner', data, userConfig, function (response) {
+                                loop.next();
+                            }, function (odooCreateError) {
+                                loop.break();
+                            });
+
+                        // End loop
+                        }, function() {
+                            // 2. Transfer all modified data from mobile to server
+                            sqlFactory.selectByConstraint('partner', 'remote_id != 0 AND customer = 1 AND modified = 1', function (modifiedPartners) {
+                                asyncLoop(modifiedPartners.length, function (loop) {
+                                    var localData = modifiedPartners.item(loop.iteration());
+
+                                    odoo.read('res.partner', [['id', '=', localData.remote_id]], userConfig, function (response) {
+                                        if (response.length == 1) {
+                                            var remoteData = response[0];
+
+                                            var remoteModifiedDate = new Date(remoteData.__last_update);
+                                            var localModifiedDate = new Date(localData.modifiedDate);
+
+                                            if (localModifiedDate > remoteModifiedDate) {
+                                                var id = localData.remote_id;
+
+                                                // Avoid odoo server warning message
+                                                delete localData.id;
+                                                delete localData.remote_id;
+                                                delete localData.modified;
+                                                delete localData.modifiedDate;
+
+                                                odoo.write('res.partner', id, localData, userConfig, function (response) {
+                                                    loop.next();
+                                                }, function (odooWriteError) {
+                                                    console.error(odooWriteError);
+                                                    loop.next();
+                                                });
+                                            } else {
+                                                loop.next();
+                                            }
+                                        } else {
+                                            loop.next();
+                                        }
+                                    }, function(odooReadError) {
+                                        console.error(odooReadError);
+                                        loop.next();
+                                    });
+
+                                // End loop
+                                }, function () {
+                                    // 3. Delete server data from mobile
+                                    sqlFactory.selectByConstraint('partner', 'remote_id != 0 AND customer = 1 AND modified = 2', function (deletedPartners) {
+                                        asyncLoop(deletedPartners.length, function (loop) {
+                                            var id = deletedPartners.item(loop.iteration()).remote_id;
+
+                                            odoo.unlink('res.partner', id, userConfig, function (response) {
+                                                loop.next();
+                                            }, function (odooUnlinkError) {
+                                                console.error(odooUnlinkError);
+                                                loop.next();
+                                            });
+
+                                        // End loop
+                                        }, function () {
+                                            // 4. Download updated data from server to mobile
+                                            odoo.read('res.partner', [['customer', '=', true]], userConfig, function (updatedPartners) {
+                                                customerStorage.removeAll(function () {
+                                                    asyncLoop(updatedPartners.length, function (loop) {
+                                                        var data = updatedPartners[loop.iteration()];
+
+                                                        // Set id for save on local database
+                                                        data.remote_id = data.id;
+                                                        delete data.id;
+
+                                                        customerStorage.save(data, function (customerId) {
+                                                            loop.next();
+                                                        } ,function (saveCustomerError) {
+                                                            console.error(saveCustomerError);
+                                                            loop.next();
+                                                        });
+                                                    }, function () {
+                                                        success(updatedPartners);
+                                                    });
+                                                }, function (deleteAllCustomersError) {
+                                                    error(deleteAllCustomersError);
+                                                });
+                                            }, function (odooReadError) {
+                                                error(odooReadError);
+                                            });
+                                        });
+                                    }, function (getDeletedPartnersError) {
+                                        error(getDeletedPartnersError);
+                                    });
+                                });
+                            }, function (getModifiedPartnersError) {
+                                error(getModifiedPartnersError);
+                            });
+                        });
+                    }, function (partnerGetByConstraintError) {
+                        error(partnerGetByConstraintError);
+                    });
+                }
+            }, function(userGetError) {
+                error(userGetError);
+            });
+        }
+    }
+});

+ 58 - 0
www/js/factories/sales/lead.storage.factory.js

@@ -0,0 +1,58 @@
+angular.module('odoo')
+
+.factory('leadsStorageFactory', function () {
+
+    // Save lead data to local storage
+    var save = function (lead, success, error) {
+        var values = [lead.remote_id ? lead.remote_id : 0, 1, lead.name ? lead.name : '', lead.description ? lead.description : '', lead.contact_name ? lead.contact_name : '', lead.phone ? lead.phone : '', lead.mobile ? lead.mobile : '', lead.fax ? lead.fax : '', lead.street ? lead.street : '', lead.street2 ? lead.street2 : '', lead.meeting_count ? lead.meeting_count : 0, lead.message_bounce ? lead.message_bounce : 0, lead.planned_cost ? lead.planned_cost : 0, lead.planned_revenue ? lead.planned_revenue : 0, lead.priority ? lead.priority : 0, lead.probability ? lead.probability : 0, lead.type ? lead.type : 'lead', lead.partner_id ? lead.partner_id : 0];
+        var sql = null;
+
+        if (lead.id) {
+            values.push(lead.id);
+            
+            sql = 'UPDATE lead SET remote_id = ?, modified = ?, modifiedDate = CURRENT_TIMESTAMP, name = ?, description = ?, contact_name = ?, phone = ?, mobile = ?, fax = ?, street = ?, street2 = ?, meeting_count = ?, message_bounce = ?, planned_cost = ?, planned_revenue = ?, priority = ?, probability = ?, type = ?, partner_id = ? WHERE id = ?';
+        } else {
+            sql = 'INSERT INTO lead(remote_id, modified, name, description, contact_name, phone, mobile, fax, street, street2, meeting_count, message_bounce, planned_cost, planned_revenue, priority, probability, type, partner_id) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)';
+        }
+
+        db.executeSql(sql, values, function (result) {
+            success(sql.startsWith('INSERT') ? result.insertId : lead.id);
+        }, function (err) {
+            error(err);
+        });
+    }
+
+    // Remove lead from local storage
+    var remove = function (lead, success, error) {
+        var values = [lead.id];
+        var sql = null;
+
+        if (lead.remote_id) {
+            sql = 'UPDATE lead SET modified = 2 WHERE id = ?';
+        } else {
+            sql = 'DELETE FROM lead WHERE id = ?'
+        }
+
+        db.executeSql(sql, values, function (result) {
+            success(result.rowsAffected);
+        }, function(err) {
+            error(err);
+        });
+    }
+
+    var removeAll = function () {
+        var sql = 'DELETE FROM lead'
+
+        db.executeSql(sql, [], function(result) {
+            success(result.rowsAffected);
+        }, function(err) {
+            error(err);
+        });
+    }
+
+    return {
+        save: save,
+        remove: remove,
+        removeAll: removeAll
+    }
+});

+ 91 - 0
www/js/factories/sales/lead.sync.factory.js

@@ -0,0 +1,91 @@
+angular.module('odoo')
+
+.factory('leadsRemoteFactory', function (config, odoo, sqlFactory, asyncLoop) {
+
+    // Retrieve leads data from server
+    var retrieve = function (success, error) {
+        config(function (configuration) {
+            odoo.read('crm.lead', [['active', '=', true]], configuration, function (leads) {
+                success(leads);
+            }, function (odooErr) {
+                error(odooErr);
+            });
+        }, function (configErr) {
+            error(configErr);
+        });
+    }
+
+    // Update leads remote data
+    var update = function (id, data, success, error) {
+        config(function (configuration) {
+            odoo.write('crm.lead', id, data, configuration, function (response) {
+                success(response);
+            }, function (odooWriteErr) {
+                error(odooWriteErr);
+            });
+        }, function (configErr) {
+            error(configErr);
+        });
+    }
+
+    // Create leads on remote data
+    var send = function (data, success, error) {
+        config(function (configuration) {retrieve(function (leads) {
+            console.log(leads);
+            success();
+        }, function (retrieveError) {
+            error(retrieveError);
+        });
+            odoo.create('crm.lead', data, configuration, function (response) {
+                success(response);
+            }, function (odooCreateErr) {
+                error(odooCreateErr);
+            });
+        }, function (configErr) {
+            error(configErr);
+        });
+    }
+
+    // Select leads from local storage
+    var select = function (contraint, success, error) {
+        sqlFactory.selectByConstraint('lead', contraint, function (leads) {
+            success(leads);
+        }, function (err) {
+            error(err);
+        });
+    }
+
+    // Sync leads data between local and remote storage
+    var sync = function (success, error) {
+        select('remote_id = 0', function (newLeads) {
+            asyncLoop(newLeads.length, function (loop) {
+                var data = newLeads.item(loop.iteration());
+
+                // Avoid odoo server warning message
+                delete data.id;
+                delete data.remote_id;
+                delete data.modified;
+                delete data.modifiedDate;
+
+                send(data, function (response) {
+                    console.log(response);
+                    loop.next();
+                }, function (sendErr) {
+                    loop.break();
+                });
+
+            }, function () {
+                success();
+            });
+        }, function (selectErr) {
+            error(selectErr);
+        });
+    }
+
+    return {
+        retrieve: retrieve,
+        update: update,
+        send: send,
+        sync: sync
+    }
+});

+ 24 - 0
www/js/factories/user.storage.factory.js

@@ -0,0 +1,24 @@
+angular.module('odoo')
+
+/**
+ * -----------------------------------------------------------------------------
+ *  Description:    Local storage manager for users data
+ * -----------------------------------------------------------------------------
+ */
+.factory('userStorage', function () {
+
+    // Save user data to local storage
+    var save = function (data, success, error) {
+        var sql = 'INSERT INTO user(remote_id, host, port, database, username, password) VALUES(?, ?, ?, ?, ?, ?)';
+
+        db.executeSql(sql, data, function (result) {
+            success(result.insertId);
+        }, function (err) {
+            error(err);
+        });
+    };
+
+    return {
+        save: save
+    }
+});

+ 13 - 46
www/js/factories/utils.factory.js

@@ -5,7 +5,7 @@ angular.module('odoo')
  *  Description:    Native SQL util instructions
  * -----------------------------------------------------------------------------
  */
-.factory('sql', function () {
+.factory('sqlFactory', function () {
     // Execute native SQL SELECT instruction
     var select = function(tableName, success, error) {
         var sql = 'SELECT * FROM ' + tableName;
@@ -51,10 +51,19 @@ angular.module('odoo')
  *  Description:    Get user configuration from local database
  * -----------------------------------------------------------------------------
  */
-.factory('config', function (sql) {
+.factory('config', function (sqlFactory) {
     return function (success, error) {
-        sql.select('user', function (users) {
-            success(users);
+        sqlFactory.select('user', function (users) {
+            if (users.length == 1) {
+                success(users.item(0));
+            } else {
+                var configs = [];
+                for (var i = 0; i < users.length; i++) {
+                    configs.push(users.item(i))
+                }
+
+                success(configs);
+            }
         }, function (err) {
             error(err);
         });
@@ -101,48 +110,6 @@ angular.module('odoo')
             }
 })
 
-/**
- * -----------------------------------------------------------------------------
- *  Description:    Async loop util
- * -----------------------------------------------------------------------------
- */
-.factory('async', function () {
-    return {
-        loop: function (iterations, func, callback) {
-            var index = 0;
-            var done = false;
-            var loop = {
-                next: function  () {
-                    if (done) {
-                        return;
-                    }
-
-                    if (index < iterations) {
-                        index++;
-                        func(loop);
-
-                    } else {
-                        done = true;
-                        callback();
-                    }
-                },
-
-                iteration: function() {
-                    return index - 1;
-                },
-
-                break: function() {
-                    done = true;
-                    callback();
-                }
-            };
-
-            loop.next();
-            return loop;
-        }
-    }
-})
-
 /**
  * -----------------------------------------------------------------------------
  *  Description:    Native camera manager

+ 5 - 0
www/js/providers/user.config.provider.js

@@ -0,0 +1,5 @@
+angular.module('odoo')
+
+.provider('userConfigProvider', function () {
+    console.log('None');
+});

+ 54 - 0
www/lib/ionic-filter-bar/.bower.json

@@ -0,0 +1,54 @@
+{
+  "name": "ionic-filter-bar",
+  "version": "1.1.1",
+  "description": "A filter directive UI for Ionic apps that animates over the header bar",
+  "author": "Devin Jett <djett41@gmail.com> (https://github.com/djett41)",
+  "main": [
+    "dist/ionic.filter.bar.css",
+    "dist/ionic.filter.bar.js"
+  ],
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/djett41/ionic-filter-bar.git"
+  },
+  "ignore": [
+    "demo",
+    "js",
+    "test",
+    ".gitignore",
+    "gulpfile.js",
+    "karma.conf.js",
+    "LICENSE",
+    "package.json",
+    "README.md"
+  ],
+  "dependencies": {},
+  "devDependencies": {
+    "ionic": "^1.0.0-rc.0",
+    "angular-mocks": "1.4.3"
+  },
+  "keywords": [
+    "mobile",
+    "html5",
+    "ionic",
+    "cordova",
+    "phonegap",
+    "search",
+    "filter",
+    "angularjs",
+    "angular"
+  ],
+  "license": "MIT",
+  "private": false,
+  "homepage": "https://github.com/djett41/ionic-filter-bar",
+  "_release": "1.1.1",
+  "_resolution": {
+    "type": "version",
+    "tag": "v1.1.1",
+    "commit": "0a0a9f310a911815b45161800b276a9404142825"
+  },
+  "_source": "https://github.com/djett41/ionic-filter-bar.git",
+  "_target": "^1.1.1",
+  "_originalSource": "ionic-filter-bar",
+  "_direct": true
+}

+ 43 - 0
www/lib/ionic-filter-bar/bower.json

@@ -0,0 +1,43 @@
+{
+  "name": "ionic-filter-bar",
+  "version": "1.1.1",
+  "description": "A filter directive UI for Ionic apps that animates over the header bar",
+  "author": "Devin Jett <djett41@gmail.com> (https://github.com/djett41)",
+  "main": [
+    "dist/ionic.filter.bar.css",
+    "dist/ionic.filter.bar.js"
+  ],
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/djett41/ionic-filter-bar.git"
+  },
+  "ignore": [
+    "demo",
+    "js",
+    "test",
+    ".gitignore",
+    "gulpfile.js",
+    "karma.conf.js",
+    "LICENSE",
+    "package.json",
+    "README.md"
+  ],
+  "dependencies": {},
+  "devDependencies": {
+    "ionic": "^1.0.0-rc.0",
+    "angular-mocks": "1.4.3"
+  },
+  "keywords": [
+    "mobile",
+    "html5",
+    "ionic",
+    "cordova",
+    "phonegap",
+    "search",
+    "filter",
+    "angularjs",
+    "angular"
+  ],
+  "license": "MIT",
+  "private": false
+}

+ 224 - 0
www/lib/ionic-filter-bar/dist/ionic.filter.bar.css

@@ -0,0 +1,224 @@
+.filter-bar-backdrop {
+  -webkit-transition: opacity 150ms ease-in-out;
+  transition: opacity 150ms ease-in-out;
+  opacity: 0;
+  background-color: rgba(0, 0, 0, 0.4);
+  position: fixed;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%; }
+  .filter-bar-backdrop.active {
+    z-index: 10;
+    opacity: 1; }
+
+.filter-bar {
+  position: fixed;
+  width: 100%;
+  height: 44px;
+  z-index: 10; }
+  .filter-bar .filter-bar-wrapper {
+    z-index: 11;
+    position: absolute;
+    top: 0;
+    right: 0;
+    width: 100%; }
+    .filter-bar .filter-bar-wrapper .item-input-inset .icon.placeholder-icon:before {
+      padding-top: 3px;
+      font-size: 16px; }
+    .filter-bar .filter-bar-wrapper .item-input-inset .item-input-wrapper {
+      background: #fff;
+      height: 28px; }
+      .filter-bar .filter-bar-wrapper .item-input-inset .item-input-wrapper .filter-bar-clear {
+        padding: 0 2px 0 0; }
+        .filter-bar .filter-bar-wrapper .item-input-inset .item-input-wrapper .filter-bar-clear:before {
+          color: #aaa;
+          font-size: 18px;
+          padding-top: 1px; }
+
+.platform-android .filter-bar .filter-bar-light .item-input-wrapper {
+  border-bottom: 1px solid #ccc;
+  background: white; }
+  .platform-android .filter-bar .filter-bar-light .item-input-wrapper input[type="search"] {
+    color: #444; }
+    .platform-android .filter-bar .filter-bar-light .item-input-wrapper input[type="search"]::-moz-placeholder {
+      color: #aaaaaa; }
+    .platform-android .filter-bar .filter-bar-light .item-input-wrapper input[type="search"]:-ms-input-placeholder {
+      color: #aaaaaa; }
+    .platform-android .filter-bar .filter-bar-light .item-input-wrapper input[type="search"]::-webkit-input-placeholder {
+      color: #aaaaaa;
+      text-indent: 0; }
+  .platform-android .filter-bar .filter-bar-light .item-input-wrapper .filter-bar-clear:before {
+    color: #444; }
+.platform-android .filter-bar .filter-bar-stable .item-input-wrapper {
+  border-bottom: 1px solid #a2a2a2;
+  background: #f8f8f8; }
+  .platform-android .filter-bar .filter-bar-stable .item-input-wrapper input[type="search"] {
+    color: #444; }
+    .platform-android .filter-bar .filter-bar-stable .item-input-wrapper input[type="search"]::-moz-placeholder {
+      color: #aaaaaa; }
+    .platform-android .filter-bar .filter-bar-stable .item-input-wrapper input[type="search"]:-ms-input-placeholder {
+      color: #aaaaaa; }
+    .platform-android .filter-bar .filter-bar-stable .item-input-wrapper input[type="search"]::-webkit-input-placeholder {
+      color: #aaaaaa;
+      text-indent: 0; }
+  .platform-android .filter-bar .filter-bar-stable .item-input-wrapper .filter-bar-clear:before {
+    color: #444; }
+.platform-android .filter-bar .filter-bar-positive .item-input-wrapper {
+  border-bottom: 1px solid #0c60ee;
+  background: #387ef5; }
+  .platform-android .filter-bar .filter-bar-positive .item-input-wrapper input[type="search"] {
+    color: #fff; }
+    .platform-android .filter-bar .filter-bar-positive .item-input-wrapper input[type="search"]::-moz-placeholder {
+      color: white; }
+    .platform-android .filter-bar .filter-bar-positive .item-input-wrapper input[type="search"]:-ms-input-placeholder {
+      color: white; }
+    .platform-android .filter-bar .filter-bar-positive .item-input-wrapper input[type="search"]::-webkit-input-placeholder {
+      color: white;
+      text-indent: 0; }
+  .platform-android .filter-bar .filter-bar-positive .item-input-wrapper .filter-bar-clear:before {
+    color: #fff; }
+.platform-android .filter-bar .filter-bar-calm .item-input-wrapper {
+  border-bottom: 1px solid #0a9dc7;
+  background: #11c1f3; }
+  .platform-android .filter-bar .filter-bar-calm .item-input-wrapper input[type="search"] {
+    color: #fff; }
+    .platform-android .filter-bar .filter-bar-calm .item-input-wrapper input[type="search"]::-moz-placeholder {
+      color: white; }
+    .platform-android .filter-bar .filter-bar-calm .item-input-wrapper input[type="search"]:-ms-input-placeholder {
+      color: white; }
+    .platform-android .filter-bar .filter-bar-calm .item-input-wrapper input[type="search"]::-webkit-input-placeholder {
+      color: white;
+      text-indent: 0; }
+  .platform-android .filter-bar .filter-bar-calm .item-input-wrapper .filter-bar-clear:before {
+    color: #fff; }
+.platform-android .filter-bar .filter-bar-assertive .item-input-wrapper {
+  border-bottom: 1px solid #e42112;
+  background: #ef473a; }
+  .platform-android .filter-bar .filter-bar-assertive .item-input-wrapper input[type="search"] {
+    color: #fff; }
+    .platform-android .filter-bar .filter-bar-assertive .item-input-wrapper input[type="search"]::-moz-placeholder {
+      color: white; }
+    .platform-android .filter-bar .filter-bar-assertive .item-input-wrapper input[type="search"]:-ms-input-placeholder {
+      color: white; }
+    .platform-android .filter-bar .filter-bar-assertive .item-input-wrapper input[type="search"]::-webkit-input-placeholder {
+      color: white;
+      text-indent: 0; }
+  .platform-android .filter-bar .filter-bar-assertive .item-input-wrapper .filter-bar-clear:before {
+    color: #fff; }
+.platform-android .filter-bar .filter-bar-balanced .item-input-wrapper {
+  border-bottom: 1px solid #28a54c;
+  background: #33cd5f; }
+  .platform-android .filter-bar .filter-bar-balanced .item-input-wrapper input[type="search"] {
+    color: #fff; }
+    .platform-android .filter-bar .filter-bar-balanced .item-input-wrapper input[type="search"]::-moz-placeholder {
+      color: white; }
+    .platform-android .filter-bar .filter-bar-balanced .item-input-wrapper input[type="search"]:-ms-input-placeholder {
+      color: white; }
+    .platform-android .filter-bar .filter-bar-balanced .item-input-wrapper input[type="search"]::-webkit-input-placeholder {
+      color: white;
+      text-indent: 0; }
+  .platform-android .filter-bar .filter-bar-balanced .item-input-wrapper .filter-bar-clear:before {
+    color: #fff; }
+.platform-android .filter-bar .filter-bar-energized .item-input-wrapper {
+  border-bottom: 1px solid #e6b500;
+  background: #ffc900; }
+  .platform-android .filter-bar .filter-bar-energized .item-input-wrapper input[type="search"] {
+    color: #fff; }
+    .platform-android .filter-bar .filter-bar-energized .item-input-wrapper input[type="search"]::-moz-placeholder {
+      color: white; }
+    .platform-android .filter-bar .filter-bar-energized .item-input-wrapper input[type="search"]:-ms-input-placeholder {
+      color: white; }
+    .platform-android .filter-bar .filter-bar-energized .item-input-wrapper input[type="search"]::-webkit-input-placeholder {
+      color: white;
+      text-indent: 0; }
+  .platform-android .filter-bar .filter-bar-energized .item-input-wrapper .filter-bar-clear:before {
+    color: #fff; }
+.platform-android .filter-bar .filter-bar-royal .item-input-wrapper {
+  border-bottom: 1px solid #6b46e5;
+  background: #886aea; }
+  .platform-android .filter-bar .filter-bar-royal .item-input-wrapper input[type="search"] {
+    color: #fff; }
+    .platform-android .filter-bar .filter-bar-royal .item-input-wrapper input[type="search"]::-moz-placeholder {
+      color: white; }
+    .platform-android .filter-bar .filter-bar-royal .item-input-wrapper input[type="search"]:-ms-input-placeholder {
+      color: white; }
+    .platform-android .filter-bar .filter-bar-royal .item-input-wrapper input[type="search"]::-webkit-input-placeholder {
+      color: white;
+      text-indent: 0; }
+  .platform-android .filter-bar .filter-bar-royal .item-input-wrapper .filter-bar-clear:before {
+    color: #fff; }
+.platform-android .filter-bar .filter-bar-dark .item-input-wrapper {
+  border-bottom: 1px solid #000;
+  background: #444444; }
+  .platform-android .filter-bar .filter-bar-dark .item-input-wrapper input[type="search"] {
+    color: #fff; }
+    .platform-android .filter-bar .filter-bar-dark .item-input-wrapper input[type="search"]::-moz-placeholder {
+      color: white; }
+    .platform-android .filter-bar .filter-bar-dark .item-input-wrapper input[type="search"]:-ms-input-placeholder {
+      color: white; }
+    .platform-android .filter-bar .filter-bar-dark .item-input-wrapper input[type="search"]::-webkit-input-placeholder {
+      color: white;
+      text-indent: 0; }
+  .platform-android .filter-bar .filter-bar-dark .item-input-wrapper .filter-bar-clear:before {
+    color: #fff; }
+.platform-android .filter-bar .filter-bar-default .item-input-wrapper {
+  border-bottom: 1px solid #ccc;
+  background: white; }
+  .platform-android .filter-bar .filter-bar-default .item-input-wrapper input[type="search"] {
+    color: #444; }
+    .platform-android .filter-bar .filter-bar-default .item-input-wrapper input[type="search"]::-moz-placeholder {
+      color: #aaaaaa; }
+    .platform-android .filter-bar .filter-bar-default .item-input-wrapper input[type="search"]:-ms-input-placeholder {
+      color: #aaaaaa; }
+    .platform-android .filter-bar .filter-bar-default .item-input-wrapper input[type="search"]::-webkit-input-placeholder {
+      color: #aaaaaa;
+      text-indent: 0; }
+  .platform-android .filter-bar .filter-bar-default .item-input-wrapper .filter-bar-clear:before {
+    color: #444; }
+.platform-android .filter-bar-wrapper .item-input-inset {
+  padding-right: 24px; }
+  .platform-android .filter-bar-wrapper .item-input-inset .filter-bar-cancel {
+    padding-left: 0; }
+    .platform-android .filter-bar-wrapper .item-input-inset .filter-bar-cancel:before {
+      font-size: 24px; }
+  .platform-android .filter-bar-wrapper .item-input-inset .item-input-wrapper {
+    border-radius: 0;
+    padding-left: 0;
+    margin-left: 10px; }
+    .platform-android .filter-bar-wrapper .item-input-inset .item-input-wrapper input[type="search"] {
+      font-weight: 500; }
+    .platform-android .filter-bar-wrapper .item-input-inset .item-input-wrapper .filter-bar-clear:before {
+      font-size: 20px; }
+
+.filter-bar-transition-horizontal {
+  -webkit-transition: -webkit-transform cubic-bezier(.25, .45, .05, 1) 300ms;
+  transition: transform cubic-bezier(.25, .45, .05, 1) 300ms;
+  -webkit-transform: translate3d(100%, 0, 0);
+  transform: translate3d(100%, 0, 0); }
+
+.filter-bar-transition-vertical {
+  -webkit-transition: -webkit-transform cubic-bezier(.25, .45, .05, 1) 350ms;
+  transition: transform cubic-bezier(.25, .45, .05, 1) 350ms;
+  -webkit-transform: translate3d(0, -100%, 0);
+  transform: translate3d(0, -100%, 0); }
+
+.filter-bar-transition-fade {
+  -webkit-transition: opacity 250ms ease-in-out;
+  transition: opacity 250ms ease-in-out;
+  opacity: 0; }
+
+.filter-bar-in {
+  -webkit-transform: translate3d(0, 0, 0);
+  transform: translate3d(0, 0, 0);
+  opacity: 1; }
+
+.filter-bar-modal .item.item-input {
+  padding-right: 16px; }
+.filter-bar-modal .list-right-editing .item.item-input {
+  opacity: .5; }
+.filter-bar-modal .button.button-icon.ion-ios-checkmark-empty:before {
+  font-size: 42px; }
+
+.filter-bar-element-hide {
+  display: none; }

+ 721 - 0
www/lib/ionic-filter-bar/dist/ionic.filter.bar.js

@@ -0,0 +1,721 @@
+angular.module('jett.ionic.filter.bar', ['ionic']);
+(function (angular, document) {
+  'use strict';
+
+  angular.module('jett.ionic.filter.bar')
+    .directive('ionFilterBar', [
+      '$timeout',
+      '$ionicGesture',
+      '$ionicPlatform',
+      function ($timeout, $ionicGesture, $ionicPlatform) {
+        var filterBarTemplate;
+
+        //create platform specific filterBar template using filterConfig items
+        if ($ionicPlatform.is('android')) {
+          filterBarTemplate =
+            '<div class="filter-bar-wrapper filter-bar-{{::config.theme}} filter-bar-transition-{{::config.transition}}">' +
+              '<div class="bar bar-header bar-{{::config.theme}} item-input-inset">' +
+                '<button class="filter-bar-cancel button button-icon icon {{::config.back}}"></button>' +
+                '<label class="item-input-wrapper">' +
+                  '<input type="search" class="filter-bar-search" ng-model="data.filterText" placeholder="{{::config.placeholder}}" />' +
+                  '<button class="filter-bar-clear button button-icon icon" ng-class="getClearButtonClass()"></button>' +
+                '</label>' +
+              '</div>' +
+            '</div>';
+        } else {
+          filterBarTemplate =
+            '<div class="filter-bar-wrapper filter-bar-{{::config.theme}} filter-bar-transition-{{::config.transition}}">' +
+              '<div class="bar bar-header bar-{{::config.theme}} item-input-inset">' +
+                '<label class="item-input-wrapper">' +
+                  '<i class="icon {{::config.search}} placeholder-icon"></i>' +
+                  '<input type="search" class="filter-bar-search" ng-model="data.filterText" placeholder="{{::config.placeholder}}"/>' +
+                  '<button class="filter-bar-clear button button-icon icon" ng-class="getClearButtonClass()"></button>' +
+                '</label>' +
+                '<button class="filter-bar-cancel button button-clear" ng-bind-html="::cancelText"></button>' +
+              '</div>' +
+            '</div>';
+        }
+
+        return {
+          restrict: 'E',
+          scope: true,
+          link: function ($scope, $element) {
+            var el = $element[0];
+            var clearEl = el.querySelector('.filter-bar-clear');
+            var cancelEl = el.querySelector('.filter-bar-cancel');
+            var inputEl = el.querySelector('.filter-bar-search');
+            var filterTextTimeout;
+            var swipeGesture;
+            var backdrop;
+            var backdropClick;
+            var filterWatch;
+
+            // Action when filter bar is cancelled via backdrop click/swipe or cancel/back buton click.
+            // Invokes cancel function defined in filterBar service
+            var cancelFilterBar = function () {
+              $scope.cancelFilterBar();
+            };
+
+            // If backdrop is enabled, create and append it to filter, then add click/swipe listeners to cancel filter
+            if ($scope.config.backdrop) {
+              backdrop = angular.element('<div class="filter-bar-backdrop"></div>');
+              $element.append(backdrop);
+
+              backdropClick = function(e) {
+                if (e.target == backdrop[0]) {
+                  cancelFilterBar();
+                }
+              };
+
+              backdrop.bind('click', backdropClick);
+              swipeGesture = $ionicGesture.on('swipe', backdropClick, backdrop);
+            }
+
+            //Sure we could have had 1 function that also checked for favoritesEnabled.. but no need to keep checking a var that wont change
+            if ($scope.favoritesEnabled) {
+              $scope.getClearButtonClass = function () {
+                return $scope.data.filterText.length ? $scope.config.clear : $scope.config.favorite;
+              }
+            } else {
+              $scope.getClearButtonClass = function () {
+                return $scope.data.filterText.length ? $scope.config.clear : 'filter-bar-element-hide';
+              }
+            }
+
+            // When clear button is clicked, clear filterText, hide clear button, show backdrop, and focus the input
+            var clearClick = function () {
+              if (clearEl.classList.contains($scope.config.favorite)) {
+                $scope.showModal();
+              } else {
+                $timeout(function () {
+                  $scope.data.filterText = '';
+                  ionic.requestAnimationFrame(function () {
+                    $scope.showBackdrop();
+                    $scope.scrollItemsTop();
+                    $scope.focusInput();
+                  });
+                });
+              }
+            };
+
+            // Bind touchstart so we can regain focus of input even while scrolling
+            var inputClick = function () {
+              $scope.scrollItemsTop();
+              $scope.focusInput();
+            };
+
+            // When a non escape key is pressed, show/hide backdrop/clear button based on filterText length
+            var keyUp = function(e) {
+              if (e.which == 27) {
+                cancelFilterBar();
+              } else if ($scope.data.filterText && $scope.data.filterText.length) {
+                $scope.hideBackdrop();
+              } else {
+                $scope.showBackdrop();
+              }
+            };
+
+            //Event Listeners
+            cancelEl.addEventListener('click', cancelFilterBar);
+            // Since we are wrapping with label, need to bind touchstart rather than click.
+            // Even if we use div instead of label need to bind touchstart.  Click isn't allowing input to regain focus quickly
+            clearEl.addEventListener('touchstart', clearClick);
+            clearEl.addEventListener('mousedown', clearClick);
+
+            inputEl.addEventListener('touchstart', inputClick);
+            inputEl.addEventListener('mousedown', inputClick);
+
+            document.addEventListener('keyup', keyUp);
+
+            // Calls the services filterItems function with the filterText to filter items
+            var filterItems = function () {
+              $scope.filterItems($scope.data.filterText);
+            };
+
+            // Clean up when scope is destroyed
+            $scope.$on('$destroy', function() {
+              $element.remove();
+              document.removeEventListener('keyup', keyUp);
+              if (backdrop) {
+                $ionicGesture.off(swipeGesture, 'swipe', backdropClick);
+              }
+              filterWatch();
+            });
+
+            // Watch for changes on filterText and call filterItems when filterText has changed.
+            // If debounce is enabled, filter items by the specified or default delay.
+            // Prefer timeout debounce over ng-model-options so if filterText is cleared, initial items show up right away with no delay
+            filterWatch = $scope.$watch('data.filterText', function (newFilterText, oldFilterText) {
+              var delay;
+
+              if (filterTextTimeout) {
+                $timeout.cancel(filterTextTimeout);
+              }
+
+              if (newFilterText !== oldFilterText) {
+                delay = (newFilterText.length && $scope.debounce) ? $scope.delay : 0;
+                filterTextTimeout = $timeout(filterItems, delay, false);
+              }
+            });
+          },
+          template: filterBarTemplate
+        };
+      }]);
+
+})(angular, document);
+
+/* global angular */
+/**
+ * This copies the functionality of the ionicConfig provider to allow for platform specific configuration
+ */
+(function (angular) {
+  'use strict';
+
+  angular.module('jett.ionic.filter.bar')
+    .provider('$ionicFilterBarConfig', function () {
+
+      var provider = this;
+      provider.platform = {};
+      var PLATFORM = 'platform';
+
+      var configProperties = {
+        theme: PLATFORM,
+        clear: PLATFORM,
+        add: PLATFORM,
+        close: PLATFORM,
+        done: PLATFORM,
+        remove: PLATFORM,
+        reorder: PLATFORM,
+        favorite: PLATFORM,
+        search: PLATFORM,
+        backdrop: PLATFORM,
+        transition: PLATFORM,
+        platform: {},
+        placeholder: PLATFORM
+      };
+
+      createConfig(configProperties, provider, '');
+
+      // Default
+      // -------------------------
+      setPlatformConfig('default', {
+        clear: 'ion-ios-close',
+        add: 'ion-ios-plus-outline',
+        close: 'ion-ios-close-empty',
+        done: 'ion-ios-checkmark-empty',
+        remove: 'ion-ios-trash-outline',
+        reorder: 'ion-drag',
+        favorite: 'ion-ios-star',
+        search: 'ion-ios-search-strong',
+        backdrop: true,
+        transition: 'vertical',
+        placeholder: 'Search'
+      });
+
+      // iOS (it is the default already)
+      // -------------------------
+      setPlatformConfig('ios', {});
+
+      // Android
+      // -------------------------
+      setPlatformConfig('android', {
+        clear: 'ion-android-close',
+        close: 'ion-android-close',
+        done: 'ion-android-done',
+        remove: 'ion-android-delete',
+        favorite: 'ion-android-star',
+        search: false,
+        backdrop: false,
+        transition: 'horizontal'
+      });
+
+      provider.setPlatformConfig = setPlatformConfig;
+
+      // private: used to set platform configs
+      function setPlatformConfig(platformName, platformConfigs) {
+        configProperties.platform[platformName] = platformConfigs;
+        provider.platform[platformName] = {};
+
+        addConfig(configProperties, configProperties.platform[platformName]);
+
+        createConfig(configProperties.platform[platformName], provider.platform[platformName], '');
+      }
+
+      // private: used to recursively add new platform configs
+      function addConfig(configObj, platformObj) {
+        for (var n in configObj) {
+          if (n != PLATFORM && configObj.hasOwnProperty(n)) {
+            if (angular.isObject(configObj[n])) {
+              if (!angular.isDefined(platformObj[n])) {
+                platformObj[n] = {};
+              }
+              addConfig(configObj[n], platformObj[n]);
+
+            } else if (!angular.isDefined(platformObj[n])) {
+              platformObj[n] = null;
+            }
+          }
+        }
+      }
+
+      // private: create methods for each config to get/set
+      function createConfig(configObj, providerObj, platformPath) {
+        angular.forEach(configObj, function(value, namespace) {
+
+          if (angular.isObject(configObj[namespace])) {
+            // recursively drill down the config object so we can create a method for each one
+            providerObj[namespace] = {};
+            createConfig(configObj[namespace], providerObj[namespace], platformPath + '.' + namespace);
+
+          } else {
+            // create a method for the provider/config methods that will be exposed
+            providerObj[namespace] = function(newValue) {
+              if (arguments.length) {
+                configObj[namespace] = newValue;
+                return providerObj;
+              }
+              if (configObj[namespace] == PLATFORM) {
+                // if the config is set to 'platform', then get this config's platform value
+                var platformConfig = stringObj(configProperties.platform, ionic.Platform.platform() + platformPath + '.' + namespace);
+                if (platformConfig || platformConfig === false) {
+                  return platformConfig;
+                }
+                // didnt find a specific platform config, now try the default
+                return stringObj(configProperties.platform, 'default' + platformPath + '.' + namespace);
+              }
+              return configObj[namespace];
+            };
+          }
+
+        });
+      }
+
+      //splits a string by dot operator and accesses the end var.  For example in a.b.c,
+      function stringObj(obj, str) {
+        str = str.split(".");
+        for (var i = 0; i < str.length; i++) {
+          if (obj && angular.isDefined(obj[str[i]])) {
+            obj = obj[str[i]];
+          } else {
+            return null;
+          }
+        }
+        return obj;
+      }
+
+      provider.$get = function() {
+        return provider;
+      };
+
+    });
+
+})(angular);
+
+/* global angular,ionic */
+/**
+ * @ngdoc service
+ * @name $ionicFilterBar
+ * @module ionic
+ * @description The Filter Bar is an animated bar that allows a user to search or filter an array of items.
+ */
+(function (angular, ionic) {
+  'use strict';
+
+  var filterBarModalTemplate =
+    '<ion-modal-view ng-controller="$ionicFilterBarModalCtrl" class="filter-bar-modal">' +
+    '<ion-header-bar class="bar bar-{{::config.theme}} disable-user-behavior">' +
+      '<button class="button button-icon {{::config.close}}" ng-click="closeModal()"></button>' +
+      '<h1 class="title" ng-bind-html="::favoritesTitle"></h1>' +
+      '<button ng-if="searches.length > 1" class="button button-icon" ng-class="displayData.showReorder ? config.done : config.reorder" ng-click="displayData.showReorder = !displayData.showReorder"></button>' +
+    '</ion-header-bar>' +
+    '<ion-content>' +
+      '<ion-list show-reorder="displayData.showReorder" delegate-handle="searches-list">' +
+        '<ion-item ng-repeat="item in searches" class="item-remove-animate" ng-class="{reordered: item.reordered}" ng-click="itemClicked(item.text, $event)">' +
+          '<span ng-bind-html="item.text"></span>' +
+          '<ion-option-button class="button-assertive icon {{::config.remove}}" ng-click="deleteItem(item)"></ion-option-button>' +
+          '<ion-reorder-button class="{{::config.reorder}}" on-reorder="moveItem(item, $fromIndex, $toIndex)"></ion-reorder-button>' +
+        '</ion-item>' +
+        '<div class="item item-input">' +
+          '<input type="text" ng-model="newItem.text" placeholder="{{::favoritesAddPlaceholder}}"/>' +
+          '<button class="button button-icon icon {{::config.add}}" ng-click="addItem(newItem)"></button>' +
+        '</div>' +
+      '</ion-list>' +
+    '</ion-content> ' +
+    '</ion-modal-view>';
+
+  var getNavBarTheme = function ($navBar) {
+    var themes = ['light', 'stable', 'positive', 'calm', 'balanced', 'energized', 'assertive', 'royal', 'dark'];
+    var classList = $navBar && $navBar.classList;
+
+    if (!classList) {
+      return;
+    }
+
+    for (var i = 0; i < themes.length; i++) {
+      if (classList.contains('bar-' + themes[i])) {
+        return themes[i];
+      }
+    }
+  };
+
+  angular.module('jett.ionic.filter.bar')
+    .factory('$ionicFilterBar', [
+      '$document',
+      '$rootScope',
+      '$compile',
+      '$timeout',
+      '$filter',
+      '$ionicPlatform',
+      '$ionicFilterBarConfig',
+      '$ionicConfig',
+      '$ionicModal',
+      '$ionicScrollDelegate',
+      function ($document, $rootScope, $compile, $timeout, $filter, $ionicPlatform, $ionicFilterBarConfig, $ionicConfig, $ionicModal, $ionicScrollDelegate) {
+        var isShown = false;
+        var $body = $document[0].body;
+        var templateConfig = {
+          theme: $ionicFilterBarConfig.theme(),
+          transition: $ionicFilterBarConfig.transition(),
+          back: $ionicConfig.backButton.icon(),
+          clear: $ionicFilterBarConfig.clear(),
+          favorite: $ionicFilterBarConfig.favorite(),
+          search: $ionicFilterBarConfig.search(),
+          backdrop: $ionicFilterBarConfig.backdrop(),
+          placeholder: $ionicFilterBarConfig.placeholder(),
+          close: $ionicFilterBarConfig.close(),
+          done: $ionicFilterBarConfig.done(),
+          reorder: $ionicFilterBarConfig.reorder(),
+          remove: $ionicFilterBarConfig.remove(),
+          add: $ionicFilterBarConfig.add()
+        };
+
+        /**
+         * @ngdoc method
+         * @name $ionicFilterBar#show
+         * @description
+         * Load and return a new filter bar.
+         *
+         * A new isolated scope will be created for the filter bar and the new filter bar will be appended to the
+         * body, covering the header bar.
+         *
+         * @returns {function} `hideFilterBar` A function which, when called, hides & cancels the filter bar.
+         */
+        function filterBar (opts) {
+          //if filterBar is already shown return
+          if (isShown) {
+            return;
+          }
+
+          isShown = true;
+          opts = opts || {};
+
+          var scope = $rootScope.$new(true);
+          var backdropShown = false;
+          var isKeyboardShown = false;
+
+          //if container option is set, determine the container element by querying for the container class
+          if (opts.container) {
+            opts.container = $body.querySelector(opts.container);
+          }
+
+          //extend scope defaults with supplied options
+          angular.extend(scope, {
+            config: templateConfig,
+            $deregisterBackButton: angular.noop,
+            update: angular.noop,
+            cancel: angular.noop,
+            done: angular.noop,
+            scrollDelegate: $ionicScrollDelegate,
+            filter: $filter('filter'),
+            filterProperties: null,
+            expression: null,
+            comparator: null,
+            debounce: true,
+            delay: 300,
+            cancelText: 'Cancel',
+            cancelOnStateChange: true,
+            container: $body,
+            favoritesTitle: 'Favorite Searches',
+            favoritesAddPlaceholder: 'Add a search term',
+            favoritesEnabled: false,
+            favoritesKey: 'ionic_filter_bar_favorites'
+          }, opts);
+
+          scope.data = {filterText: ''};
+
+          //if no custom theme was configured, get theme of containers bar-header
+          if (!scope.config.theme) {
+            scope.config.theme = getNavBarTheme(scope.container.querySelector('.bar.bar-header'));
+          }
+
+          // Compile the template
+          var element = scope.element = $compile('<ion-filter-bar class="filter-bar"></ion-filter-bar>')(scope);
+
+          // Grab required jQLite elements
+          var filterWrapperEl = element.children().eq(0);
+          var input = filterWrapperEl.find('input')[0];
+          var backdropEl = element.children().eq(1);
+
+          //get scrollView
+          var scrollView = scope.scrollDelegate.getScrollView();
+          var canScroll = !!scrollView;
+
+          //get the scroll container if scrolling is available
+          var $scrollContainer = canScroll ? scrollView.__container : null;
+
+          var stateChangeListenDone = scope.cancelOnStateChange ?
+            $rootScope.$on('$stateChangeSuccess', function () { scope.cancelFilterBar(); }) :
+            angular.noop;
+
+          // Focus the input which will show the keyboard.
+          var showKeyboard = function () {
+            if (!isKeyboardShown) {
+              isKeyboardShown = true;
+              input && input.focus();
+            }
+          };
+
+          // Blur the input which will hide the keyboard.
+          // Even if we need to bring in ionic.keyboard in the future, blur is preferred so keyboard animates out.
+          var hideKeyboard = function () {
+            if (isKeyboardShown) {
+              isKeyboardShown = false;
+              input && input.blur();
+            }
+          };
+
+          // When the filtered list is scrolled, we want to hide the keyboard as long as it's not already hidden
+          var handleScroll = function () {
+            if (scrollView.__scrollTop > 0) {
+              hideKeyboard();
+            }
+          };
+
+          // Scrolls the list of items to the top via the scroll delegate
+          scope.scrollItemsTop = function () {
+            if (canScroll && scrollView.__scrollTop > 0 && scope.scrollDelegate.scrollTop) {
+              scope.scrollDelegate.scrollTop();
+            }
+          };
+
+          // Set isKeyboardShown to force showing keyboard on search focus.
+          scope.focusInput = function () {
+            isKeyboardShown = false;
+            showKeyboard();
+          };
+
+          // Hide the filterBar backdrop if in the DOM and not already hidden.
+          scope.hideBackdrop = function () {
+            if (backdropEl.length && backdropShown) {
+              backdropShown = false;
+              backdropEl.removeClass('active').css('display', 'none');
+            }
+          };
+
+          // Show the filterBar backdrop if in the DOM and not already shown.
+          scope.showBackdrop = function () {
+            if (backdropEl.length && !backdropShown) {
+              backdropShown = true;
+              backdropEl.css('display', 'block').addClass('active');
+            }
+          };
+
+          scope.showModal = function () {
+            scope.modal = $ionicModal.fromTemplate(filterBarModalTemplate, {
+              scope: scope
+            });
+            scope.modal.show();
+          };
+
+          // Filters the supplied list of items via the supplied filterText.
+          // How items are filtered depends on the supplied filter object, and expression
+          // Filtered items will be sent to update
+          scope.filterItems = function(filterText) {
+            var filterExp, filteredItems;
+
+            // pass back original list if filterText is empty.
+            // Otherwise filter by expression, supplied properties, or filterText.
+            if (!filterText.length) {
+              filteredItems = scope.items;
+            } else {
+              if (scope.expression) {
+                filterExp = angular.bind(this, scope.expression, filterText);
+              } else if (angular.isArray(scope.filterProperties)) {
+                filterExp = {};
+                angular.forEach(scope.filterProperties, function (property) {
+                  filterExp[property] = filterText;
+                });
+              } else if (scope.filterProperties) {
+                filterExp = {};
+                filterExp[scope.filterProperties] = filterText;
+              } else {
+                filterExp = filterText;
+              }
+
+              filteredItems = scope.filter(scope.items, filterExp, scope.comparator);
+            }
+
+            $timeout(function() {
+              scope.update(filteredItems, filterText);
+              scope.scrollItemsTop();
+            });
+          };
+
+          // registerBackButtonAction returns a callback to deregister the action
+          scope.$deregisterBackButton = $ionicPlatform.registerBackButtonAction(
+            function() {
+              $timeout(scope.cancelFilterBar);
+            }, 300
+          );
+
+          // Removes the filterBar from the body and cleans up vars/events.  Once the backdrop is hidden we can invoke done
+          scope.removeFilterBar = function(done) {
+            if (scope.removed) return;
+
+            scope.removed = true;
+
+            //animate the filterBar out, hide keyboard and backdrop
+            ionic.requestAnimationFrame(function () {
+              filterWrapperEl.removeClass('filter-bar-in');
+              hideKeyboard();
+              scope.hideBackdrop();
+
+              //Wait before cleaning up so element isn't removed before filter bar animates out
+              $timeout(function () {
+                scope.scrollItemsTop();
+                scope.update(scope.items);
+
+                scope.$destroy();
+                element.remove();
+                scope.cancelFilterBar.$scope = scope.modal = $scrollContainer = scrollView = filterWrapperEl = backdropEl = input = null;
+                isShown = false;
+                (done || angular.noop)();
+              }, 350);
+            });
+
+            $timeout(function () {
+              // wait to remove this due to a 300ms delay native
+              // click which would trigging whatever was underneath this
+              scope.container.classList.remove('filter-bar-open');
+            }, 400);
+
+            scope.$deregisterBackButton();
+            stateChangeListenDone();
+
+            //unbind scroll event
+            if ($scrollContainer) {
+              $scrollContainer.removeEventListener('scroll', handleScroll);
+            }
+          };
+
+          // Appends the filterBar to the body.  Once the backdrop is hidden we can invoke done
+          scope.showFilterBar = function(done) {
+            if (scope.removed) return;
+
+            scope.container.appendChild(element[0]);
+            scope.container.classList.add('filter-bar-open');
+
+            //scroll items to the top before starting the animation
+            scope.scrollItemsTop();
+
+            //start filterBar animation, show backrop and focus the input
+            ionic.requestAnimationFrame(function () {
+              if (scope.removed) return;
+
+              $timeout(function () {
+                filterWrapperEl.addClass('filter-bar-in');
+                scope.focusInput();
+                scope.showBackdrop();
+                (done || angular.noop)();
+              }, 20, false);
+            });
+
+            if ($scrollContainer) {
+              $scrollContainer.addEventListener('scroll', handleScroll);
+            }
+          };
+
+          // called when the user presses the backdrop, cancel/back button, changes state
+          scope.cancelFilterBar = function() {
+            // after the animation is out, call the cancel callback
+            scope.removeFilterBar(scope.cancel);
+          };
+
+          scope.showFilterBar(scope.done);
+
+          // Expose the scope on $ionFilterBar's return value for the sake of testing it.
+          scope.cancelFilterBar.$scope = scope;
+
+          return scope.cancelFilterBar;
+        }
+
+        return {
+          show: filterBar
+        };
+      }]);
+
+
+})(angular, ionic);
+
+/* global angular */
+(function (angular) {
+  'use strict';
+
+  angular.module('jett.ionic.filter.bar')
+    .controller('$ionicFilterBarModalCtrl', [
+      '$window',
+      '$scope',
+      '$timeout',
+      '$ionicListDelegate',
+      function ($window, $scope, $timeout, $ionicListDelegate) {
+        var searchesKey = $scope.$parent.favoritesKey;
+
+        $scope.displayData = {showReorder: false};
+        $scope.searches = angular.fromJson($window.localStorage.getItem(searchesKey)) || [];
+        $scope.newItem = {text: ''};
+
+        $scope.moveItem = function(item, fromIndex, toIndex) {
+          item.reordered = true;
+          $scope.searches.splice(fromIndex, 1);
+          $scope.searches.splice(toIndex, 0, item);
+
+          $timeout(function () {
+            delete item.reordered;
+          }, 500);
+        };
+
+        $scope.deleteItem = function(item) {
+          var index = $scope.searches.indexOf(item);
+          $scope.searches.splice(index, 1);
+        };
+
+        $scope.addItem = function () {
+          if ($scope.newItem.text) {
+            $scope.searches.push({
+              text: $scope.newItem.text
+            });
+            $scope.newItem.text = '';
+          }
+        };
+
+        $scope.closeModal = function () {
+          $window.localStorage.setItem(searchesKey, angular.toJson($scope.searches));
+          $scope.$parent.modal.remove();
+        };
+
+        $scope.itemClicked = function (filterText, $event) {
+          var isOptionButtonsClosed = !!$event.currentTarget.querySelector('.item-options.invisible');
+
+          if (isOptionButtonsClosed) {
+            $scope.closeModal();
+            $scope.$parent.hideBackdrop();
+            $scope.$parent.data.filterText = filterText;
+            $scope.$parent.filterItems(filterText);
+          } else {
+            $ionicListDelegate.$getByHandle('searches-list').closeOptionButtons();
+          }
+        };
+
+      }]);
+
+})(angular);

Різницю між файлами не показано, бо вона завелика
+ 0 - 0
www/lib/ionic-filter-bar/dist/ionic.filter.bar.min.css


Різницю між файлами не показано, бо вона завелика
+ 0 - 0
www/lib/ionic-filter-bar/dist/ionic.filter.bar.min.js


+ 179 - 0
www/lib/ionic-filter-bar/scss/ionic.filter.bar.scss

@@ -0,0 +1,179 @@
+// Filter Bar
+
+// Variables
+//-----------------------------------
+
+$z-index-filter-bar: 11;
+
+// Mixins
+//-----------------------------------
+
+@mixin filter-bar-style($filter-bar-bg-color, $filter-bar-active-border-color, $filter-bar-text) {
+  .item-input-wrapper {
+    border-bottom: 1px solid $filter-bar-active-border-color;
+    background: $filter-bar-bg-color;
+    input[type="search"] {
+      @include placeholder(lighten($filter-bar-text, 40%));
+      color: $filter-bar-text;
+    }
+    .filter-bar-clear {
+      &:before {
+        color: $filter-bar-text;
+      }
+    }
+  }
+}
+
+// Styles
+//-----------------------------------
+
+.filter-bar-backdrop {
+  @include transition(opacity 150ms ease-in-out);
+  opacity: 0;
+  background-color: rgba(0,0,0,0.4);
+  position: fixed;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+
+  &.active {
+    z-index: $z-index-bar-above;
+    opacity: 1;
+  }
+}
+
+.filter-bar {
+  position: fixed;
+  width: 100%;
+  height: $bar-height;
+  z-index: $z-index-bar-above;
+
+  .filter-bar-wrapper {
+    z-index: $z-index-filter-bar;
+    position: absolute;
+    top: 0;
+    right:0;
+    width: 100%;
+
+    .item-input-inset {
+      .icon.placeholder-icon:before {
+        padding-top: 3px;
+        font-size: 16px;
+      }
+      .item-input-wrapper {
+        background: $light;
+        height: 28px;
+        .filter-bar-clear {
+          padding: 0 2px 0 0;
+          &:before {
+            color: #aaa;
+            font-size: 18px;
+            padding-top: 1px;
+          }
+        }
+      }
+    }
+  }
+}
+
+//android
+.platform-android {
+  .filter-bar {
+    .filter-bar-light  {
+      @include filter-bar-style($bar-light-bg, $bar-light-active-border, $bar-light-text);
+    }
+    .filter-bar-stable  {
+      @include filter-bar-style($bar-stable-bg, $bar-stable-active-border, $bar-stable-text);
+    }
+    .filter-bar-positive  {
+      @include filter-bar-style($bar-positive-bg, $bar-positive-active-border, $bar-positive-text);
+    }
+    .filter-bar-calm  {
+      @include filter-bar-style($bar-calm-bg, $bar-calm-active-border, $bar-positive-text);
+    }
+    .filter-bar-assertive  {
+      @include filter-bar-style($bar-assertive-bg, $bar-assertive-active-border, $bar-assertive-text);
+    }
+    .filter-bar-balanced  {
+      @include filter-bar-style($bar-balanced-bg, $bar-balanced-active-border, $bar-balanced-text);
+    }
+    .filter-bar-energized  {
+      @include filter-bar-style($bar-energized-bg, $bar-energized-active-border, $bar-energized-text);
+    }
+    .filter-bar-royal  {
+      @include filter-bar-style($bar-royal-bg, $bar-royal-active-border, $bar-royal-text);
+    }
+    .filter-bar-dark  {
+      @include filter-bar-style($bar-dark-bg, $bar-dark-active-border, $bar-dark-text);
+    }
+    .filter-bar-default  {
+      @include filter-bar-style($bar-default-bg, $bar-default-active-border, $bar-default-text)
+    };
+  }
+
+  .filter-bar-wrapper {
+    .item-input-inset {
+      padding-right: $button-padding * 2;
+      .filter-bar-cancel {
+        padding-left: 0;
+        &:before {
+          font-size: 24px;
+        }
+      }
+      .item-input-wrapper {
+        border-radius: 0;
+        padding-left: 0;
+        margin-left: 10px;
+        input[type="search"] {
+          font-weight: 500;
+        }
+        .filter-bar-clear:before {
+          font-size: 20px;
+        }
+      }
+    }
+  }
+}
+
+.filter-bar-transition-horizontal {
+  @include transition-transform(cubic-bezier(.25, .45, .05, 1) 300ms);
+  @include translate3d(100%, 0, 0);
+}
+
+.filter-bar-transition-vertical {
+  @include transition-transform(cubic-bezier(.25, .45, .05, 1) 350ms);
+  @include translate3d(0, -100%, 0);
+}
+
+.filter-bar-transition-fade {
+  @include transition(opacity 250ms ease-in-out) ;
+  opacity: 0;
+}
+
+.filter-bar-in {
+  @include translate3d(0, 0, 0);
+  opacity: 1;
+}
+
+.filter-bar-modal {
+  .item {
+    &.item-input {
+      padding-right: $item-padding;
+    }
+  }
+  .list-right-editing {
+    .item.item-input {
+      opacity: .5;
+    }
+  }
+
+  //in my opinion the ios checkmark is a little skimp.. make it bigger
+  .button.button-icon.ion-ios-checkmark-empty:before {
+    font-size: 42px;
+  }
+}
+
+.filter-bar-hide {
+  display: none;
+}

Різницю між файлами не показано, бо вона завелика
+ 11 - 0
www/templates/sales/customer.html


Різницю між файлами не показано, бо вона завелика
+ 12 - 0
www/templates/sales/customers.html


+ 38 - 0
www/templates/sales/lead.html

@@ -0,0 +1,38 @@
+<ion-modal-view>
+    <ion-header-bar class="bar bar-positive">
+        <h1 class="title">Iniciativa</h1>
+        <div class="buttons">
+            <button class="button button-clear ion-checkmark-round" style="font-size:22px !important; padding-left: 5px;" type="submit" form="lead-form" ng-disabled="!lead.name"></button>
+        </div>
+   </ion-header-bar>
+    <ion-content>
+        <form id="lead-form" ng-submit="save()">
+            <div class="list">
+                <label class="item item-input">
+                    <input type="text" autofocus="autofocus" placeholder="Asunto" ng-model="lead.name">
+                </label>
+                <label class="item item-input">
+                    <input type="text" placeholder="Contacto" ng-model="lead.contact_name">
+                </label>
+                <label class="item item-input">
+                    <input type="text" placeholder="Dirección" ng-model="lead.street">
+                </label>
+                <label class="item item-input">
+                    <input type="text" placeholder="Dirección" ng-model="lead.street2">
+                </label>
+                <label class="item item-input">
+                    <input type="tel" placeholder="Teléfono" ng-model="lead.phone">
+                </label>
+                <label class="item item-input">
+                    <input type="tel" placeholder="Celular" ng-model="lead.mobile">
+                </label>
+                <label class="item item-input">
+                    <input type="tel" placeholder="Fax" ng-model="lead.fax">
+                </label>
+                <label class="item item-input">
+                    <textarea placeholder="Nota" rows="8" cols="40" ng-model="lead.description"></textarea>
+                </label>
+            </div>
+        </form>
+    </ion-content>
+</ion-modal-view>

+ 19 - 0
www/templates/sales/leads.html

@@ -0,0 +1,19 @@
+<ion-view title="Iniciativas">
+
+    <ion-nav-buttons side="right">
+        <button class="button button-clear ion-search" style="font-size:22px !important; padding-right: 5px;" ng-click="toggleSearch()"></i></button>
+        <button class="button button-clear ion-plus-round" style="font-size:22px !important;  padding-left: 5px;" ng-click="show()"></i></button>
+    </ion-nav-buttons>
+
+    <ion-content>
+        <ion-list>
+          <ion-item assertive on-hold="openOptions($index)" ng-repeat="l in leads">
+              <h2><strong>{{ l.name }}</strong></h2>
+              <p><strong>Contacto:</strong>{{ l.contact_name }}</p>
+              <p><strong>Teléfono:</strong>{{ l.phone }}</p>
+              <p><strong>Celular:</strong>{{ l.mobile }}</p>
+              <span class="badge badge-positive">{{ l.meeting_count || 0 }} </span>
+          </ion-item>
+        </ion-list>
+    </ion-content>
+</ion-view>

+ 9 - 0
www/templates/sales/sales.html

@@ -0,0 +1,9 @@
+<ion-view title="Ventas">
+    <ion-content>
+        <ion-list>
+            <ion-item class="item-icon-left" ui-sref="app.customers"><i class="ion-person-stalker" style="font-size:22px"></i> Clientes</ion-item>
+            <ion-item class="item-icon-left" ui-sref="app.leads"><i class="ion-happy" style="font-size:22px"></i> Iniciativas</ion-item>
+            <ion-item class="item-icon-left"><i class="ion-star" style="font-size:22px"></i> Oportunidades</ion-item>
+        </ion-list>
+    </ion-content>
+</ion-view>

Деякі файли не було показано, через те що забагато файлів було змінено