├── src ├── client │ ├── assets │ │ ├── empty.html │ │ └── icon │ │ │ ├── favicon.ico │ │ │ └── favicon.icns │ ├── pages │ │ ├── home │ │ │ ├── home.scss │ │ │ ├── home.ts │ │ │ └── home.html │ │ ├── settings │ │ │ ├── audit │ │ │ │ ├── audit.scss │ │ │ │ ├── audititem.ts │ │ │ │ ├── audit.ts │ │ │ │ └── audit.html │ │ │ ├── settings.scss │ │ │ ├── error │ │ │ │ ├── error.scss │ │ │ │ ├── erroritem.ts │ │ │ │ ├── error.ts │ │ │ │ └── error.html │ │ │ ├── locationmanage │ │ │ │ ├── location.management.scss │ │ │ │ ├── location.management.ts │ │ │ │ └── location.management.html │ │ │ ├── about │ │ │ │ ├── about.ts │ │ │ │ └── about.html │ │ │ ├── settings.ts │ │ │ ├── settings.html │ │ │ └── edit │ │ │ │ └── editsettings.ts │ │ ├── inventory │ │ │ ├── inventory.scss │ │ │ ├── ivmanage │ │ │ │ ├── ivmanage.scss │ │ │ │ └── ivmanage.html │ │ │ ├── quick │ │ │ │ ├── quick.scss │ │ │ │ ├── quick.ts │ │ │ │ └── quick.html │ │ │ ├── oumanage │ │ │ │ ├── ou.management.scss │ │ │ │ ├── ou.management.html │ │ │ │ └── ou.management.ts │ │ │ ├── inventoryitem.ts │ │ │ ├── inventory.ts │ │ │ ├── management │ │ │ │ └── inventory.management.ts │ │ │ └── inventory.html │ │ ├── invoices │ │ │ ├── invoices.scss │ │ │ ├── view │ │ │ │ ├── invoice.view.scss │ │ │ │ └── invoice.view.ts │ │ │ ├── invoiceitem.ts │ │ │ └── invoices.ts │ │ ├── promotions │ │ │ ├── promotions.scss │ │ │ ├── management │ │ │ │ └── promotions.management.scss │ │ │ ├── promotiondisplay.ts │ │ │ ├── promotions.ts │ │ │ └── promotions.html │ │ ├── pointofsale │ │ │ ├── cashpay │ │ │ │ ├── pointofsale.cashpay.scss │ │ │ │ ├── pointofsale.cashpay.html │ │ │ │ └── pointofsale.cashpay.ts │ │ │ ├── pointofsale.scss │ │ │ ├── transactionpromo.ts │ │ │ └── transactionitem.ts │ │ └── reporting │ │ │ └── reporting.scss │ ├── models │ │ ├── pageditems.ts │ │ ├── location.ts │ │ ├── organizationalunit.ts │ │ ├── stockitemvendor.ts │ │ ├── auditmessage.ts │ │ ├── errormessage.ts │ │ ├── promoitem.ts │ │ ├── invoicepromo.ts │ │ ├── invoiceitem.ts │ │ ├── invoice.ts │ │ ├── stockitem.ts │ │ ├── promotion.ts │ │ ├── reportconfiguration.ts │ │ └── settings.ts │ ├── app │ │ ├── main.ts │ │ ├── app.component.ts │ │ └── app.scss │ ├── manifest.json │ ├── pipes │ │ ├── truncate.ts │ │ └── currency-from-settings.ts │ ├── components │ │ ├── add-button.ts │ │ ├── update-button.ts │ │ ├── edit-button.ts │ │ ├── view-button.ts │ │ ├── remove-button.ts │ │ ├── reset-button.ts │ │ ├── manage-button.ts │ │ ├── refresh-button.ts │ │ ├── import-button.ts │ │ ├── export-button.ts │ │ ├── confirm-button.ts │ │ ├── print-button.ts │ │ ├── form-error.ts │ │ ├── void-button.ts │ │ ├── resume-button.ts │ │ ├── update-quantity-button.ts │ │ └── top-icon-button.ts │ ├── services │ │ ├── override │ │ │ ├── error.custom.ts │ │ │ └── http.custom.ts │ │ ├── audit.service.ts │ │ ├── inventory.service.ts │ │ ├── error.service.ts │ │ ├── logger.service.ts │ │ ├── location.service.ts │ │ ├── organizationalunit.service.ts │ │ ├── report.service.ts │ │ ├── invoice.service.ts │ │ ├── stockitem.service.ts │ │ └── promotion.service.ts │ ├── index.html │ ├── theme │ │ └── variables.scss │ └── service-worker.js └── server │ ├── index.ts │ ├── tsconfig.json │ ├── orm │ ├── auditmessage.ts │ ├── errormessage.ts │ ├── location.ts │ ├── organizationalunit.ts │ ├── reportconfiguration.ts │ ├── invoicepromo.ts │ ├── invoiceitem.ts │ ├── stockitemvendor.ts │ ├── promoitem.ts │ ├── invoice.ts │ ├── promotion.ts │ └── stockitem.ts │ ├── knexfile.ts │ ├── routes │ ├── index.ts │ ├── audit.ts │ ├── error.ts │ ├── system.ts │ ├── _logging.ts │ ├── location.ts │ ├── organizationalunit.ts │ └── inventory.ts │ ├── _settings.ts │ ├── validator.ts │ ├── logger.ts │ └── server.ts ├── resources ├── icon.png ├── splash.png ├── ios │ ├── icon │ │ ├── icon.png │ │ ├── icon-40.png │ │ ├── icon-50.png │ │ ├── icon-60.png │ │ ├── icon-72.png │ │ ├── icon-76.png │ │ ├── icon@2x.png │ │ ├── icon-40@2x.png │ │ ├── icon-50@2x.png │ │ ├── icon-60@2x.png │ │ ├── icon-60@3x.png │ │ ├── icon-72@2x.png │ │ ├── icon-76@2x.png │ │ ├── icon-small.png │ │ ├── icon-small@2x.png │ │ └── icon-small@3x.png │ └── splash │ │ ├── Default-667h.png │ │ ├── Default-736h.png │ │ ├── Default~iphone.png │ │ ├── Default@2x~iphone.png │ │ ├── Default-568h@2x~iphone.png │ │ ├── Default-Landscape-736h.png │ │ ├── Default-Landscape~ipad.png │ │ ├── Default-Portrait~ipad.png │ │ ├── Default-Portrait@2x~ipad.png │ │ └── Default-Landscape@2x~ipad.png └── android │ ├── icon │ ├── drawable-hdpi-icon.png │ ├── drawable-ldpi-icon.png │ ├── drawable-mdpi-icon.png │ ├── drawable-xhdpi-icon.png │ ├── drawable-xxhdpi-icon.png │ └── drawable-xxxhdpi-icon.png │ └── splash │ ├── drawable-land-hdpi-screen.png │ ├── drawable-land-ldpi-screen.png │ ├── drawable-land-mdpi-screen.png │ ├── drawable-port-hdpi-screen.png │ ├── drawable-port-ldpi-screen.png │ ├── drawable-port-mdpi-screen.png │ ├── drawable-land-xhdpi-screen.png │ ├── drawable-land-xxhdpi-screen.png │ ├── drawable-land-xxxhdpi-screen.png │ ├── drawable-port-xhdpi-screen.png │ ├── drawable-port-xxhdpi-screen.png │ └── drawable-port-xxxhdpi-screen.png ├── ionic.config.json ├── typings.json ├── .editorconfig ├── .gitignore ├── .travis.yml ├── tsconfig.json ├── seeds ├── dev │ ├── location.js │ ├── organizationalunit.js │ └── stockitem.js └── prod │ ├── location.js │ └── organizationalunit.js ├── webpack.config.js ├── README.md ├── config.xml ├── setup └── win │ └── install.bat ├── electron.js ├── hooks └── after_prepare │ └── 010_add_platform_class.js └── tslint.json /src/client/assets/empty.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/client/pages/home/home.scss: -------------------------------------------------------------------------------- 1 | my-page-home { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /src/client/pages/settings/audit/audit.scss: -------------------------------------------------------------------------------- 1 | my-page-audit { 2 | } 3 | -------------------------------------------------------------------------------- /src/client/pages/inventory/inventory.scss: -------------------------------------------------------------------------------- 1 | my-page-inventory { 2 | } 3 | -------------------------------------------------------------------------------- /src/client/pages/invoices/invoices.scss: -------------------------------------------------------------------------------- 1 | my-page-invoices { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /src/client/pages/settings/settings.scss: -------------------------------------------------------------------------------- 1 | my-page-settings { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /src/client/pages/inventory/ivmanage/ivmanage.scss: -------------------------------------------------------------------------------- 1 | [my-modal-ivmanage] { 2 | } 3 | -------------------------------------------------------------------------------- /src/client/pages/promotions/promotions.scss: -------------------------------------------------------------------------------- 1 | my-page-promotions { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /resources/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seiyria/posys/HEAD/resources/icon.png -------------------------------------------------------------------------------- /resources/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seiyria/posys/HEAD/resources/splash.png -------------------------------------------------------------------------------- /src/server/index.ts: -------------------------------------------------------------------------------- 1 | import { start, setup } from './server'; 2 | 3 | setup(); 4 | start(); 5 | -------------------------------------------------------------------------------- /resources/ios/icon/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seiyria/posys/HEAD/resources/ios/icon/icon.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seiyria/posys/HEAD/resources/ios/icon/icon-40.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seiyria/posys/HEAD/resources/ios/icon/icon-50.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seiyria/posys/HEAD/resources/ios/icon/icon-60.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seiyria/posys/HEAD/resources/ios/icon/icon-72.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seiyria/posys/HEAD/resources/ios/icon/icon-76.png -------------------------------------------------------------------------------- /resources/ios/icon/icon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seiyria/posys/HEAD/resources/ios/icon/icon@2x.png -------------------------------------------------------------------------------- /ionic.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "posys", 3 | "app_id": "", 4 | "v2": true, 5 | "typescript": true 6 | } 7 | -------------------------------------------------------------------------------- /resources/ios/icon/icon-40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seiyria/posys/HEAD/resources/ios/icon/icon-40@2x.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-50@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seiyria/posys/HEAD/resources/ios/icon/icon-50@2x.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seiyria/posys/HEAD/resources/ios/icon/icon-60@2x.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seiyria/posys/HEAD/resources/ios/icon/icon-60@3x.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-72@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seiyria/posys/HEAD/resources/ios/icon/icon-72@2x.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seiyria/posys/HEAD/resources/ios/icon/icon-76@2x.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seiyria/posys/HEAD/resources/ios/icon/icon-small.png -------------------------------------------------------------------------------- /src/client/assets/icon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seiyria/posys/HEAD/src/client/assets/icon/favicon.ico -------------------------------------------------------------------------------- /resources/ios/icon/icon-small@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seiyria/posys/HEAD/resources/ios/icon/icon-small@2x.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-small@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seiyria/posys/HEAD/resources/ios/icon/icon-small@3x.png -------------------------------------------------------------------------------- /resources/ios/splash/Default-667h.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seiyria/posys/HEAD/resources/ios/splash/Default-667h.png -------------------------------------------------------------------------------- /resources/ios/splash/Default-736h.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seiyria/posys/HEAD/resources/ios/splash/Default-736h.png -------------------------------------------------------------------------------- /src/client/assets/icon/favicon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seiyria/posys/HEAD/src/client/assets/icon/favicon.icns -------------------------------------------------------------------------------- /resources/ios/splash/Default~iphone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seiyria/posys/HEAD/resources/ios/splash/Default~iphone.png -------------------------------------------------------------------------------- /resources/ios/splash/Default@2x~iphone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seiyria/posys/HEAD/resources/ios/splash/Default@2x~iphone.png -------------------------------------------------------------------------------- /resources/android/icon/drawable-hdpi-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seiyria/posys/HEAD/resources/android/icon/drawable-hdpi-icon.png -------------------------------------------------------------------------------- /resources/android/icon/drawable-ldpi-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seiyria/posys/HEAD/resources/android/icon/drawable-ldpi-icon.png -------------------------------------------------------------------------------- /resources/android/icon/drawable-mdpi-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seiyria/posys/HEAD/resources/android/icon/drawable-mdpi-icon.png -------------------------------------------------------------------------------- /resources/android/icon/drawable-xhdpi-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seiyria/posys/HEAD/resources/android/icon/drawable-xhdpi-icon.png -------------------------------------------------------------------------------- /resources/android/icon/drawable-xxhdpi-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seiyria/posys/HEAD/resources/android/icon/drawable-xxhdpi-icon.png -------------------------------------------------------------------------------- /resources/ios/splash/Default-568h@2x~iphone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seiyria/posys/HEAD/resources/ios/splash/Default-568h@2x~iphone.png -------------------------------------------------------------------------------- /resources/ios/splash/Default-Landscape-736h.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seiyria/posys/HEAD/resources/ios/splash/Default-Landscape-736h.png -------------------------------------------------------------------------------- /resources/ios/splash/Default-Landscape~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seiyria/posys/HEAD/resources/ios/splash/Default-Landscape~ipad.png -------------------------------------------------------------------------------- /resources/ios/splash/Default-Portrait~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seiyria/posys/HEAD/resources/ios/splash/Default-Portrait~ipad.png -------------------------------------------------------------------------------- /resources/android/icon/drawable-xxxhdpi-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seiyria/posys/HEAD/resources/android/icon/drawable-xxxhdpi-icon.png -------------------------------------------------------------------------------- /resources/ios/splash/Default-Portrait@2x~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seiyria/posys/HEAD/resources/ios/splash/Default-Portrait@2x~ipad.png -------------------------------------------------------------------------------- /resources/ios/splash/Default-Landscape@2x~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seiyria/posys/HEAD/resources/ios/splash/Default-Landscape@2x~ipad.png -------------------------------------------------------------------------------- /resources/android/splash/drawable-land-hdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seiyria/posys/HEAD/resources/android/splash/drawable-land-hdpi-screen.png -------------------------------------------------------------------------------- /resources/android/splash/drawable-land-ldpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seiyria/posys/HEAD/resources/android/splash/drawable-land-ldpi-screen.png -------------------------------------------------------------------------------- /resources/android/splash/drawable-land-mdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seiyria/posys/HEAD/resources/android/splash/drawable-land-mdpi-screen.png -------------------------------------------------------------------------------- /resources/android/splash/drawable-port-hdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seiyria/posys/HEAD/resources/android/splash/drawable-port-hdpi-screen.png -------------------------------------------------------------------------------- /resources/android/splash/drawable-port-ldpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seiyria/posys/HEAD/resources/android/splash/drawable-port-ldpi-screen.png -------------------------------------------------------------------------------- /resources/android/splash/drawable-port-mdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seiyria/posys/HEAD/resources/android/splash/drawable-port-mdpi-screen.png -------------------------------------------------------------------------------- /resources/android/splash/drawable-land-xhdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seiyria/posys/HEAD/resources/android/splash/drawable-land-xhdpi-screen.png -------------------------------------------------------------------------------- /resources/android/splash/drawable-land-xxhdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seiyria/posys/HEAD/resources/android/splash/drawable-land-xxhdpi-screen.png -------------------------------------------------------------------------------- /resources/android/splash/drawable-land-xxxhdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seiyria/posys/HEAD/resources/android/splash/drawable-land-xxxhdpi-screen.png -------------------------------------------------------------------------------- /resources/android/splash/drawable-port-xhdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seiyria/posys/HEAD/resources/android/splash/drawable-port-xhdpi-screen.png -------------------------------------------------------------------------------- /resources/android/splash/drawable-port-xxhdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seiyria/posys/HEAD/resources/android/splash/drawable-port-xxhdpi-screen.png -------------------------------------------------------------------------------- /resources/android/splash/drawable-port-xxxhdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seiyria/posys/HEAD/resources/android/splash/drawable-port-xxxhdpi-screen.png -------------------------------------------------------------------------------- /src/client/pages/promotions/management/promotions.management.scss: -------------------------------------------------------------------------------- 1 | [my-modal-promotion-management] { 2 | 3 | [scroll-grid] { 4 | height: 20% !important; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/client/models/pageditems.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Pagination } from 'ionic2-pagination'; 3 | 4 | export class PagedItems { 5 | items: T[]; 6 | pagination: Pagination; 7 | } 8 | -------------------------------------------------------------------------------- /typings.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": {}, 3 | "devDependencies": {}, 4 | "globalDependencies": { 5 | "es6-shim": "registry:dt/es6-shim#0.31.2+20160602141504" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/client/app/main.ts: -------------------------------------------------------------------------------- 1 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 2 | 3 | import { AppModule } from './app.module'; 4 | 5 | platformBrowserDynamic().bootstrapModule(AppModule); 6 | -------------------------------------------------------------------------------- /src/client/pages/inventory/quick/quick.scss: -------------------------------------------------------------------------------- 1 | [my-modal-quick] { 2 | [scroll-grid] { 3 | height: 100%; 4 | 5 | [scroll-row] { 6 | margin-top: 20px; 7 | overflow: auto; 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | # Unix-style newlines with a newline ending every file 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true -------------------------------------------------------------------------------- /src/client/pages/invoices/view/invoice.view.scss: -------------------------------------------------------------------------------- 1 | [my-modal-invoice-view] { 2 | [top-row] { 3 | flex: 1; 4 | } 5 | 6 | [info-row] { 7 | flex: 4; 8 | } 9 | 10 | [bottom-row] { 11 | flex: 5; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/client/pages/inventory/oumanage/ou.management.scss: -------------------------------------------------------------------------------- 1 | [my-modal-ou-management] { 2 | [scroll-grid] { 3 | height: 100%; 4 | 5 | [scroll-row] { 6 | margin-top: 20px; 7 | overflow: auto; 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/client/pages/settings/error/error.scss: -------------------------------------------------------------------------------- 1 | .stack-trace-alert { 2 | 3 | .alert-wrapper { 4 | max-width: 90%; 5 | 6 | .alert-sub-title { 7 | white-space: pre-wrap; 8 | } 9 | } 10 | } 11 | 12 | my-page-error { 13 | } 14 | -------------------------------------------------------------------------------- /src/client/pages/settings/locationmanage/location.management.scss: -------------------------------------------------------------------------------- 1 | [my-modal-location-management] { 2 | [scroll-grid] { 3 | height: 100%; 4 | 5 | [scroll-row] { 6 | margin-top: 20px; 7 | overflow: auto; 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "experimentalDecorators": true, 4 | "module": "commonjs", 5 | "target": "es6", 6 | "outDir": "../../www", 7 | "types": ["node"], 8 | "allowJs": true 9 | }, 10 | "exclude": [ 11 | "*.json" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /src/client/models/location.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as _ from 'lodash'; 3 | 4 | export class Location { 5 | id?: number; 6 | name: string; 7 | 8 | constructor(initializer?: Location) { 9 | _.assign(this, initializer); 10 | 11 | if(this.name) { 12 | this.name = this.name.trim(); 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/client/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Posys", 3 | "short_name": "Posys", 4 | "start_url": "index.html", 5 | "display": "standalone", 6 | "icons": [{ 7 | "src": "assets/imgs/logo.png", 8 | "sizes": "512x512", 9 | "type": "image/png" 10 | }], 11 | "background_color": "#4e8ef7", 12 | "theme_color": "#4e8ef7" 13 | } -------------------------------------------------------------------------------- /src/client/pipes/truncate.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as _ from 'lodash'; 3 | 4 | import { PipeTransform, Pipe } from '@angular/core'; 5 | 6 | @Pipe({ 7 | name: 'truncate' 8 | }) 9 | export class TruncatePipe implements PipeTransform { 10 | transform(value: any, maxLength = 30): string { 11 | return _.truncate(value, { length: maxLength }); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/client/components/add-button.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Component } from '@angular/core'; 3 | 4 | @Component({ 5 | selector: 'add-button', 6 | template: ` 7 | 11 | ` 12 | }) 13 | export class AddButtonComponent { 14 | 15 | constructor() {} 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/client/components/update-button.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Component } from '@angular/core'; 3 | 4 | @Component({ 5 | selector: 'update-button', 6 | template: ` 7 | 11 | ` 12 | }) 13 | export class UpdateButtonComponent { 14 | 15 | constructor() {} 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/client/models/organizationalunit.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as _ from 'lodash'; 3 | 4 | export class OrganizationalUnit { 5 | id?: number; 6 | name: string; 7 | description?: string; 8 | 9 | constructor(initializer?: OrganizationalUnit) { 10 | _.assign(this, initializer); 11 | 12 | if(this.name) { 13 | this.name = this.name.trim(); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/client/components/edit-button.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Component } from '@angular/core'; 3 | 4 | @Component({ 5 | selector: 'edit-button', 6 | template: ` 7 | 11 | ` 12 | }) 13 | export class EditButtonComponent { 14 | 15 | constructor() {} 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/client/components/view-button.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Component } from '@angular/core'; 3 | 4 | @Component({ 5 | selector: 'view-button', 6 | template: ` 7 | 11 | ` 12 | }) 13 | export class ViewButtonComponent { 14 | 15 | constructor() {} 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/client/components/remove-button.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Component } from '@angular/core'; 3 | 4 | @Component({ 5 | selector: 'remove-button', 6 | template: ` 7 | 11 | ` 12 | }) 13 | export class RemoveButtonComponent { 14 | 15 | constructor() {} 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/client/components/reset-button.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Component } from '@angular/core'; 3 | 4 | @Component({ 5 | selector: 'reset-button', 6 | template: ` 7 | 11 | ` 12 | }) 13 | export class ResetButtonComponent { 14 | 15 | constructor() {} 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/client/components/manage-button.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Component } from '@angular/core'; 3 | 4 | @Component({ 5 | selector: 'manage-button', 6 | template: ` 7 | 11 | ` 12 | }) 13 | export class ManageButtonComponent { 14 | 15 | constructor() {} 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/client/components/refresh-button.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Component } from '@angular/core'; 3 | 4 | @Component({ 5 | selector: 'refresh-button', 6 | template: ` 7 | 11 | ` 12 | }) 13 | export class RefreshButtonComponent { 14 | 15 | constructor() {} 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/client/pages/pointofsale/cashpay/pointofsale.cashpay.scss: -------------------------------------------------------------------------------- 1 | [my-modal-pointofsale-cashpay] { 2 | ion-grid { 3 | height: 100%; 4 | 5 | ion-row:nth-child(2) { 6 | height: 100%; 7 | padding: 2rem; 8 | 9 | ion-col { 10 | height: 25%; 11 | 12 | button { 13 | height: 100%; 14 | font-size: 250%; 15 | } 16 | } 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Specifies files to intentionally ignore when using Git 2 | # http://git-scm.com/docs/gitignore 3 | 4 | node_modules/ 5 | www/build/ 6 | build/ 7 | platforms/ 8 | plugins/ 9 | *.swp 10 | .DS_Store 11 | Thumbs.db 12 | release 13 | cache 14 | visual-inventory-* 15 | output 16 | *.log 17 | www 18 | .tmp 19 | .idea 20 | *.metadata.json 21 | *.ngfactory.ts 22 | *.js.map 23 | src/**/*.js 24 | server.config.json 25 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | addons: 4 | apt: 5 | sources: 6 | - ubuntu-toolchain-r-test 7 | packages: 8 | - gcc-4.8 9 | - g++-4.8 10 | - libcups2-dev 11 | - cups 12 | - cups-ppdc 13 | 14 | compiler: 15 | - gcc 16 | 17 | install: 18 | - if [ "$CXX" = "g++" ]; then export CXX="g++-4.8" CC="gcc-4.8"; fi 19 | - npm install 20 | 21 | node_js: 22 | - "stable" 23 | - "6.5" 24 | -------------------------------------------------------------------------------- /src/client/components/import-button.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Component, Input } from '@angular/core'; 3 | 4 | @Component({ 5 | selector: 'import-button', 6 | template: ` 7 | 11 | ` 12 | }) 13 | export class ImportButtonComponent { 14 | 15 | @Input() disabled: boolean; 16 | 17 | constructor() {} 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/client/components/export-button.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Component, Input } from '@angular/core'; 3 | 4 | @Component({ 5 | selector: 'export-button', 6 | template: ` 7 | 11 | ` 12 | }) 13 | export class ExportButtonComponent { 14 | 15 | @Input() disabled: boolean; 16 | 17 | constructor() {} 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/client/models/stockitemvendor.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as _ from 'lodash'; 3 | 4 | export class StockItemVendor { 5 | id?: number; 6 | stockitemId?: number; 7 | 8 | name: string; 9 | stockId: string; 10 | cost: number; 11 | isPreferred: boolean; 12 | 13 | constructor(initializer?: StockItemVendor) { 14 | _.assign(this, initializer); 15 | 16 | if(this.name) { 17 | this.name = this.name.trim(); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/client/components/confirm-button.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Component, Input } from '@angular/core'; 3 | 4 | @Component({ 5 | selector: 'confirm-button', 6 | template: ` 7 | 11 | ` 12 | }) 13 | export class ConfirmButtonComponent { 14 | 15 | @Input() disabled: boolean; 16 | 17 | constructor() {} 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/server/orm/auditmessage.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:only-arrow-functions no-invalid-this */ 2 | 3 | import { bookshelf } from '../server'; 4 | import { Location } from './location'; 5 | 6 | export const AuditMessage = bookshelf.Model.extend({ 7 | tableName: 'auditmessage', 8 | hasTimestamps: true, 9 | softDelete: false, 10 | location: function() { 11 | return this.belongsTo(Location, 'locationId'); 12 | }, 13 | validations: { 14 | } 15 | }); 16 | -------------------------------------------------------------------------------- /src/server/orm/errormessage.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:only-arrow-functions no-invalid-this */ 2 | 3 | import { bookshelf } from '../server'; 4 | import { Location } from './location'; 5 | 6 | export const ErrorMessage = bookshelf.Model.extend({ 7 | tableName: 'errormessage', 8 | hasTimestamps: true, 9 | softDelete: false, 10 | location: function() { 11 | return this.belongsTo(Location, 'locationId'); 12 | }, 13 | validations: { 14 | } 15 | }); 16 | -------------------------------------------------------------------------------- /src/client/components/print-button.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Component, Input } from '@angular/core'; 3 | 4 | @Component({ 5 | selector: 'print-button', 6 | template: ` 7 | 11 | ` 12 | }) 13 | export class PrintButtonComponent { 14 | 15 | @Input() disabled: boolean; 16 | 17 | constructor() {} 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/client/models/auditmessage.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as _ from 'lodash'; 3 | 4 | import { Location } from './location'; 5 | 6 | export class AuditMessage { 7 | id?: number; 8 | name: string; 9 | created_at: Date; 10 | module: string; 11 | message: string; 12 | locationId: number; 13 | terminalId: string; 14 | refObj: any; 15 | 16 | location: Location; 17 | 18 | constructor(initializer?: AuditMessage) { 19 | _.assign(this, initializer); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/client/pages/settings/about/about.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { ViewController } from 'ionic-angular'; 3 | 4 | declare const VERSION: string; 5 | 6 | @Component({ 7 | templateUrl: 'about.html' 8 | }) 9 | export class AboutComponent { 10 | 11 | constructor(public viewCtrl: ViewController) {} 12 | 13 | get version() { 14 | return VERSION; 15 | } 16 | 17 | dismiss() { 18 | this.viewCtrl.dismiss(); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/client/components/form-error.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Component, Input } from '@angular/core'; 3 | 4 | @Component({ 5 | selector: 'form-error', 6 | template: ` 7 | {{ errorObj[key][0] }} 8 | `, 9 | styles: [` 10 | ion-note { 11 | color: #f53d3d !important; 12 | } 13 | ` 14 | ] 15 | }) 16 | export class FormErrorComponent { 17 | @Input() errorObj: any; 18 | @Input() key: string; 19 | 20 | constructor() {} 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/client/models/errormessage.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as _ from 'lodash'; 3 | 4 | import { Location } from './location'; 5 | 6 | type FoundAt = 'Client' | 'Server'; 7 | 8 | export class ErrorMessage { 9 | id?: number; 10 | foundAt: FoundAt; 11 | message: string; 12 | stack: string; 13 | created_at?: Date; 14 | 15 | locationId?: number; 16 | terminalId?: string; 17 | 18 | location?: Location; 19 | 20 | constructor(initializer?: ErrorMessage) { 21 | _.assign(this, initializer); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/server/orm/location.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:only-arrow-functions no-invalid-this */ 2 | 3 | import { bookshelf } from '../server'; 4 | 5 | export const Location = bookshelf.Model.extend({ 6 | tableName: 'location', 7 | hasTimestamps: true, 8 | softDelete: false, 9 | validations: { 10 | name: [ 11 | { 12 | method: 'isLength', 13 | args: { min: 1, max: 50 }, 14 | error: 'Your Location name must be between 1 and 50 characters.' 15 | } 16 | ] 17 | } 18 | }); 19 | -------------------------------------------------------------------------------- /src/client/components/void-button.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Component, Input } from '@angular/core'; 3 | 4 | import { Invoice } from '../models/invoice'; 5 | 6 | @Component({ 7 | selector: 'void-button', 8 | template: ` 9 | 13 | ` 14 | }) 15 | export class VoidButtonComponent { 16 | 17 | @Input() item: Invoice; 18 | 19 | constructor() {} 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/server/orm/organizationalunit.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:only-arrow-functions no-invalid-this */ 2 | 3 | import { bookshelf } from '../server'; 4 | 5 | export const OrganizationalUnit = bookshelf.Model.extend({ 6 | tableName: 'organizationalunit', 7 | hasTimestamps: true, 8 | softDelete: false, 9 | validations: { 10 | name: [ 11 | { 12 | method: 'isLength', 13 | args: { min: 1, max: 50 }, 14 | error: 'Your OU name must be between 1 and 50 characters.' 15 | } 16 | ] 17 | } 18 | }); 19 | -------------------------------------------------------------------------------- /src/server/orm/reportconfiguration.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:only-arrow-functions no-invalid-this */ 2 | 3 | import { bookshelf } from '../server'; 4 | 5 | export const ReportConfiguration = bookshelf.Model.extend({ 6 | tableName: 'reportconfiguration', 7 | hasTimestamps: true, 8 | softDelete: true, 9 | validations: { 10 | name: [ 11 | { 12 | method: 'isLength', 13 | args: { min: 1, max: 50 }, 14 | error: 'Your report name must be between 1 and 50 characters.' 15 | } 16 | ] 17 | } 18 | }); 19 | -------------------------------------------------------------------------------- /src/client/components/resume-button.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Component, Input } from '@angular/core'; 3 | 4 | @Component({ 5 | selector: 'resume-transaction-button', 6 | template: ` 7 | 11 | ` 12 | }) 13 | export class ResumeTransactionButtonComponent { 14 | 15 | @Input() disabled: boolean; 16 | @Input() isReturn: boolean; 17 | 18 | constructor() {} 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/server/orm/invoicepromo.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:only-arrow-functions no-invalid-this */ 2 | 3 | import { bookshelf } from '../server'; 4 | import { Invoice } from './invoice'; 5 | import { Promotion } from './promotion'; 6 | 7 | export const InvoicePromo = bookshelf.Model.extend({ 8 | tableName: 'invoicepromo', 9 | hasTimestamps: true, 10 | softDelete: false, 11 | promotion: function() { 12 | return this.belongsTo(Invoice); 13 | }, 14 | _promoData: function() { 15 | return this.belongsTo(Promotion, 'promoId'); 16 | }, 17 | validations: { 18 | } 19 | }); 20 | -------------------------------------------------------------------------------- /src/server/orm/invoiceitem.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:only-arrow-functions no-invalid-this */ 2 | 3 | import { bookshelf } from '../server'; 4 | import { Invoice } from './invoice'; 5 | import { StockItem } from './stockitem'; 6 | 7 | export const InvoiceItem = bookshelf.Model.extend({ 8 | tableName: 'invoiceitem', 9 | hasTimestamps: true, 10 | softDelete: false, 11 | promotion: function() { 12 | return this.belongsTo(Invoice); 13 | }, 14 | _stockitemData: function() { 15 | return this.belongsTo(StockItem, 'stockitemId'); 16 | }, 17 | validations: { 18 | } 19 | }); 20 | -------------------------------------------------------------------------------- /src/client/pipes/currency-from-settings.ts: -------------------------------------------------------------------------------- 1 | 2 | import { PipeTransform, Pipe } from '@angular/core'; 3 | import { CurrencyPipe } from '@angular/common'; 4 | 5 | import { ApplicationSettingsService } from '../services/settings.service'; 6 | 7 | @Pipe({ 8 | name: 'currencyFromSettings' 9 | }) 10 | export class CurrencyFromSettingsPipe implements PipeTransform { 11 | constructor(public currency: CurrencyPipe, public settings: ApplicationSettingsService) {} 12 | 13 | transform(value: any): string { 14 | return this.currency.transform(value, this.settings.currencyCode, true); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/server/knexfile.ts: -------------------------------------------------------------------------------- 1 | 2 | const appRoot = require('app-root-path'); 3 | const config = require(`${appRoot}/server.config.json`); 4 | const isElectron = process.execPath.toLowerCase().search('electron') !== -1; 5 | 6 | const env = isElectron ? 'prod' : 'dev'; 7 | 8 | module.exports = { 9 | client: 'pg', 10 | connection: { 11 | host: config.db.hostname, 12 | user: config.db.username, 13 | password: config.db.password, 14 | database: config.db.database 15 | }, 16 | migrations: { 17 | directory: `${appRoot}/migrations` 18 | }, 19 | seeds: { 20 | directory: `${appRoot}/seeds/${env}` 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /src/client/pages/reporting/reporting.scss: -------------------------------------------------------------------------------- 1 | my-page-reporting { 2 | [report-container] { 3 | display: flex; 4 | flex-direction: column; 5 | 6 | [header-row] { 7 | flex: 1; 8 | } 9 | 10 | [filter-row] { 11 | flex: 1; 12 | } 13 | 14 | [options-row] { 15 | flex: 7; 16 | display: flex; 17 | flex-direction: row; 18 | } 19 | } 20 | 21 | [scroll-list-container] { 22 | display: flex; 23 | flex-direction: column; 24 | 25 | [scroll-list-header] { 26 | flex: 1; 27 | } 28 | 29 | [scroll-list] { 30 | flex: 6; 31 | overflow: auto; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/server/orm/stockitemvendor.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:only-arrow-functions no-invalid-this */ 2 | 3 | import { bookshelf } from '../server'; 4 | 5 | import { StockItem } from './stockitem'; 6 | 7 | export const StockItemVendor = bookshelf.Model.extend({ 8 | tableName: 'stockitemvendor', 9 | hasTimestamps: false, 10 | softDelete: false, 11 | stockitem: function() { 12 | return this.belongsTo(StockItem, 'stockitemId'); 13 | }, 14 | validations: { 15 | name: [ 16 | { 17 | method: 'isLength', 18 | args: { min: 1, max: 50 }, 19 | error: 'Your OU name must be between 1 and 50 characters.' 20 | } 21 | ] 22 | } 23 | }); 24 | -------------------------------------------------------------------------------- /src/server/routes/index.ts: -------------------------------------------------------------------------------- 1 | 2 | import stockitem from './stockitem'; 3 | import organizationalunit from './organizationalunit'; 4 | import promotion from './promotion'; 5 | import invoice from './invoice'; 6 | import inventory from './inventory'; 7 | import location from './location'; 8 | import system from './system'; 9 | import report from './report'; 10 | import audit from './audit'; 11 | import error from './error'; 12 | 13 | export default (app) => { 14 | stockitem(app); 15 | organizationalunit(app); 16 | promotion(app); 17 | invoice(app); 18 | inventory(app); 19 | location(app); 20 | system(app); 21 | report(app); 22 | audit(app); 23 | error(app); 24 | }; 25 | -------------------------------------------------------------------------------- /src/server/_settings.ts: -------------------------------------------------------------------------------- 1 | 2 | const fs = require('fs'); 3 | const appRoot = require('app-root-path'); 4 | 5 | export default { 6 | pagination: { 7 | pageSize: 50 8 | }, 9 | search: { 10 | pageSize: 50 11 | } 12 | }; 13 | 14 | export const readSettings = (callback) => { 15 | fs.readFile(`${appRoot}/server.config.json`, 'utf8', (err, data) => { 16 | if(err) { throw err; } 17 | callback(JSON.parse(data)); 18 | }); 19 | }; 20 | 21 | export const writeSettings = (data, callback?) => { 22 | fs.writeFile(`${appRoot}/server.config.json`, JSON.stringify(data, null, 4), (err) => { 23 | if(err) { throw err; } 24 | if(callback) { 25 | callback(); 26 | } 27 | }); 28 | }; 29 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "declaration": false, 5 | "emitDecoratorMetadata": true, 6 | "experimentalDecorators": true, 7 | "lib": [ 8 | "dom", 9 | "es2015" 10 | ], 11 | "module": "commonjs", 12 | "moduleResolution": "node", 13 | "sourceMap": true, 14 | "target": "es5", 15 | "types": ["node"] 16 | }, 17 | "compileOnSave": false, 18 | "buildOnSave": false, 19 | "exclude": [ 20 | "node_modules", 21 | "platforms", 22 | "hooks", 23 | "migrations", 24 | "plugins", 25 | "resources", 26 | "seeds", 27 | "www", 28 | "src/client/app/main.prod.ts" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /seeds/dev/location.js: -------------------------------------------------------------------------------- 1 | 2 | const locations = [ 3 | { name: 'Home Base' } 4 | ]; 5 | 6 | const createLocation = (knex, ou) => { 7 | return knex.table('location') 8 | .returning('id') 9 | .insert(ou); 10 | }; 11 | 12 | 13 | exports.seed = (knex) => { 14 | return knex('location') 15 | .count() 16 | .then(countArray => { 17 | const count = countArray[0].count; 18 | if(count > 0) { 19 | console.log('[Seed] Skipping location, count(*) > 0.'); 20 | return; 21 | } 22 | return knex('location') 23 | // .del() 24 | .then(() => { 25 | return Promise.all(locations.map(location => createLocation(knex, location))); 26 | }); 27 | }); 28 | }; 29 | -------------------------------------------------------------------------------- /seeds/prod/location.js: -------------------------------------------------------------------------------- 1 | 2 | const locations = [ 3 | { name: 'Home Base' } 4 | ]; 5 | 6 | const createLocation = (knex, ou) => { 7 | return knex.table('location') 8 | .returning('id') 9 | .insert(ou); 10 | }; 11 | 12 | 13 | exports.seed = (knex) => { 14 | return knex('location') 15 | .count() 16 | .then(countArray => { 17 | const count = countArray[0].count; 18 | if(count > 0) { 19 | console.log('[Seed] Skipping location, count(*) > 0.'); 20 | return; 21 | } 22 | return knex('location') 23 | // .del() 24 | .then(() => { 25 | return Promise.all(locations.map(location => createLocation(knex, location))); 26 | }); 27 | }); 28 | }; 29 | -------------------------------------------------------------------------------- /src/client/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { Platform } from 'ionic-angular'; 3 | import { StatusBar, Splashscreen } from 'ionic-native'; 4 | 5 | import { HomePageComponent } from '../pages/home/home'; 6 | 7 | @Component({ 8 | template: `` 9 | }) 10 | export class MyAppComponent { 11 | rootPage = HomePageComponent; 12 | 13 | constructor(platform: Platform) { 14 | platform.ready().then(() => { 15 | // Okay, so the platform is ready and our plugins are available. 16 | // Here you can do any higher level native things you might need. 17 | // StatusBar.styleDefault(); 18 | // Splashscreen.hide(); 19 | }); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/client/models/promoitem.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as _ from 'lodash'; 3 | 4 | import { StockItem } from './stockitem'; 5 | 6 | export class PromoItem { 7 | id?: number; 8 | sku: string; 9 | name: string; 10 | description?: string; 11 | stockitemId?: number; 12 | stockitem?: StockItem; 13 | temporary?: boolean; 14 | 15 | constructor(initializer?: PromoItem|StockItem|any) { 16 | if(!initializer) { return; } 17 | this.id = initializer.id; 18 | this.sku = initializer.sku; 19 | this.name = initializer.name; 20 | this.description = initializer.description; 21 | this.stockitemId = initializer.stockitemId; 22 | this.stockitem = initializer.stockitem; 23 | this.temporary = initializer.temporary; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /seeds/prod/organizationalunit.js: -------------------------------------------------------------------------------- 1 | 2 | const ous = [ 3 | { name: 'Unspecified', description: '' } 4 | ]; 5 | 6 | const createOU = (knex, ou) => { 7 | return knex.table('organizationalunit') 8 | .returning('id') 9 | .insert(ou); 10 | }; 11 | 12 | 13 | exports.seed = (knex) => { 14 | return knex('organizationalunit') 15 | .count() 16 | .then(countArray => { 17 | const count = countArray[0].count; 18 | if(count > 0) { 19 | console.log('[Seed] Skipping organizationalunit, count(*) > 0.'); 20 | return; 21 | } 22 | return knex('organizationalunit') 23 | // .del() 24 | .then(() => { 25 | return Promise.all(ous.map(ou => createOU(knex, ou))); 26 | }); 27 | }); 28 | }; 29 | -------------------------------------------------------------------------------- /src/client/models/invoicepromo.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as _ from 'lodash'; 3 | 4 | import { Promotion } from './promotion'; 5 | 6 | export class InvoicePromo { 7 | id?: number; 8 | invoiceId?: number; 9 | promoId?: number; 10 | promoData?: Promotion; 11 | cost?: number; 12 | skus?: string[]; 13 | 14 | realData?: any; 15 | 16 | applyId?: string; 17 | 18 | constructor(initializer?: InvoicePromo) { 19 | if(!initializer) { return; } 20 | this.id = initializer.id; 21 | this.invoiceId = initializer.invoiceId; 22 | this.promoId = initializer.promoId; 23 | this.promoData = initializer.promoData; 24 | this.cost = initializer.cost; 25 | this.skus = initializer.skus; 26 | this.applyId = initializer.applyId; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/server/validator.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as _ from 'lodash'; 3 | 4 | export default { 5 | isRequired: (x) => !_.isUndefined(x), 6 | isNum: (x, { min, max } = { min: undefined, max: undefined }) => { 7 | const test = +x; 8 | if(_.isNaN(+test)) { return false; } 9 | if(min && test < min) { return false; } 10 | if(max && test > max) { return false; } 11 | return true; 12 | }, 13 | isLength: (x, { min, max } = { min: undefined, max: undefined }) => { 14 | const test = '' + x; 15 | if(!test) { return false; } 16 | if(min && test.length < min) { return false; } 17 | if(max && test.length > max) { return false; } 18 | return true; 19 | }, 20 | isIn: (x, { arr } = { arr: [] }) => { 21 | return _.includes(arr, x); 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /seeds/dev/organizationalunit.js: -------------------------------------------------------------------------------- 1 | 2 | const ous = [ 3 | { name: 'Unspecified', description: '' }, 4 | { name: 'Diabetic Supplies', description: '' }, 5 | { name: 'Food', description: '' } 6 | ]; 7 | 8 | const createOU = (knex, ou) => { 9 | return knex.table('organizationalunit') 10 | .returning('id') 11 | .insert(ou); 12 | }; 13 | 14 | 15 | exports.seed = (knex) => { 16 | return knex('organizationalunit') 17 | .count() 18 | .then(countArray => { 19 | const count = countArray[0].count; 20 | if(count > 0) { 21 | console.log('[Seed] Skipping organizationalunit, count(*) > 0.'); 22 | return; 23 | } 24 | return knex('organizationalunit') 25 | // .del() 26 | .then(() => { 27 | return Promise.all(ous.map(ou => createOU(knex, ou))); 28 | }); 29 | }); 30 | }; 31 | -------------------------------------------------------------------------------- /src/client/models/invoiceitem.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as _ from 'lodash'; 3 | 4 | import { StockItem } from './stockitem'; 5 | 6 | export class InvoiceItem { 7 | id?: number; 8 | invoiceId?: number; 9 | stockitemId?: number; 10 | stockitemData?: StockItem; 11 | 12 | taxable?: boolean; 13 | quantity?: number; 14 | cost?: number; 15 | 16 | realData?: any; 17 | promoApplyId?: string; 18 | 19 | constructor(initializer?: InvoiceItem) { 20 | if(!initializer) { return; } 21 | this.id = initializer.id; 22 | this.invoiceId = initializer.invoiceId; 23 | this.stockitemId = initializer.stockitemId; 24 | this.stockitemData = initializer.stockitemData; 25 | this.cost = initializer.cost; 26 | this.taxable = initializer.taxable; 27 | this.quantity = initializer.quantity; 28 | this.promoApplyId = initializer.promoApplyId; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/client/pages/pointofsale/pointofsale.scss: -------------------------------------------------------------------------------- 1 | my-page-pointofsale { 2 | 3 | [search-area] { 4 | height: 60px; 5 | } 6 | 7 | [body-area] { 8 | height: calc(100% - 100px - 56px - 16px - 5px - 5px - 60px); 9 | 10 | [header] ion-col { 11 | font-weight: bold; 12 | } 13 | 14 | [can-scroll] { 15 | overflow: auto; 16 | height: 100%; 17 | 18 | &[has-header] { 19 | height: calc(100% - 44px); 20 | } 21 | } 22 | } 23 | 24 | [total-area] { 25 | height: 100px; 26 | } 27 | 28 | [tax-cost-header] { 29 | display: flex; 30 | align-content: space-between; 31 | } 32 | 33 | [tax-cost-entry] { 34 | .item-inner { 35 | padding-right: 0 !important; 36 | } 37 | } 38 | 39 | #transaction-list > .item > .item-inner > .input-wrapper > .label { 40 | margin-right: 0 !important; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/server/logger.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as _ from 'lodash'; 3 | 4 | export class Logger { 5 | 6 | static parseDatabaseError(err: any, category: string): string { 7 | if(_.includes(err.message, 'violates foreign key constraint')) { 8 | return `Items that depend on that ${category} still exist. Removal aborted.`; 9 | } 10 | 11 | if(_.includes(err.message, 'duplicate key value violates unique constraint')) { 12 | return `An item with a duplicate chunk of unique data exists. Aborted.`; 13 | } 14 | 15 | return 'Unknown error.'; 16 | } 17 | 18 | static browserError(err: string): any { 19 | return { message: err }; 20 | } 21 | 22 | static error(tag: string, msg: string|Error): string { 23 | const sentMessage = new Error(`[${tag}] ${msg}`); 24 | console.error(sentMessage); 25 | return sentMessage.message; 26 | } 27 | 28 | static info(tag: string, msg: string): void { 29 | console.log(`[${tag}] ${msg}`); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/client/pages/settings/audit/audititem.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Component, Input } from '@angular/core'; 3 | 4 | import { AuditMessage } from '../../../models/auditmessage'; 5 | 6 | @Component({ 7 | selector: 'audit-item', 8 | template: ` 9 | 10 | 11 | 12 | {{ item.created_at | date:'medium' }} 13 | 14 | 15 | {{ item.message }} 16 | 17 | 18 | {{ item.module }} 19 | 20 | 21 | {{ item.location.name }} 22 | 23 | 24 | {{ item.terminalId }} 25 | 26 | 27 | 28 | `, 29 | }) 30 | export class AuditItemComponent { 31 | @Input() item: AuditMessage; 32 | } 33 | -------------------------------------------------------------------------------- /src/client/services/override/error.custom.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash'; 2 | 3 | import { Injectable } from '@angular/core'; 4 | import { ErrorHandler } from '@angular/core'; 5 | import { IonicErrorHandler } from 'ionic-angular'; 6 | import { ErrorService } from '../error.service'; 7 | 8 | import { ErrorMessage } from '../../models/errormessage'; 9 | 10 | @Injectable() 11 | export class CustomErrorHandler extends IonicErrorHandler implements ErrorHandler { 12 | 13 | constructor(public errorService: ErrorService) { super(); } 14 | 15 | handleError(err: any): void { 16 | 17 | super.handleError(err); 18 | 19 | const messageObject = new ErrorMessage({ 20 | message: err.message, 21 | stack: err.originalError ? err.originalError.stack : err.stack, 22 | foundAt: 'Client' 23 | }); 24 | 25 | if(_.includes(messageObject.message, 'ProgressEvent')) { 26 | return; 27 | } 28 | 29 | this.errorService 30 | .addClientError(messageObject) 31 | .toPromise(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/client/models/invoice.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as _ from 'lodash'; 3 | 4 | import { InvoiceItem } from './invoiceitem'; 5 | import { InvoicePromo } from './invoicepromo'; 6 | import { Location } from './location'; 7 | 8 | export type PurchaseMethod = 9 | 'Cash' | 'Check' | 'Debit' | 'Credit' | 'Custom' | 'Return' | 'Void'; 10 | 11 | export class Invoice { 12 | id?: number; 13 | purchaseTime?: Date; 14 | purchaseMethod?: PurchaseMethod; 15 | purchasePrice?: number; 16 | cashGiven?: number; 17 | taxCollected?: number; 18 | subtotal?: number; 19 | isVoided?: boolean; 20 | isReturned?: boolean; 21 | isOnHold?: boolean; 22 | 23 | locationId?: number; 24 | terminalId?: string; 25 | location?: Location; 26 | 27 | previousId?: number; 28 | 29 | invoiceReferenceId?: number; 30 | invoiceReference?: Invoice; 31 | invoices?: Invoice[]; 32 | 33 | stockitems?: InvoiceItem[]; 34 | promotions?: InvoicePromo[]; 35 | 36 | constructor(initializer?: Invoice) { 37 | _.extend(this, initializer); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/client/models/stockitem.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as _ from 'lodash'; 3 | 4 | import { OrganizationalUnit } from './organizationalunit'; 5 | import { StockItemVendor } from './stockitemvendor'; 6 | 7 | export class StockItem { 8 | id?: number; 9 | sku: string; 10 | name: string; 11 | photoUrl?: string; 12 | description?: string; 13 | organizationalunitId?: number; 14 | organizationalunit?: OrganizationalUnit; 15 | taxable: boolean; 16 | cost: number; 17 | quantity: number; 18 | reorderThreshold?: number; 19 | reorderUpToAmount?: number; 20 | lastSoldAt?: Date; 21 | 22 | vendors?: StockItemVendor[]; 23 | 24 | temporary?: boolean; 25 | promoApplyId?: string; 26 | 27 | constructor(initializer?: StockItem) { 28 | _.assign(this, initializer); 29 | 30 | if(this.name) { 31 | this.name = this.name.trim(); 32 | } 33 | 34 | if(this.description) { 35 | this.description = this.description.trim(); 36 | } 37 | 38 | if(!this.vendors) { 39 | this.vendors = []; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/client/pages/settings/settings.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { ModalController, NavController } from 'ionic-angular'; 3 | 4 | import { AboutComponent } from './about/about'; 5 | import { EditSettingsComponent } from './edit/editsettings'; 6 | import { AuditPageComponent } from './audit/audit'; 7 | import { ErrorPageComponent } from './error/error'; 8 | 9 | @Component({ 10 | selector: 'my-page-settings', 11 | templateUrl: 'settings.html' 12 | }) 13 | export class SettingsPageComponent { 14 | 15 | constructor(public modalCtrl: ModalController, public navCtrl: NavController) {} 16 | 17 | openEdit() { 18 | let modal = this.modalCtrl.create(EditSettingsComponent, { enableBackdropDismiss: false }); 19 | modal.present(); 20 | } 21 | 22 | openAbout() { 23 | let modal = this.modalCtrl.create(AboutComponent, { enableBackdropDismiss: false }); 24 | modal.present(); 25 | } 26 | 27 | openAuditLog() { 28 | this.navCtrl.push(AuditPageComponent); 29 | } 30 | 31 | openErrorLog() { 32 | this.navCtrl.push(ErrorPageComponent); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/client/services/audit.service.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as _ from 'lodash'; 3 | 4 | import { PagedItems } from '../models/pageditems'; 5 | import { AuditMessage } from '../models/auditmessage'; 6 | 7 | import { LoggerService } from './logger.service'; 8 | import { ApplicationSettingsService } from './settings.service'; 9 | 10 | import { Injectable } from '@angular/core'; 11 | import { Response } from '@angular/http'; 12 | import { HttpClient } from './override/http.custom'; 13 | import { Observable } from 'rxjs/Rx'; 14 | 15 | @Injectable() 16 | export class AuditService { 17 | 18 | private url = 'auditmessage'; 19 | 20 | constructor(private http: HttpClient, 21 | private logger: LoggerService, 22 | private settings: ApplicationSettingsService) {} 23 | 24 | getMany(args: any): Observable> { 25 | return this.http.get(this.settings.buildAPIURL(this.url), { search: this.settings.buildSearchParams(args) }) 26 | .map((res: Response) => this.logger.observableUnwrap(res.json())) 27 | .catch(e => this.logger.observableError(e)); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/client/pages/settings/about/about.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | About Posys 5 | 6 | 7 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | Bug Tracker 19 | 20 | 21 | 22 | Contact the Developers 23 | 24 | 25 | 26 | License 27 | GPL 3.0 28 | 29 | 30 | 31 | Build Information 32 | {{ version }} 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/client/pages/pointofsale/cashpay/pointofsale.cashpay.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Cash Pay 5 | 6 | 7 | 8 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | Cash Tendered 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/server/routes/audit.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:only-arrow-functions no-invalid-this */ 2 | 3 | import { AuditMessage } from '../orm/auditmessage'; 4 | 5 | import { Logger } from '../logger'; 6 | import Settings from '../_settings'; 7 | 8 | export default (app) => { 9 | app.get('/auditmessage', (req, res) => { 10 | 11 | const pageOpts = { 12 | pageSize: +req.query.pageSize || Settings.pagination.pageSize, 13 | page: +req.query.page || 1, 14 | withRelated: ['location'] 15 | }; 16 | 17 | AuditMessage 18 | .forge() 19 | .query(qb => { 20 | if(req.query.module) { 21 | qb.andWhere('module', '=', req.query.module); 22 | } 23 | 24 | if(req.query.location) { 25 | qb.andWhere('locationId', '=', +req.query.location); 26 | } 27 | }) 28 | .orderBy('-id') 29 | .fetchPage(pageOpts) 30 | .then(collection => { 31 | res.json({ items: collection.toJSON(), pagination: collection.pagination }); 32 | }) 33 | .catch(e => { 34 | res.status(500).json(Logger.browserError(Logger.error('Route:AuditMessage:GET', e))); 35 | }); 36 | }); 37 | }; 38 | -------------------------------------------------------------------------------- /src/client/pages/settings/settings.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Posys System 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/client/services/inventory.service.ts: -------------------------------------------------------------------------------- 1 | 2 | import { StockItem } from '../models/stockitem'; 3 | 4 | import { LoggerService } from './logger.service'; 5 | import { ApplicationSettingsService } from './settings.service'; 6 | 7 | import { Injectable } from '@angular/core'; 8 | import { Response } from '@angular/http'; 9 | import { HttpClient } from './override/http.custom'; 10 | import { Observable } from 'rxjs/Rx'; 11 | 12 | @Injectable() 13 | export class InventoryService { 14 | 15 | private url = 'inventory'; 16 | 17 | constructor(private http: HttpClient, 18 | private logger: LoggerService, 19 | private settings: ApplicationSettingsService) {} 20 | 21 | export(columns: string[]): Observable { 22 | return this.http.post(this.settings.buildAPIURL(`${this.url}/export`), { columns }) 23 | .map((res: Response) => this.logger.observableUnwrap(res.json())) 24 | .catch(e => this.logger.observableError(e)); 25 | } 26 | 27 | import(items: StockItem[]): Observable { 28 | return this.http.post(this.settings.buildAPIURL(`${this.url}/import`), items) 29 | .map((res: Response) => this.logger.observableUnwrap(res.json())) 30 | .catch(e => this.logger.observableError(e)); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/client/models/promotion.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as _ from 'lodash'; 3 | 4 | import { OrganizationalUnit } from './organizationalunit'; 5 | import { PromoItem } from './promoitem'; 6 | import { InvoicePromo } from './invoicepromo'; 7 | 8 | export type DiscountType = 9 | 'Dollar' | 'Percent'; 10 | 11 | export type ItemReductionType = 12 | 'BuyXGetNext' | 'All' | 'SetTo'; 13 | 14 | export type DiscountGrouping = 15 | 'SKU' | 'Category' 16 | 17 | export class Promotion { 18 | id?: number; 19 | name: string; 20 | description?: string; 21 | discountType: DiscountType; 22 | itemReductionType: ItemReductionType; 23 | discountGrouping: DiscountGrouping; 24 | 25 | discountValue: number; 26 | numItemsRequired: number; 27 | 28 | startDate?: string; 29 | endDate?: string; 30 | 31 | organizationalunitId?: number; 32 | organizationalunit?: OrganizationalUnit; 33 | 34 | promoItems?: PromoItem[]; 35 | invoicePromos?: InvoicePromo[]; 36 | 37 | temporary?: boolean; 38 | 39 | constructor(initializer?: Promotion) { 40 | _.assign(this, initializer); 41 | 42 | if(this.name) { 43 | this.name = this.name.trim(); 44 | } 45 | 46 | if(this.description) { 47 | this.description = this.description.trim(); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/client/components/update-quantity-button.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Component, Input, Output, EventEmitter } from '@angular/core'; 3 | 4 | import { AlertController } from 'ionic-angular'; 5 | 6 | @Component({ 7 | selector: 'update-quantity-button', 8 | template: ` 9 | 12 | ` 13 | }) 14 | export class UpdateQuantityButtonComponent { 15 | 16 | @Input() quantity: number; 17 | @Input() disabled: boolean; 18 | @Output() quantityChange = new EventEmitter(); 19 | 20 | constructor(public alertCtrl: AlertController) {} 21 | 22 | updateQuantity() { 23 | let alert = this.alertCtrl.create({ 24 | title: 'Update Quantity', 25 | inputs: [ 26 | { 27 | name: 'quantity', 28 | type: 'number', 29 | placeholder: 'New Quantity', 30 | value: '' + this.quantity 31 | } 32 | ], 33 | buttons: [ 34 | { 35 | text: 'Cancel' 36 | }, 37 | { 38 | text: 'Confirm', 39 | handler: (data) => { 40 | this.quantityChange.emit(+data.quantity); 41 | } 42 | } 43 | ] 44 | }); 45 | alert.present(); 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/server/orm/promoitem.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:only-arrow-functions no-invalid-this */ 2 | 3 | import { bookshelf } from '../server'; 4 | import { Promotion } from './promotion'; 5 | 6 | export const PromoItem = bookshelf.Model.extend({ 7 | tableName: 'promoitem', 8 | hasTimestamps: true, 9 | softDelete: false, 10 | promotion: function() { 11 | return this.belongsTo(Promotion); 12 | }, 13 | validations: { 14 | name: [ 15 | { 16 | method: 'isLength', 17 | args: { min: 1, max: 50 }, 18 | error: 'Your item name must be between 1 and 50 characters.' 19 | } 20 | ], 21 | sku: [ 22 | { 23 | method: 'isLength', 24 | args: { min: 1, max: 50 }, 25 | error: 'Your SKU must be between 1 and 50 characters.' 26 | } 27 | ], 28 | description: [ 29 | { 30 | method: 'isLength', 31 | args: { min: 1, max: 500 }, 32 | error: 'Your description must be between 1 and 500 characters.' 33 | } 34 | ], 35 | stockitemId: [ 36 | { 37 | method: 'isNum', 38 | error: 'You must specify a valid item.' 39 | } 40 | ], 41 | organizationalunitId: [ 42 | { 43 | method: 'isNum', 44 | error: 'You must specify a valid OU id.' 45 | } 46 | ] 47 | } 48 | }); 49 | -------------------------------------------------------------------------------- /src/client/services/error.service.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as _ from 'lodash'; 3 | 4 | import { PagedItems } from '../models/pageditems'; 5 | import { ErrorMessage } from '../models/errormessage'; 6 | 7 | import { LoggerService } from './logger.service'; 8 | import { ApplicationSettingsService } from './settings.service'; 9 | 10 | import { Injectable } from '@angular/core'; 11 | import { Response } from '@angular/http'; 12 | import { HttpClient } from './override/http.custom'; 13 | import { Observable } from 'rxjs/Rx'; 14 | 15 | @Injectable() 16 | export class ErrorService { 17 | 18 | private url = 'errormessage'; 19 | 20 | constructor(private http: HttpClient, 21 | private logger: LoggerService, 22 | private settings: ApplicationSettingsService) {} 23 | 24 | getMany(args: any): Observable> { 25 | return this.http.get(this.settings.buildAPIURL(this.url), { search: this.settings.buildSearchParams(args) }) 26 | .map((res: Response) => this.logger.observableUnwrap(res.json())) 27 | .catch(e => this.logger.observableError(e)); 28 | } 29 | 30 | addClientError(item: ErrorMessage): Observable { 31 | return this.http.put(this.settings.buildAPIURL(this.url), item) 32 | .map((res: Response) => this.logger.observableUnwrap(res.json())) 33 | .catch(e => this.logger.observableError(e)); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Posys 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/server/routes/error.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:only-arrow-functions no-invalid-this */ 2 | 3 | import { ErrorMessage } from '../orm/errormessage'; 4 | 5 | import { Logger } from '../logger'; 6 | import Settings from '../_settings'; 7 | 8 | import { recordErrorMessageFromClient, recordErrorMessageFromServer, MESSAGE_CATEGORIES } from './_logging'; 9 | 10 | export default (app) => { 11 | app.get('/errormessage', (req, res) => { 12 | 13 | const pageOpts = { 14 | pageSize: +req.query.pageSize || Settings.pagination.pageSize, 15 | page: +req.query.page || 1, 16 | withRelated: ['location'] 17 | }; 18 | 19 | ErrorMessage 20 | .forge() 21 | .query(qb => { 22 | if(req.query.module) { 23 | qb.andWhere('module', '=', req.query.module); 24 | } 25 | 26 | if(req.query.location) { 27 | qb.andWhere('locationId', '=', +req.query.location); 28 | } 29 | }) 30 | .orderBy('-id') 31 | .fetchPage(pageOpts) 32 | .then(collection => { 33 | res.json({ items: collection.toJSON(), pagination: collection.pagination }); 34 | }) 35 | .catch(e => { 36 | recordErrorMessageFromServer(req, MESSAGE_CATEGORIES.SYSTEM, e); 37 | res.status(500).json(Logger.browserError(Logger.error('Route:ErrorMessage:GET', e))); 38 | }); 39 | }); 40 | 41 | app.put('/errormessage', (req) => { 42 | recordErrorMessageFromClient(req, req.body); 43 | }); 44 | }; 45 | -------------------------------------------------------------------------------- /src/client/pages/inventory/inventoryitem.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Component, Input, Output, EventEmitter } from '@angular/core'; 3 | 4 | import { StockItem } from '../../models/stockitem'; 5 | 6 | @Component({ 7 | selector: 'inventory-item', 8 | template: ` 9 | 10 | 11 | 12 | 13 | {{ item.name | truncate:50 }} 14 | 15 | 16 | {{ item.description | truncate:50 }} 17 | 18 | 19 | 20 | {{ item.organizationalunit.name | truncate:15 }} 21 | 22 | 23 | {{ item.sku }} 24 | 25 | 26 | {{ item.quantity }} 27 | 28 | 29 | {{ item.cost | currencyFromSettings }} 30 | 31 | 32 | 33 | 34 | 35 | 36 | `, 37 | }) 38 | export class InventoryItemComponent { 39 | @Input() item: StockItem; 40 | @Output() edit = new EventEmitter(); 41 | } 42 | -------------------------------------------------------------------------------- /src/server/orm/invoice.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:only-arrow-functions no-invalid-this */ 2 | 3 | import { bookshelf } from '../server'; 4 | 5 | import { InvoiceItem } from './invoiceitem'; 6 | import { InvoicePromo } from './invoicepromo'; 7 | import { Location } from './location'; 8 | 9 | export const Invoice = bookshelf.Model.extend({ 10 | tableName: 'invoice', 11 | hasTimestamps: true, 12 | softDelete: true, 13 | stockitems: function() { 14 | return this.hasMany(InvoiceItem, 'invoiceId'); 15 | }, 16 | promotions: function() { 17 | return this.hasMany(InvoicePromo, 'invoiceId'); 18 | }, 19 | invoices: function() { 20 | return this.hasMany(Invoice, 'invoiceReferenceId'); 21 | }, 22 | location: function() { 23 | return this.belongsTo(Location, 'locationId'); 24 | }, 25 | validations: { 26 | purchaseTime: [ 27 | { 28 | method: 'isRequired', 29 | error: 'Invoice purchase time required.' 30 | } 31 | ], 32 | purchaseMethod: [ 33 | { 34 | method: 'isRequired', 35 | error: 'Invoice purchase method required.' 36 | }, 37 | { 38 | method: 'isIn', 39 | args: { arr: ['Cash', 'Credit', 'Debit', 'Check', 'Custom', 'Return', 'Hold', 'Void'] }, 40 | error: 'Invalid invoice purchase method specified.' 41 | } 42 | ], 43 | purchasePrice: [ 44 | { 45 | method: 'isRequired', 46 | error: 'Invoice purchase price required.' 47 | } 48 | ] 49 | } 50 | }); 51 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | var ionicWebpackFactory = require(process.env.IONIC_WEBPACK_FACTORY); 4 | var GitRevisionPlugin = require('git-revision-webpack-plugin'); 5 | 6 | var gitRevisionPlugin = new GitRevisionPlugin(); 7 | 8 | module.exports = { 9 | entry: process.env.IONIC_APP_ENTRY_POINT, 10 | output: { 11 | path: '{{BUILD}}', 12 | filename: process.env.IONIC_OUTPUT_JS_FILE_NAME, 13 | devtoolModuleFilenameTemplate: ionicWebpackFactory.getSourceMapperFunction(), 14 | }, 15 | devtool: process.env.IONIC_GENERATE_SOURCE_MAP ? process.env.IONIC_SOURCE_MAP_TYPE : '', 16 | target: 'electron', 17 | 18 | resolve: { 19 | extensions: ['.ts', '.js', '.json'], 20 | modules: [path.resolve('node_modules')] 21 | }, 22 | 23 | module: { 24 | loaders: [ 25 | { 26 | test: /\.json$/, 27 | loader: 'json-loader' 28 | }, 29 | { 30 | //test: /\.(ts|ngfactory.js)$/, 31 | test: /\.ts$/, 32 | loader: process.env.IONIC_WEBPACK_LOADER 33 | } 34 | ] 35 | }, 36 | 37 | plugins: [ 38 | ionicWebpackFactory.getIonicEnvironmentPlugin(), 39 | new webpack.DefinePlugin({ 40 | 'VERSION': JSON.stringify(gitRevisionPlugin.version()) 41 | }) 42 | ], 43 | 44 | // Some libraries import Node modules but don't use them in the browser. 45 | // Tell Webpack to provide empty mocks for them so importing them works. 46 | node: { 47 | fs: 'empty', 48 | net: 'empty', 49 | tls: 'empty' 50 | } 51 | }; 52 | -------------------------------------------------------------------------------- /src/server/routes/system.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as _ from 'lodash'; 3 | import { readSettings, writeSettings } from '../_settings'; 4 | import { recordAuditMessage, MESSAGE_CATEGORIES } from './_logging'; 5 | 6 | let nodePrinter = null; 7 | try { 8 | nodePrinter = require('printer'); 9 | } catch(e) { 10 | console.error('Could not load node-printer.'); 11 | } 12 | 13 | export default (app) => { 14 | app.get('/system', (req, res) => { 15 | readSettings(data => res.json(data)); 16 | }); 17 | 18 | app.get('/system/printers', (req, res) => { 19 | if(!nodePrinter) { 20 | return res.json([]); 21 | } 22 | res.json(nodePrinter.getPrinters()); 23 | }); 24 | 25 | app.patch('/system', (req, res) => { 26 | if(req.body.application) { 27 | req.body.application.taxRate = +req.body.application.taxRate; 28 | req.body.application.businessName = _.truncate(req.body.application.businessName, { length: 50, omission: '' }); 29 | req.body.application.locationName = +req.body.application.locationName; 30 | req.body.application.customBusinessCurrency = _.truncate(req.body.application.customBusinessCurrency, { length: 25, omission: '' }); 31 | } 32 | 33 | if(req.body.printer) { 34 | req.body.printer.characterWidth = +req.body.printer.characterWidth; 35 | } 36 | 37 | writeSettings(req.body, () => { 38 | recordAuditMessage(req, MESSAGE_CATEGORIES.SYSTEM, `System settings were updated.`, { settings: req.body }); 39 | res.json({ flash: 'Settings updated successfully.', data: req.body }); 40 | }); 41 | }); 42 | }; 43 | -------------------------------------------------------------------------------- /src/client/pages/settings/error/erroritem.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Component, Input } from '@angular/core'; 3 | import { AlertController } from 'ionic-angular'; 4 | 5 | import { ErrorMessage } from '../../../models/errormessage'; 6 | 7 | @Component({ 8 | selector: 'error-item', 9 | template: ` 10 | 11 | 12 | 13 | {{ item.created_at | date:'medium' }} 14 | 15 | 16 | 19 |   20 | {{ item.message | truncate:75 }} 21 | 22 | 23 | {{ item.module }} 24 | 25 | 26 | {{ item.location.name }} 27 | 28 | 29 | {{ item.terminalId }} 30 | 31 | 32 | 33 | `, 34 | }) 35 | export class ErrorItemComponent { 36 | @Input() item: ErrorMessage; 37 | 38 | constructor(private alertCtrl: AlertController) {} 39 | 40 | showStack() { 41 | let alert = this.alertCtrl.create({ 42 | title: 'Error Stack Trace', 43 | subTitle: this.item.stack, 44 | cssClass: 'stack-trace-alert', 45 | buttons: ['Close'] 46 | }); 47 | alert.present(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/client/pages/home/home.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | import { NavController } from 'ionic-angular'; 4 | 5 | import { LocalStorageService } from 'ng2-webstorage'; 6 | 7 | import { ApplicationSettingsService } from '../../services/settings.service'; 8 | 9 | import { PointOfSalePageComponent } from '../pointofsale/pointofsale'; 10 | import { PromotionsPageComponent } from '../promotions/promotions'; 11 | import { InventoryPageComponent } from '../inventory/inventory'; 12 | import { ReportingPageComponent } from '../reporting/reporting'; 13 | import { InvoicesPageComponent } from '../invoices/invoices'; 14 | import { SettingsPageComponent } from '../settings/settings'; 15 | 16 | @Component({ 17 | selector: 'my-page-home', 18 | templateUrl: 'home.html' 19 | }) 20 | export class HomePageComponent { 21 | 22 | constructor(public navCtrl: NavController, public settings: ApplicationSettingsService, public storage: LocalStorageService) {} 23 | 24 | isInvalidSetup() { 25 | return !this.settings.isValidConfiguration() || !this.storage.retrieve('terminalId'); 26 | } 27 | 28 | goToInventory() { 29 | this.navCtrl.push(InventoryPageComponent); 30 | } 31 | 32 | goToReporting() { 33 | this.navCtrl.push(ReportingPageComponent); 34 | } 35 | 36 | goToInvoices() { 37 | this.navCtrl.push(InvoicesPageComponent); 38 | } 39 | 40 | goToPromotions() { 41 | this.navCtrl.push(PromotionsPageComponent); 42 | } 43 | 44 | goToSettings() { 45 | this.navCtrl.push(SettingsPageComponent); 46 | } 47 | 48 | goToPointOfSale() { 49 | this.navCtrl.push(PointOfSalePageComponent); 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Posys [![Build Status](https://travis-ci.org/seiyria/posys.svg?branch=master)](https://travis-ci.org/seiyria/posys) 2 | 3 | A Point of Sale system for the modern age. 4 | 5 | ## Installation 6 | 7 | If installing on windows, you may need `--msvs_version=` to install the printer module correctly. It's really finnicky. 8 | 9 | * clone the repository 10 | * install postgresql 11 | * `npm install -g ionic` 12 | * `npm install` 13 | * create `src/server/server.config.json` (sample below) 14 | * `npm run migrate:latest` (set up the database) 15 | * `npm run seed:run` (get some sample data in the db to work with) 16 | * `npm start:server` (in one terminal) 17 | * `npm start` (in another terminal) 18 | 19 | ### Sample `server.config.json` 20 | ```json 21 | { 22 | "server": { 23 | "port": 8080 24 | }, 25 | "db": { 26 | "hostname": "localhost", 27 | "username": "postgres", 28 | "password": "postgres", 29 | "database": "posys" 30 | } 31 | } 32 | ``` 33 | 34 | Further setup is done at runtime. 35 | 36 | ## Building For Electron 37 | You should just be able to run `npm run build:osx:dev` or `npm run build:win:dev`. There is a task for dist/asar but there are currently some issues with it. 38 | 39 | ## Screenshots 40 | See a gallery on [imgur](http://imgur.com/a/LJ3Hl). 41 | 42 | ## Hardware Recommendations 43 | 44 | * A barcode scanner (this application makes heavy use of an "omni search" input which allows for text entry even when not focused, to facilitate quick lookup of items - a barcode scanner is used in testing and makes the interface significantly easier to use) 45 | * A receipt printer (STAR or Epson-compatible) 46 | -------------------------------------------------------------------------------- /src/client/services/logger.service.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as _ from 'lodash'; 3 | 4 | import { Injectable } from '@angular/core'; 5 | import { Response } from '@angular/http'; 6 | import { Observable } from 'rxjs/Rx'; 7 | 8 | import { ToastController } from 'ionic-angular'; 9 | 10 | @Injectable() 11 | export class LoggerService { 12 | 13 | constructor(public toastCtrl: ToastController) {} 14 | 15 | error(e: Error|Response): void { 16 | 17 | let errMsg: string; 18 | 19 | if (e instanceof Response) { 20 | 21 | // swallow errors trying to hit while api is rebooting 22 | if(e.status === 0) { 23 | return; 24 | } 25 | 26 | const body = e.json() || ''; 27 | const err = body.error || JSON.stringify(body); 28 | errMsg = `${e.status} - ${e.statusText || ''} ${err}`; 29 | } else { 30 | errMsg = e.message ? e.message : e.toString(); 31 | } 32 | 33 | console.error(errMsg); 34 | } 35 | 36 | observableError(e: Error|Response) { 37 | this.error(e); 38 | let returnedValue = e instanceof Response ? e.json() : e; 39 | 40 | if(returnedValue.flash) { 41 | this.doFlash(returnedValue.flash); 42 | } 43 | 44 | return Observable.throw(returnedValue); 45 | } 46 | 47 | observableUnwrap(e: any) { 48 | if(e.flash) { 49 | this.doFlash(e.flash); 50 | return e.data; 51 | } 52 | 53 | if(!_.isUndefined(e.data)) { 54 | return e.data; 55 | } 56 | 57 | return e; 58 | } 59 | 60 | doFlash(message: string) { 61 | this.toastCtrl.create({ 62 | message: message, 63 | duration: 5000, 64 | position: 'top', 65 | showCloseButton: true 66 | }).present(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/client/services/location.service.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Location } from '../models/location'; 3 | 4 | import { LoggerService } from './logger.service'; 5 | import { ApplicationSettingsService } from './settings.service'; 6 | 7 | import { Injectable } from '@angular/core'; 8 | import { Response } from '@angular/http'; 9 | import { HttpClient } from './override/http.custom'; 10 | import { Observable } from 'rxjs/Rx'; 11 | 12 | @Injectable() 13 | export class LocationService { 14 | 15 | private url = 'location'; 16 | 17 | constructor(private http: HttpClient, 18 | private logger: LoggerService, 19 | private settings: ApplicationSettingsService) {} 20 | 21 | getAll(): Observable { 22 | return this.http.get(this.settings.buildAPIURL(this.url)) 23 | .map((res: Response) => this.logger.observableUnwrap(res.json())) 24 | .catch(e => this.logger.observableError(e)); 25 | } 26 | 27 | create(item: Location): Observable { 28 | return this.http.put(this.settings.buildAPIURL(this.url), item) 29 | .map((res: Response) => this.logger.observableUnwrap(res.json())) 30 | .catch(e => this.logger.observableError(e)); 31 | } 32 | 33 | update(item: Location): Observable { 34 | return this.http.patch(this.settings.buildAPIURL(this.url, item.id), item) 35 | .map((res: Response) => this.logger.observableUnwrap(res.json())) 36 | .catch(e => this.logger.observableError(e)); 37 | } 38 | 39 | remove(item: Location) { 40 | return this.http.delete(this.settings.buildAPIURL(this.url, item.id)) 41 | .map((res: Response) => this.logger.observableUnwrap(res.json())) 42 | .catch(e => this.logger.observableError(e)); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/client/pages/home/home.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Posys - {{ settings.businessName || 'Please Finish Setup' }} 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/server/routes/_logging.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as _ from 'lodash'; 3 | 4 | import { AuditMessage } from '../orm/auditmessage'; 5 | import { ErrorMessage } from '../orm/errormessage'; 6 | 7 | export const MESSAGE_CATEGORIES = { 8 | INVOICE: 'Invoice', 9 | INVENTORY: 'Inventory', 10 | LOCATION: 'Location', 11 | OU: 'Category', 12 | PROMOTION: 'Promotion', 13 | REPORT: 'Report', 14 | STOCKITEM: 'StockItem', 15 | SYSTEM: 'System' 16 | }; 17 | 18 | export const recordAuditMessage = (req, module, message, refObject?) => { 19 | const insertRecord: any = { module, message, refObject }; 20 | insertRecord.locationId = +req.header('X-Location'); 21 | insertRecord.terminalId = req.header('X-Terminal'); 22 | 23 | if(_.isNaN(insertRecord.locationId)) { 24 | return; 25 | } 26 | 27 | AuditMessage 28 | .forge(insertRecord) 29 | .save(); 30 | }; 31 | 32 | export const recordErrorMessage = (req, module, message, stack, foundAt = 'Server') => { 33 | 34 | message = _.truncate(message, { length: 500, omission: '' }); 35 | 36 | const insertRecord: any = { module, message, stack, foundAt }; 37 | insertRecord.locationId = +req.header('X-Location'); 38 | insertRecord.terminalId = req.header('X-Terminal'); 39 | 40 | if(_.isNaN(insertRecord.locationId)) { 41 | return; 42 | } 43 | 44 | ErrorMessage 45 | .forge(insertRecord) 46 | .save(); 47 | }; 48 | 49 | export const recordErrorMessageFromClient = (req, errorMessage) => { 50 | recordErrorMessage(req, 'Unknown', errorMessage.message, errorMessage.stack, 'Client'); 51 | }; 52 | 53 | export const recordErrorMessageFromServer = (req, module, error) => { 54 | if(error.data) { return; } 55 | recordErrorMessage(req, module, error.message, error.stack, 'Server'); 56 | }; 57 | -------------------------------------------------------------------------------- /src/client/pages/inventory/ivmanage/ivmanage.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Mass Inventory Management 5 | 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | All items must have a Name, SKU, Cost > 0, Quantity >= 0. 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | {{ column.name }} 36 | 37 | 38 | 39 | Alias 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /src/client/pages/settings/audit/audit.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash'; 2 | 3 | import { Component, OnInit } from '@angular/core'; 4 | 5 | import { LocalStorage } from 'ng2-webstorage'; 6 | 7 | import { AuditMessage } from '../../../models/auditmessage'; 8 | import { Location } from '../../../models/location'; 9 | 10 | import { LocationService } from '../../../services/location.service'; 11 | import { AuditService } from '../../../services/audit.service'; 12 | import { Pagination } from 'ionic2-pagination'; 13 | 14 | @Component({ 15 | selector: 'my-page-audit', 16 | templateUrl: 'audit.html' 17 | }) 18 | export class AuditPageComponent implements OnInit { 19 | 20 | public currentAuditItems: AuditMessage[] = []; 21 | public paginationInfo: Pagination; 22 | 23 | @LocalStorage() 24 | public moduleFilter: string; 25 | 26 | @LocalStorage() 27 | public locationFilter: number; 28 | 29 | public locations: Location[] = []; 30 | public modules = ['Category', 'Inventory', 'Invoice', 'Location', 'Promotion', 'Report', 'StockItem', 'System']; 31 | 32 | constructor(public audService: AuditService, public locaService: LocationService) {} 33 | 34 | ngOnInit() { 35 | this.changePage(1); 36 | 37 | this.locaService 38 | .getAll() 39 | .toPromise() 40 | .then(locas => { 41 | this.locations = locas; 42 | }); 43 | } 44 | 45 | filterData() { 46 | this.changePage(1); 47 | } 48 | 49 | changePage(newPage) { 50 | this.audService 51 | .getMany({ page: newPage, location: this.locationFilter, module: this.moduleFilter }) 52 | .toPromise() 53 | .then(({ items, pagination }) => { 54 | this.currentAuditItems = items; 55 | this.paginationInfo = pagination; 56 | }); 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /src/client/pages/settings/error/error.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash'; 2 | 3 | import { Component, OnInit } from '@angular/core'; 4 | 5 | import { LocalStorage } from 'ng2-webstorage'; 6 | 7 | import { ErrorMessage } from '../../../models/errormessage'; 8 | import { Location } from '../../../models/location'; 9 | 10 | import { LocationService } from '../../../services/location.service'; 11 | import { ErrorService } from '../../../services/error.service'; 12 | import { Pagination } from 'ionic2-pagination'; 13 | 14 | @Component({ 15 | selector: 'my-page-error', 16 | templateUrl: 'error.html' 17 | }) 18 | export class ErrorPageComponent implements OnInit { 19 | 20 | public currentAuditItems: ErrorMessage[] = []; 21 | public paginationInfo: Pagination; 22 | 23 | @LocalStorage() 24 | public moduleFilter: string; 25 | 26 | @LocalStorage() 27 | public locationFilter: number; 28 | 29 | public locations: Location[] = []; 30 | public modules = ['Category', 'Inventory', 'Invoice', 'Location', 'Promotion', 'Report', 'StockItem', 'System']; 31 | 32 | constructor(public errService: ErrorService, public locaService: LocationService) {} 33 | 34 | ngOnInit() { 35 | this.changePage(1); 36 | 37 | this.locaService 38 | .getAll() 39 | .toPromise() 40 | .then(locas => { 41 | this.locations = locas; 42 | }); 43 | } 44 | 45 | filterData() { 46 | this.changePage(1); 47 | } 48 | 49 | changePage(newPage) { 50 | this.errService 51 | .getMany({ page: newPage, location: this.locationFilter, module: this.moduleFilter }) 52 | .toPromise() 53 | .then(({ items, pagination }) => { 54 | this.currentAuditItems = items; 55 | this.paginationInfo = pagination; 56 | }); 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /src/client/pages/inventory/quick/quick.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as _ from 'lodash'; 3 | 4 | import { Component } from '@angular/core'; 5 | import { ViewController } from 'ionic-angular'; 6 | 7 | import { StockItem } from '../../../models/stockitem'; 8 | 9 | import { StockItemService } from '../../../services/stockitem.service'; 10 | 11 | @Component({ 12 | templateUrl: 'quick.html' 13 | }) 14 | export class QuickComponent { 15 | 16 | public scanItems: StockItem[] = []; 17 | 18 | constructor(public viewCtrl: ViewController, 19 | public siService: StockItemService) {} 20 | 21 | handleSingleSearchResult(result: any) { 22 | this.handleSearchResults({ items: [result] }); 23 | } 24 | 25 | handleSearchResults(result: any) { 26 | if(result.items.length !== 1) { return; } 27 | const newItem = _.cloneDeep(result.items[0]); 28 | newItem.quantity = 1; 29 | this.scanItems.push(newItem); 30 | 31 | setTimeout(() => { 32 | const transactionList = document.getElementById('scan-list'); 33 | transactionList.scrollTop = transactionList.scrollHeight; 34 | }); 35 | } 36 | 37 | removeItem(item: StockItem) { 38 | this.scanItems = _.reject(this.scanItems, i => i === item); 39 | } 40 | 41 | importItems() { 42 | this.siService 43 | .importMany(this.scanItems) 44 | .toPromise() 45 | .then(() => { 46 | this.dismiss(); 47 | }); 48 | } 49 | 50 | exportItems() { 51 | this.siService 52 | .exportMany(this.scanItems) 53 | .toPromise() 54 | .then(() => { 55 | this.dismiss(); 56 | }); 57 | } 58 | 59 | updateQuantity(newQuantity, item) { 60 | item.quantity = newQuantity; 61 | } 62 | 63 | dismiss() { 64 | this.viewCtrl.dismiss(); 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /src/client/components/top-icon-button.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Component, Input, Output, EventEmitter } from '@angular/core'; 3 | 4 | type iconSize = 'large' | 'medium'; 5 | 6 | @Component({ 7 | selector: 'top-icon-button', 8 | template: ` 9 | 27 | `, 28 | styles: [` 29 | button[top-icon] { 30 | white-space: pre; 31 | min-height: 200px; 32 | } 33 | 34 | button[top-icon].medium-size { 35 | min-height: 150px; 36 | } 37 | 38 | button[top-icon] .icon-large .icon { 39 | font-size: 70px; 40 | padding-top: 30px; 41 | padding-bottom: 30px; 42 | } 43 | 44 | button[top-icon] .icon-medium .icon { 45 | font-size: 50px; 46 | padding-top: 15px; 47 | padding-bottom: 15px; 48 | } 49 | 50 | button[top-icon] .full-width { 51 | width: 100%; 52 | padding: 10px; 53 | white-space: normal; 54 | } 55 | ` 56 | ] 57 | }) 58 | export class TopIconButtonComponent { 59 | @Input() text: string = ''; 60 | @Input() icon: string = ''; 61 | @Input() size: iconSize = 'large'; 62 | @Input() disabled: boolean = false; 63 | @Output() subClick = new EventEmitter(); 64 | 65 | constructor() {} 66 | 67 | } 68 | -------------------------------------------------------------------------------- /src/client/pages/invoices/invoiceitem.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Component, Input, Output, EventEmitter } from '@angular/core'; 3 | 4 | import { ApplicationSettingsService } from '../../services/settings.service'; 5 | import { Invoice } from '../../models/invoice'; 6 | 7 | @Component({ 8 | selector: 'invoice-item', 9 | template: ` 10 | 11 | 12 | 13 | {{ item.id }} 14 | 15 | 16 | {{ item.purchaseTime | date:'medium' }} 17 | 18 | 19 | {{ settings.invoiceMethodDisplay(item.purchaseMethod) | truncate:10 }} 20 | 21 | 22 | {{ invoiceStatus(item) }} 23 | 24 | 25 | {{ item.stockitems.length }} 26 | 27 | 28 | {{ item.purchasePrice | currencyFromSettings }} 29 | 30 | 31 | 32 | 33 | 34 | 35 | `, 36 | }) 37 | export class InvoiceItemComponent { 38 | @Input() item: Invoice; 39 | @Output() view = new EventEmitter(); 40 | 41 | constructor(public settings: ApplicationSettingsService) {} 42 | 43 | invoiceStatus(invoice: Invoice) { 44 | if(invoice.isOnHold) { return 'On Hold'; } 45 | if(invoice.isReturned) { return 'Returned'; } 46 | if(invoice.isVoided) { return 'Voided'; } 47 | return 'Completed'; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/client/services/organizationalunit.service.ts: -------------------------------------------------------------------------------- 1 | 2 | import { OrganizationalUnit } from '../models/organizationalunit'; 3 | 4 | import { LoggerService } from './logger.service'; 5 | import { ApplicationSettingsService } from './settings.service'; 6 | 7 | import { Injectable } from '@angular/core'; 8 | import { Response } from '@angular/http'; 9 | import { HttpClient } from './override/http.custom'; 10 | import { Observable } from 'rxjs/Rx'; 11 | 12 | @Injectable() 13 | export class OrganizationalUnitService { 14 | 15 | private url = 'organizationalunit'; 16 | 17 | constructor(private http: HttpClient, 18 | private logger: LoggerService, 19 | private settings: ApplicationSettingsService) {} 20 | 21 | getAll(): Observable { 22 | return this.http.get(this.settings.buildAPIURL(this.url)) 23 | .map((res: Response) => this.logger.observableUnwrap(res.json())) 24 | .catch(e => this.logger.observableError(e)); 25 | } 26 | 27 | create(item: OrganizationalUnit): Observable { 28 | return this.http.put(this.settings.buildAPIURL(this.url), item) 29 | .map((res: Response) => this.logger.observableUnwrap(res.json())) 30 | .catch(e => this.logger.observableError(e)); 31 | } 32 | 33 | update(item: OrganizationalUnit): Observable { 34 | return this.http.patch(this.settings.buildAPIURL(this.url, item.id), item) 35 | .map((res: Response) => this.logger.observableUnwrap(res.json())) 36 | .catch(e => this.logger.observableError(e)); 37 | } 38 | 39 | remove(item: OrganizationalUnit) { 40 | return this.http.delete(this.settings.buildAPIURL(this.url, item.id)) 41 | .map((res: Response) => this.logger.observableUnwrap(res.json())) 42 | .catch(e => this.logger.observableError(e)); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | posys 4 | A PoS System. 5 | Kyle Kemp 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/client/models/reportconfiguration.ts: -------------------------------------------------------------------------------- 1 | 2 | export type ReportRoute = 3 | 'base/inventory/current' 4 | | 'base/inventory/old' 5 | | 'base/inventory/reorder' 6 | | 'base/promotions/all' 7 | | 'base/promotions/pos' 8 | | 'base/sales/completed' 9 | | 'base/sales/voided'; 10 | 11 | export class LimitedReportConfiguration { 12 | id?: number; 13 | name: string; 14 | basedOn?: number; 15 | 16 | startDate?: string; 17 | endDate?: string; 18 | datePeriod?: number; 19 | dateDenomination?: string; 20 | 21 | columnOrder?: string[]; 22 | columnChecked?: string[]; 23 | 24 | options?: any[]; 25 | 26 | sortBy?: string; 27 | groupBy?: string; 28 | groupByDate?: string; 29 | 30 | ouFilter?: number; 31 | locationFilter?: number; 32 | 33 | constructor(initializer: LimitedReportConfiguration) { 34 | this.id = initializer.id; 35 | this.name = initializer.name; 36 | this.basedOn = initializer.basedOn; 37 | this.startDate = initializer.startDate; 38 | this.endDate = initializer.endDate; 39 | this.datePeriod = initializer.datePeriod; 40 | this.dateDenomination = initializer.dateDenomination; 41 | this.columnOrder = initializer.columnOrder; 42 | this.columnChecked = initializer.columnChecked; 43 | this.options = initializer.options; 44 | this.sortBy = initializer.sortBy; 45 | this.groupBy = initializer.groupBy; 46 | this.groupByDate = initializer.groupByDate; 47 | this.ouFilter = initializer.ouFilter; 48 | this.locationFilter = initializer.locationFilter; 49 | } 50 | } 51 | 52 | export class ReportConfiguration extends LimitedReportConfiguration { 53 | internalId?: number; 54 | reportRoute: ReportRoute; 55 | 56 | dateText?: string = 'Date Range'; 57 | 58 | columns: any[]; 59 | optionValues?: any; 60 | 61 | modifyColumns?: Function; 62 | modifyData?: Function; 63 | 64 | filters?: any; 65 | } 66 | -------------------------------------------------------------------------------- /src/client/models/settings.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as _ from 'lodash'; 3 | 4 | class Location { 5 | id: number; 6 | name: string; 7 | } 8 | 9 | type PrinterType = 'epson' | 'star'; 10 | 11 | class ApplicationSettings { 12 | businessName: string; 13 | locationName: number; 14 | location?: Location; 15 | terminalId: string; 16 | taxRate: number; 17 | currencyCode: string; 18 | customBusinessCurrency: string; 19 | } 20 | 21 | class PrinterSettings { 22 | name: string; 23 | type: PrinterType; 24 | characterWidth: number; 25 | header: string; 26 | footer: string; 27 | printMerchantReceipts: boolean; 28 | printReceiptBarcodes: boolean; 29 | } 30 | 31 | class DatabaseSettings { 32 | hostname: string; 33 | username: string; 34 | password: string; 35 | database: string; 36 | } 37 | 38 | class ServerSettings { 39 | port: number; 40 | } 41 | 42 | export class Settings { 43 | application: any; 44 | printer: any; 45 | db: any; 46 | server: any; 47 | 48 | constructor(initializer) { 49 | _.merge(this, initializer); 50 | if(!this.application) { this.application = new ApplicationSettings(); } 51 | if(!this.printer) { this.printer = new PrinterSettings(); } 52 | if(!this.db) { this.db = new DatabaseSettings(); } 53 | if(!this.server) { this.server = new ServerSettings(); } 54 | } 55 | 56 | get isValid(): boolean { 57 | const { application, db } = this; 58 | 59 | return !_.isUndefined(application.currencyCode) && application.currencyCode.length > 0 60 | && application.taxRate >= 0 61 | && application.businessName 62 | && application.locationName 63 | 64 | && !_.isUndefined(db.hostname) && db.hostname.length > 0 65 | && !_.isUndefined(db.username) && db.username.length > 0 66 | && !_.isUndefined(db.password) 67 | && !_.isUndefined(db.database) && db.database.length > 0; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /setup/win/install.bat: -------------------------------------------------------------------------------- 1 | 2 | @echo off 3 | 4 | :: check for admin 5 | echo Administrative permissions required. Detecting permissions... 6 | 7 | net session >nul 2>&1 8 | if %errorLevel% == 0 ( 9 | echo SUCCESS: Administrative permissions confirmed. 10 | ) else ( 11 | echo FAILURE: Current permissions inadequate. 12 | pause 13 | exit /b 1 14 | ) 15 | 16 | :: change to current dir; needed for admin batch files 17 | cd /d %~dp0 18 | 19 | :: defaults 20 | set DefaultInstallDirectory=C:\Program Files\PostgreSQL\9.6 21 | set DefaultBackupDirectory=C:\PosysBackup 22 | 23 | :: prompts to override defaults 24 | set /p InstallDirectory="Install Directory (%DefaultInstallDirectory%): " 25 | set /p BackupDirectory="Backup Directory (%DefaultBackupDirectory%): " 26 | 27 | if "%InstallDirectory%"=="" (set InstallDirectory=%DefaultInstallDirectory%) 28 | if "%BackupDirectory%"=="" (set BackupDirectory=%DefaultBackupDirectory%) 29 | 30 | :: install postgres 31 | echo Installing Postgres in %InstallDirectory%... 32 | start /wait .\postgresql-9.6.1-1-windows-x64.exe --unattendedmodeui minimal --mode unattended --superpassword "postgres" --servicename "postgreSQL" --servicepassword "postgres" --serverport 5432 --prefix "%InstallDirectory%" 33 | 34 | :: create postgres db 35 | echo Creating database "posys"... 36 | set PGPASSWORD=postgres 37 | "%InstallDirectory%\bin\createdb.exe" -h localhost -p 5432 -U postgres -w posys 38 | 39 | :: create daily backup script 40 | echo Creating backup script (backups to %BackupDirectory%)... 41 | ( 42 | echo "%InstallDirectory%\bin\pg_dump.exe" posys ^> "%BackupDirectory%\posys_%%date%%_%%time%%.bak" 43 | echo forfiles -p "%InstallDirectory%" -s -m *.* -d 30 -c "cmd /c del @path" 44 | ) > "%InstallDirectory%\backup.bat" 45 | 46 | :: create scheduled task for every day at 3 AM 47 | SchTasks /Create /F /SC DAILY /TN "Posys Backup" /TR "%InstallDirectory%\backup.bat" /ST 03:00 48 | 49 | echo Done! 50 | pause 51 | -------------------------------------------------------------------------------- /electron.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const electron = require('electron'); 4 | const open = require('open'); 5 | 6 | const isDev = require('electron-is-dev'); 7 | const appRoot = require('app-root-path'); 8 | 9 | const fs = require('fs'); 10 | 11 | const { 12 | app, 13 | BrowserWindow 14 | } = electron; 15 | 16 | try { 17 | require('electron-debug')({ showDevTools: true }); 18 | } catch(e) { 19 | console.error('Could not load electron-debug'); 20 | } 21 | 22 | process.on('uncaughtException', function(err) { 23 | fs.writeFile('error.log', JSON.stringify(err, null, 4)); 24 | }); 25 | 26 | let win; 27 | 28 | function createWindow() { 29 | const { width, height } = electron.screen.getPrimaryDisplay().workAreaSize; 30 | win = new BrowserWindow({ 31 | width, 32 | height, 33 | minWidth: 1024, 34 | minHeight: 768, 35 | title: 'Posys', 36 | show: false 37 | }); 38 | 39 | win.once('ready-to-show', () => { 40 | win.show(); 41 | }); 42 | 43 | let url = 'http://localhost:8100'; 44 | 45 | var express = require('express'); 46 | var expressApp = express(); 47 | 48 | expressApp.use(express.static(__dirname)); 49 | expressApp.listen(30517); 50 | 51 | if(!isDev) { 52 | url = 'http://localhost:30517/www/index.html'; 53 | } 54 | 55 | win.loadURL(url); 56 | 57 | require('./www/server/index'); 58 | 59 | win.webContents.on('new-window', function(event, url) { 60 | if(_.includes(url, 'localhost')) { return; } 61 | event.preventDefault(); 62 | open(url); 63 | }); 64 | 65 | win.on('closed', () => { 66 | win = null; 67 | }); 68 | } 69 | 70 | app.on('ready', createWindow); 71 | 72 | app.on('window-all-closed', () => { 73 | if (process.platform !== 'darwin') { 74 | app.quit(); 75 | } 76 | }); 77 | 78 | app.on('activate', () => { 79 | if (win === null) { 80 | createWindow(); 81 | } 82 | }); 83 | -------------------------------------------------------------------------------- /seeds/dev/stockitem.js: -------------------------------------------------------------------------------- 1 | 2 | const items = [ 3 | { name: 'Reli-on Prime BGMS', sku: '605388022929', cost: 19.99, taxable: true, organizationalunitId: 2, 4 | description: 'A blood-glucose monitoring system.' }, 5 | { name: 'Reli-on Prime Test Strips', sku: '681131130332', cost: 4.99, taxable: true, organizationalunitId: 2, 6 | description: 'Test strips for the Reli-on BGMS.' }, 7 | { name: 'BD Alcohol Swabs 100ct', sku: '382903268955', cost: 1.99, taxable: true, organizationalunitId: 2, 8 | description: 'Alcoholic cleaning swabs.' }, 9 | { name: 'A1C Self-check System', sku: 'X000ONY6KT', cost: 41.99, taxable: true, organizationalunitId: 2, 10 | description: 'A system to help you check your A1C value.' }, 11 | { name: '4mm Needles', sku: '301691855918', cost: 2.99, taxable: true, organizationalunitId: 2, 12 | description: 'Short needles.' }, 13 | { name: '6mm Needles', sku: '301691851972', cost: 2.99, taxable: true, organizationalunitId: 2, 14 | description: 'Medium-length needles.' }, 15 | { name: '8mm Needles', sku: '761059307242', cost: 2.99, taxable: true, organizationalunitId: 2, 16 | description: 'Long needles.' }, 17 | { name: 'OneTouch Lancets 100ct', sku: '353885393102', cost: 1.99, taxable: true, organizationalunitId: 2, 18 | description: 'Lancet stabby things.' }, 19 | { name: 'Sugar Cubes', sku: 'SUGAR-CUBE', cost: 2.00, taxable: false, organizationalunitId: 3, 20 | description: 'Sweet.' } 21 | ]; 22 | 23 | const createStockitem = (knex, item) => { 24 | return knex.table('stockitem') 25 | .returning('id') 26 | .insert(item); 27 | }; 28 | 29 | 30 | exports.seed = (knex) => { 31 | return knex('stockitem') 32 | .count() 33 | .then(countArray => { 34 | const count = countArray[0].count; 35 | if(count > 0) { 36 | console.log('[Seed] Skipping stockitem, count(*) > 0.'); 37 | return; 38 | } 39 | return knex('stockitem') 40 | // .del() 41 | .then(() => { 42 | return Promise.all(items.map(item => createStockitem(knex, item))); 43 | }); 44 | }); 45 | }; 46 | -------------------------------------------------------------------------------- /src/client/pages/settings/edit/editsettings.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { ViewController, ModalController } from 'ionic-angular'; 3 | 4 | import { ApplicationSettingsService } from '../../../services/settings.service'; 5 | import { LocationService } from '../../../services/location.service'; 6 | import { Settings } from '../../../models/settings'; 7 | 8 | import { LocalStorage } from 'ng2-webstorage'; 9 | 10 | import { LocationManagerComponent } from '../locationmanage/location.management'; 11 | 12 | const cc = require('currency-codes'); 13 | 14 | @Component({ 15 | templateUrl: 'editsettings.html' 16 | }) 17 | export class EditSettingsComponent implements OnInit { 18 | 19 | public settings: Settings = new Settings({}); 20 | public currencyCodes = cc.codes(); 21 | 22 | public printers: any[]; 23 | public locations: any[]; 24 | 25 | @LocalStorage() 26 | public terminalId: string; 27 | 28 | constructor(public viewCtrl: ViewController, 29 | public modalCtrl: ModalController, 30 | public locationService: LocationService, 31 | public settingsService: ApplicationSettingsService) {} 32 | 33 | ngOnInit() { 34 | this.settings = new Settings(this.settingsService.settings); 35 | this.refreshPrinters(); 36 | this.refreshLocations(); 37 | } 38 | 39 | refreshPrinters() { 40 | this.settingsService 41 | .getAllPrinters() 42 | .toPromise() 43 | .then(printers => { 44 | this.printers = printers; 45 | }); 46 | } 47 | 48 | manageLocations() { 49 | let modal = this.modalCtrl.create(LocationManagerComponent, { enableBackdropDismiss: false }); 50 | modal.onDidDismiss(() => { 51 | this.refreshLocations(); 52 | }); 53 | modal.present(); 54 | } 55 | 56 | refreshLocations() { 57 | this.locationService 58 | .getAll() 59 | .toPromise() 60 | .then(locations => { 61 | this.locations = locations; 62 | }); 63 | } 64 | 65 | saveSettings() { 66 | this.settingsService 67 | .changeSettings(this.settings) 68 | .then(() => this.dismiss()); 69 | } 70 | 71 | dismiss() { 72 | this.viewCtrl.dismiss(); 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /src/client/services/report.service.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as _ from 'lodash'; 3 | 4 | import { LimitedReportConfiguration, ReportConfiguration } from '../models/reportconfiguration'; 5 | 6 | import { LoggerService } from './logger.service'; 7 | import { ApplicationSettingsService } from './settings.service'; 8 | 9 | import { Injectable } from '@angular/core'; 10 | import { Response } from '@angular/http'; 11 | import { HttpClient } from './override/http.custom'; 12 | import { Observable } from 'rxjs/Rx'; 13 | 14 | @Injectable() 15 | export class ReportService { 16 | 17 | private url = 'report'; 18 | 19 | constructor(private http: HttpClient, 20 | private logger: LoggerService, 21 | private settings: ApplicationSettingsService) {} 22 | 23 | runReport(item: ReportConfiguration): Observable { 24 | return this.http.post(this.settings.buildAPIURL(`${this.url}/${item.reportRoute}`), item) 25 | .map((res: Response) => this.logger.observableUnwrap(res.json())) 26 | .catch(e => this.logger.observableError(e)); 27 | } 28 | 29 | getAll(): Observable { 30 | return this.http.get(this.settings.buildAPIURL(this.url)) 31 | .map((res: Response) => this.logger.observableUnwrap(res.json())) 32 | .catch(e => this.logger.observableError(e)); 33 | } 34 | 35 | create(item: LimitedReportConfiguration): Observable { 36 | return this.http.put(this.settings.buildAPIURL(this.url), item) 37 | .map((res: Response) => this.logger.observableUnwrap(res.json())) 38 | .catch(e => this.logger.observableError(e)); 39 | } 40 | 41 | update(item: LimitedReportConfiguration): Observable { 42 | return this.http.patch(this.settings.buildAPIURL(this.url, item.id), item) 43 | .map((res: Response) => this.logger.observableUnwrap(res.json())) 44 | .catch(e => this.logger.observableError(e)); 45 | } 46 | 47 | remove(item: LimitedReportConfiguration): Observable { 48 | return this.http.delete(this.settings.buildAPIURL(this.url, item.id)) 49 | .map((res: Response) => this.logger.observableUnwrap(res.json())) 50 | .catch(e => this.logger.observableError(e)); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/client/pages/pointofsale/cashpay/pointofsale.cashpay.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash'; 2 | 3 | import { Component } from '@angular/core'; 4 | import { ViewController, AlertController, NavParams } from 'ionic-angular'; 5 | 6 | import { CurrencyFromSettingsPipe } from '../../../pipes/currency-from-settings'; 7 | 8 | @Component({ 9 | templateUrl: 'pointofsale.cashpay.html', 10 | providers: [CurrencyFromSettingsPipe] 11 | }) 12 | export class CashPayComponent { 13 | 14 | public cashExpected: number; 15 | public cashPaid: string = ''; 16 | 17 | public buttons = [7, 8, 9, 4, 5, 6, 1, 2, 3, '.', 0, '<']; 18 | 19 | constructor(public viewCtrl: ViewController, 20 | public alertCtrl: AlertController, 21 | public currencyPipe: CurrencyFromSettingsPipe, 22 | public navParams: NavParams) { 23 | 24 | this.cashExpected = navParams.get('cashExpected'); 25 | } 26 | 27 | adjustValue(newKey: string|number) { 28 | if(newKey === '<') { 29 | this.cashPaid = this.cashPaid.substring(0, this.cashPaid.length - 1); 30 | return; 31 | } 32 | 33 | this.cashPaid += newKey; 34 | } 35 | 36 | canPay() { 37 | return +this.cashPaid >= this.cashExpected; 38 | } 39 | 40 | dismiss(useValue?: boolean) { 41 | 42 | const cashPaid = +this.cashPaid; 43 | const diff = Math.abs(cashPaid - this.cashExpected); 44 | 45 | let message = `The customer has given ${this.currencyPipe.transform(cashPaid)} 46 | on a transaction worth ${this.currencyPipe.transform(this.cashExpected)}. `; 47 | if(diff === 0) { 48 | message += 'There is no change to be given.'; 49 | } else { 50 | message += `There is ${this.currencyPipe.transform(diff)} due in change.`; 51 | } 52 | 53 | const confirm = this.alertCtrl.create({ 54 | title: 'Finish Cash Pay?', 55 | message, 56 | buttons: [ 57 | { 58 | text: 'Cancel' 59 | }, 60 | { 61 | text: 'Confirm', 62 | handler: () => { 63 | this.viewCtrl.dismiss(cashPaid); 64 | } 65 | } 66 | ] 67 | }); 68 | 69 | if(useValue) { 70 | confirm.present(); 71 | return; 72 | } 73 | 74 | this.viewCtrl.dismiss(); 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /src/client/services/override/http.custom.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash'; 2 | 3 | import { Injectable } from '@angular/core'; 4 | import { Http, Headers, RequestOptionsArgs, Request, Response, RequestOptions, RequestMethod } from '@angular/http'; 5 | import { Observable } from 'rxjs/Observable'; 6 | 7 | import { LocalStorageService } from 'ng2-webstorage'; 8 | 9 | @Injectable() 10 | export class HttpClient { 11 | 12 | constructor(private http: Http, 13 | private localStorage: LocalStorageService) {} 14 | 15 | private _setCustomHeaders(options?: RequestOptionsArgs): RequestOptionsArgs { 16 | 17 | if(!options) { 18 | options = new RequestOptions({}); 19 | } 20 | 21 | if(!options.headers) { 22 | options.headers = new Headers(); 23 | } 24 | 25 | const terminalId = this.localStorage.retrieve('terminalId'); 26 | const locationName = this.localStorage.retrieve('locationName'); 27 | 28 | if(locationName) { 29 | options.headers.set('X-Location', locationName); 30 | } 31 | 32 | if(terminalId) { 33 | options.headers.set('X-Terminal', terminalId); 34 | } 35 | 36 | return options; 37 | } 38 | 39 | get(url, options?) { 40 | const reqOpts = { method: RequestMethod.Get }; 41 | _.extend(reqOpts, options); 42 | return this.request(url, reqOpts); 43 | } 44 | 45 | delete(url, options?) { 46 | const reqOpts = { method: RequestMethod.Delete }; 47 | _.extend(reqOpts, options); 48 | return this.request(url, reqOpts); 49 | } 50 | 51 | post(url, body, options?) { 52 | const reqOpts = { method: RequestMethod.Post, body }; 53 | _.extend(reqOpts, options); 54 | return this.request(url, reqOpts); 55 | } 56 | 57 | put(url, body, options?) { 58 | const reqOpts = { method: RequestMethod.Put, body }; 59 | _.extend(reqOpts, options); 60 | return this.request(url, reqOpts); 61 | } 62 | 63 | patch(url, body, options?) { 64 | const reqOpts = { method: RequestMethod.Patch, body }; 65 | _.extend(reqOpts, options); 66 | return this.request(url, reqOpts); 67 | } 68 | 69 | request(url: string|Request, options?: RequestOptionsArgs): Observable { 70 | options = this._setCustomHeaders(options); 71 | return this.http.request(url, options); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/client/theme/variables.scss: -------------------------------------------------------------------------------- 1 | // Ionic Variables and Theming. For more info, please see: 2 | // http://ionicframework.com/docs/v2/theming/ 3 | @import "ionic.globals"; 4 | 5 | 6 | // Shared Variables 7 | // -------------------------------------------------- 8 | // To customize the look and feel of this app, you can override 9 | // the Sass variables found in Ionic's source scss files. 10 | // To view all the possible Ionic variables, see: 11 | // http://ionicframework.com/docs/v2/theming/overriding-ionic-variables/ 12 | 13 | $text-color: #000; 14 | $background-color: #fff; 15 | 16 | 17 | // Named Color Variables 18 | // -------------------------------------------------- 19 | // Named colors makes it easy to reuse colors on various components. 20 | // It's highly recommended to change the default colors 21 | // to match your app's branding. Ionic uses a Sass map of 22 | // colors so you can create, rename and remove colors as needed. 23 | // The "primary" color is the only required color in the map. 24 | 25 | $colors: ( 26 | primary: #387ef5, 27 | secondary: #32db64, 28 | danger: #f53d3d, 29 | light: #f4f4f4, 30 | dark: #222 31 | ); 32 | 33 | 34 | // App iOS Variables 35 | // -------------------------------------------------- 36 | // iOS only Sass variables can go here 37 | 38 | 39 | 40 | 41 | // App Material Design Variables 42 | // -------------------------------------------------- 43 | // Material Design only Sass variables can go here 44 | 45 | 46 | 47 | 48 | // App Windows Variables 49 | // -------------------------------------------------- 50 | // Windows only Sass variables can go here 51 | 52 | 53 | 54 | 55 | // App Theme 56 | // -------------------------------------------------- 57 | // Ionic apps can have different themes applied, which can 58 | // then be future customized. This import comes last 59 | // so that the above variables are used and Ionic's 60 | // default are overridden. 61 | 62 | @import "ionic.theme.default"; 63 | 64 | 65 | // Ionicons 66 | // -------------------------------------------------- 67 | // The premium icon font for Ionic. For more info, please see: 68 | // http://ionicframework.com/docs/v2/ionicons/ 69 | 70 | $ionicons-font-path: "../assets/fonts"; 71 | @import "ionicons"; 72 | 73 | 74 | $modal-inset-height-large: 700px; 75 | $modal-inset-height-small: 600px; 76 | $modal-inset-width: 700px; 77 | $alert-md-max-width: 350px; 78 | -------------------------------------------------------------------------------- /src/server/server.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as _ from 'lodash'; 3 | import { Logger } from './logger'; 4 | 5 | const fs = require('fs'); 6 | const appRoot = require('app-root-path'); 7 | 8 | import { readSettings, writeSettings } from './_settings'; 9 | 10 | Logger.info('Init', 'Starting server...'); 11 | 12 | const defaultConfig = { 13 | server: { 14 | port: 8080 15 | }, 16 | db: { 17 | hostname: 'localhost', 18 | username: 'postgres', 19 | password: 'postgres', 20 | database: 'posys' 21 | } 22 | }; 23 | 24 | try { 25 | fs.accessSync(`${appRoot}/server.config.json`, fs.F_OK); 26 | } catch (e) { 27 | Logger.info('Init', 'Can\'t find the server.config.json file. Creating a default one...'); 28 | fs.writeFileSync(`${appRoot}/server.config.json`, JSON.stringify(defaultConfig, null, 4)); 29 | } 30 | 31 | const config = require(`${appRoot}/server.config.json`); 32 | 33 | const knexConfig = require('./knexfile'); 34 | export const knex = require('knex')(knexConfig); 35 | 36 | export const bookshelf = require('bookshelf')(knex); 37 | 38 | const validator = require('./validator').default; 39 | 40 | bookshelf.plugin(require('./ext/bookshelf-pagination')); 41 | bookshelf.plugin(require('bookshelf-validate'), { validateOnSave: true, validator }); 42 | bookshelf.plugin(require('bookshelf-paranoia')); 43 | 44 | export const setup = () => { 45 | knex.migrate.latest(knexConfig) 46 | .then(() => { 47 | Logger.info('Init', 'At latest migration.'); 48 | readSettings(data => { 49 | if(data.initialSetup) { return; } 50 | knex.seed.run(knexConfig) 51 | .then(() => { 52 | Logger.info('Init', 'Seed data added.'); 53 | data.initialSetup = true; 54 | writeSettings(data); 55 | }); 56 | }); 57 | }); 58 | }; 59 | 60 | export const start = () => { 61 | const express = require('express'); 62 | const bodyParser = require('body-parser'); 63 | const cors = require('cors'); 64 | const app = express(); 65 | app.use(bodyParser.json()); 66 | 67 | const corsOptions = { 68 | methods: ['GET', 'POST', 'PATCH', 'PUT', 'DELETE'], 69 | allowedHeaders: ['X-Location', 'X-Terminal', 'Content-Type'], 70 | exposedHeaders: ['X-Location', 'X-Terminal', 'Content-Type'] 71 | }; 72 | 73 | app.use(cors(corsOptions)); 74 | 75 | require('./routes/index').default(app); 76 | 77 | const server = require('http').createServer(app); 78 | server.listen(config.server.port); 79 | Logger.info('Init', `REST API listening on port ${config.server.port}`); 80 | }; 81 | -------------------------------------------------------------------------------- /src/client/pages/inventory/oumanage/ou.management.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Manage Categories 5 | 6 | 7 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | New Category Name 21 | 22 | 23 | 24 | Category Name 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 |
41 | You don't have any Categories. You'll need to create some to add inventory. 42 |
43 |
44 |
45 | 46 | 47 | 48 | 49 | 50 | 51 | {{ ou.name }} 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 |
63 | 64 |
65 | -------------------------------------------------------------------------------- /src/client/pages/promotions/promotiondisplay.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Component, Input, Output, EventEmitter } from '@angular/core'; 3 | 4 | import { Promotion } from '../../models/promotion'; 5 | 6 | @Component({ 7 | selector: 'promotion-display', 8 | template: ` 9 | 10 | 11 | 12 | 13 | {{ item.name | truncate:50 }} 14 | 15 | 16 | {{ item.description | truncate:50 }} 17 | 18 | 19 | 20 | {{ item.startDate | date:'short' }} 21 | 22 | 23 | {{ item.endDate | date:'short' }} 24 | 25 | 26 | {{ reductionType() }} {{ discountDisplay() }} 27 | 28 | 29 | {{ affected() | truncate:15 }} 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | `, 38 | }) 39 | export class PromotionDisplayComponent { 40 | @Input() item: Promotion; 41 | @Output() edit = new EventEmitter(); 42 | @Output() remove = new EventEmitter(); 43 | 44 | affected() { 45 | return this.item.promoItems && this.item.promoItems.length > 0 46 | ? `${this.item.promoItems.length} item${this.item.promoItems.length > 1 ? 's' : ''}` 47 | : this.item.organizationalunit.name; 48 | } 49 | 50 | discountDisplay() { 51 | if(this.item.discountType === 'Dollar') { 52 | return `$${this.item.discountValue}`; 53 | } 54 | 55 | return `${this.item.discountValue}%`; 56 | } 57 | 58 | reductionType() { 59 | if(this.item.itemReductionType === 'All' && this.item.numItemsRequired === 1) { 60 | return 'All'; 61 | } 62 | 63 | if(this.item.itemReductionType === 'All') { 64 | return `After ${this.item.numItemsRequired}, `; 65 | 66 | } else if(this.item.itemReductionType === 'BuyXGetNext') { 67 | return `B${this.item.numItemsRequired}GN`; 68 | 69 | } else if(this.item.itemReductionType === 'SetTo') { 70 | return `${this.item.numItemsRequired} for `; 71 | } 72 | 73 | return 'Unknown'; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/server/routes/location.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Location } from '../orm/location'; 3 | import { Logger } from '../logger'; 4 | 5 | import { Location as LocationModel } from '../../client/models/location'; 6 | import { recordAuditMessage, recordErrorMessageFromServer, MESSAGE_CATEGORIES } from './_logging'; 7 | 8 | export default (app) => { 9 | app.get('/location', (req, res) => { 10 | Location 11 | .collection() 12 | .orderBy('name', 'ASC') 13 | .fetch() 14 | .then(collection => { 15 | res.json(collection.toJSON()); 16 | }) 17 | .catch(e => { 18 | recordErrorMessageFromServer(req, MESSAGE_CATEGORIES.LOCATION, e); 19 | res.status(500).json(Logger.browserError(Logger.error('Route:Location:GET', e))); 20 | }); 21 | }); 22 | 23 | app.put('/location', (req, res) => { 24 | const loca = new LocationModel(req.body); 25 | 26 | Location 27 | .forge(loca) 28 | .save() 29 | .then(item => { 30 | recordAuditMessage(req, MESSAGE_CATEGORIES.LOCATION, `A new location was added (${loca.name}).`, { id: item.id }); 31 | res.json(item); 32 | }) 33 | .catch(e => { 34 | recordErrorMessageFromServer(req, MESSAGE_CATEGORIES.LOCATION, e); 35 | res.status(500).json({ formErrors: e.data || [] }); 36 | }); 37 | }); 38 | 39 | app.patch('/location/:id', (req, res) => { 40 | const loca = new LocationModel(req.body); 41 | 42 | Location 43 | .forge({ id: req.params.id }) 44 | .save(loca, { patch: true }) 45 | .then(item => { 46 | recordAuditMessage(req, MESSAGE_CATEGORIES.LOCATION, `A location was changed (${loca.name}).`, { id: item.id }); 47 | res.json(item); 48 | }) 49 | .catch(e => { 50 | recordErrorMessageFromServer(req, MESSAGE_CATEGORIES.LOCATION, e); 51 | res.status(500).json({ formErrors: e.data || [] }); 52 | }); 53 | }); 54 | 55 | app.delete('/location/:id', (req, res) => { 56 | if(+req.params.id === 1) { 57 | res.status(500).json({ flash: 'Cannot remove the first Location. '}); 58 | return; 59 | } 60 | 61 | Location 62 | .forge({ id: req.params.id }) 63 | .destroy() 64 | .then(item => { 65 | recordAuditMessage(req, MESSAGE_CATEGORIES.LOCATION, `A location was removed.`, { id: +req.params.id, oldId: +req.params.id }); 66 | res.json(item); 67 | }) 68 | .catch(e => { 69 | recordErrorMessageFromServer(req, MESSAGE_CATEGORIES.LOCATION, e); 70 | const errorMessage = Logger.parseDatabaseError(e, 'Location'); 71 | res.status(500).json({ flash: errorMessage }); 72 | }); 73 | }); 74 | }; 75 | -------------------------------------------------------------------------------- /src/client/pages/inventory/quick/quick.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Quick Scan Inventory 5 | 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 25 | 26 | 27 | 28 | 29 |
30 | You don't have any items scanned. 31 |
32 |
33 |
34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | #{{ i + 1 }} 42 | 43 | 44 | 45 | {{ item.name | truncate:25 }} 46 | 47 | 48 | {{ item.description | truncate:25 }} 49 | 50 | 51 | 52 | x 53 | {{ item.cost | currencyFromSettings }} 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 |
65 | 66 |
67 | -------------------------------------------------------------------------------- /src/client/pages/inventory/oumanage/ou.management.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as _ from 'lodash'; 3 | 4 | import { Component, OnInit } from '@angular/core'; 5 | import { ViewController, AlertController } from 'ionic-angular'; 6 | import { Observable, BehaviorSubject } from 'rxjs/Rx'; 7 | 8 | import { OrganizationalUnit } from '../../../models/organizationalunit'; 9 | 10 | import { OrganizationalUnitService } from '../../../services/organizationalunit.service'; 11 | 12 | @Component({ 13 | templateUrl: 'ou.management.html' 14 | }) 15 | export class OUManagerComponent implements OnInit { 16 | 17 | public ou: OrganizationalUnit = new OrganizationalUnit(); 18 | public allOU: OrganizationalUnit[] = []; 19 | public _formErrors: BehaviorSubject = new BehaviorSubject({}); 20 | public formErrors: Observable = this._formErrors.asObservable(); 21 | 22 | constructor(public viewCtrl: ViewController, 23 | public alertCtrl: AlertController, 24 | public ouService: OrganizationalUnitService) {} 25 | 26 | ngOnInit() { 27 | this.ouService.getAll().toPromise().then(data => { 28 | this.allOU = data; 29 | }); 30 | } 31 | 32 | addNewOU() { 33 | this.ouService 34 | .create(this.ou) 35 | .subscribe((newOU) => { 36 | this.allOU.push(newOU); 37 | this.allOU = _.sortBy(this.allOU, 'name'); 38 | this.resetOU(); 39 | this._formErrors.next({}); 40 | }, e => this._formErrors.next(e.formErrors)); 41 | } 42 | 43 | removeOU(ou: OrganizationalUnit) { 44 | 45 | const confirm = this.alertCtrl.create({ 46 | title: `Remove Category "${ou.name}"?`, 47 | message: 'Exercise caution when doing this. You may receive errors if you still have items assigned to this category.', 48 | buttons: [ 49 | { 50 | text: 'Cancel' 51 | }, 52 | { 53 | text: 'Confirm', 54 | handler: () => { 55 | this.ouService 56 | .remove(ou) 57 | .subscribe(() => { 58 | this.allOU = _.reject(this.allOU, checkOU => checkOU.id === ou.id); 59 | }); 60 | } 61 | } 62 | ] 63 | }); 64 | confirm.present(); 65 | } 66 | 67 | updateOU(ou: OrganizationalUnit) { 68 | this.ouService 69 | .update(ou) 70 | .subscribe(() => { 71 | _.extend(_.find(this.allOU, { id: ou.id }), ou); 72 | this.resetOU(); 73 | }); 74 | } 75 | 76 | resetOU() { 77 | this.ou = new OrganizationalUnit(); 78 | } 79 | 80 | editOU(ou: OrganizationalUnit) { 81 | this.ou = new OrganizationalUnit(ou); 82 | } 83 | 84 | dismiss() { 85 | this.viewCtrl.dismiss(); 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /src/client/pages/settings/locationmanage/location.management.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as _ from 'lodash'; 3 | 4 | import { Component, OnInit } from '@angular/core'; 5 | import { ViewController, AlertController } from 'ionic-angular'; 6 | import { Observable, BehaviorSubject } from 'rxjs/Rx'; 7 | 8 | import { Location } from '../../../models/location'; 9 | 10 | import { LocationService } from '../../../services/location.service'; 11 | 12 | @Component({ 13 | templateUrl: 'location.management.html' 14 | }) 15 | export class LocationManagerComponent implements OnInit { 16 | 17 | public location: Location = new Location(); 18 | public allLocations: Location[] = []; 19 | public _formErrors: BehaviorSubject = new BehaviorSubject({}); 20 | public formErrors: Observable = this._formErrors.asObservable(); 21 | 22 | constructor(public viewCtrl: ViewController, 23 | public alertCtrl: AlertController, 24 | public locaService: LocationService) {} 25 | 26 | ngOnInit() { 27 | this.locaService.getAll().toPromise().then(data => { 28 | this.allLocations = data; 29 | }); 30 | } 31 | 32 | addNewLocation() { 33 | this.locaService 34 | .create(this.location) 35 | .subscribe((newOU) => { 36 | this.allLocations.push(newOU); 37 | this.allLocations = _.sortBy(this.allLocations, 'name'); 38 | this.resetLocation(); 39 | this._formErrors.next({}); 40 | }, e => this._formErrors.next(e.formErrors)); 41 | } 42 | 43 | removeLocation(loca: Location) { 44 | 45 | const confirm = this.alertCtrl.create({ 46 | title: `Remove Location "${loca.name}"?`, 47 | message: 'Exercise caution when doing this. You may receive errors.', 48 | buttons: [ 49 | { 50 | text: 'Cancel' 51 | }, 52 | { 53 | text: 'Confirm', 54 | handler: () => { 55 | this.locaService 56 | .remove(loca) 57 | .subscribe(() => { 58 | this.allLocations = _.reject(this.allLocations, checkLoca => checkLoca.id === loca.id); 59 | }); 60 | } 61 | } 62 | ] 63 | }); 64 | confirm.present(); 65 | } 66 | 67 | updateLocation(loca: Location) { 68 | this.locaService 69 | .update(loca) 70 | .subscribe(() => { 71 | _.extend(_.find(this.allLocations, { id: loca.id }), loca); 72 | this.resetLocation(); 73 | }); 74 | } 75 | 76 | resetLocation() { 77 | this.location = new Location(); 78 | } 79 | 80 | editLocation(loca: Location) { 81 | this.location = new Location(loca); 82 | } 83 | 84 | dismiss() { 85 | this.viewCtrl.dismiss(); 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /src/client/pages/settings/locationmanage/location.management.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Manage Locations 5 | 6 | 7 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | New Location Name 21 | 22 | 23 | 24 | Category Name 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 |
41 | You don't have any Locations. You'll need to create one to create items. 42 |
43 |
44 |
45 | 46 | 47 | 48 | 49 | 50 | 51 | {{ location.name }} 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 |
63 | 64 |
65 | -------------------------------------------------------------------------------- /src/client/services/invoice.service.ts: -------------------------------------------------------------------------------- 1 | 2 | import { PagedItems } from '../models/pageditems'; 3 | import { Invoice } from '../models/invoice'; 4 | 5 | import { LoggerService } from './logger.service'; 6 | import { ApplicationSettingsService } from './settings.service'; 7 | 8 | import { Injectable } from '@angular/core'; 9 | import { Response } from '@angular/http'; 10 | import { HttpClient } from './override/http.custom'; 11 | import { Observable } from 'rxjs/Rx'; 12 | 13 | @Injectable() 14 | export class InvoiceService { 15 | 16 | private url = 'invoice'; 17 | 18 | constructor(private http: HttpClient, 19 | private logger: LoggerService, 20 | private settings: ApplicationSettingsService) {} 21 | 22 | getMany(args: any): Observable> { 23 | return this.http.get(this.settings.buildAPIURL(this.url), { search: this.settings.buildSearchParams(args) }) 24 | .map((res: Response) => this.logger.observableUnwrap(res.json())) 25 | .catch(e => this.logger.observableError(e)); 26 | } 27 | 28 | get(item: Invoice): Observable { 29 | return this.http.get(this.settings.buildAPIURL(this.url, item.id)) 30 | .map((res: Response) => this.logger.observableUnwrap(res.json())) 31 | .catch(e => this.logger.observableError(e)); 32 | } 33 | 34 | create(item: Invoice): Observable { 35 | return this.http.put(this.settings.buildAPIURL(this.url), item) 36 | .map((res: Response) => this.logger.observableUnwrap(res.json())) 37 | .catch(e => this.logger.observableError(e)); 38 | } 39 | 40 | update(item: Invoice): Observable { 41 | return this.http.patch(this.settings.buildAPIURL(this.url, item.id), item) 42 | .map((res: Response) => this.logger.observableUnwrap(res.json())) 43 | .catch(e => this.logger.observableError(e)); 44 | } 45 | 46 | remove(item: Invoice): Observable { 47 | return this.http.delete(this.settings.buildAPIURL(this.url, item.id)) 48 | .map((res: Response) => this.logger.observableUnwrap(res.json())) 49 | .catch(e => this.logger.observableError(e)); 50 | } 51 | 52 | toggleVoid(item: Invoice): Observable { 53 | return this.http.post(this.settings.buildAPIURL(`${this.url}/void`, item.id), item) 54 | .map((res: Response) => this.logger.observableUnwrap(res.json())) 55 | .catch(e => this.logger.observableError(e)); 56 | } 57 | 58 | print(item: Invoice, printCustomer: boolean): Observable { 59 | return this.http.post(`${this.settings.buildAPIURL(`${this.url}/print`, item.id)}?printCustomer=${+printCustomer}`, item) 60 | .map((res: Response) => this.logger.observableUnwrap(res.json())) 61 | .catch(e => this.logger.observableError(e)); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/client/pages/pointofsale/transactionpromo.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core'; 3 | import { PopoverController, ViewController, NavParams } from 'ionic-angular'; 4 | 5 | import { InvoicePromo } from '../../models/invoicepromo'; 6 | 7 | @Component({ 8 | selector: 'transaction-promo', 9 | template: ` 10 | 11 | 12 | 13 | 16 | 17 | 18 | 19 | {{ item.realData.name | truncate:50 }} 20 | 21 | 22 | {{ item.realData.description | truncate:50 }} 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | {{ item.cost | currencyFromSettings }} 32 | 33 | 34 | 35 | 36 | `, 37 | styles: [` 38 | [space-between-col] { 39 | justify-content: space-around; 40 | } 41 | `] 42 | }) 43 | export class TransactionPromoComponent { 44 | @Input() item: InvoicePromo; 45 | @Input() buttons: any[]; 46 | @Input() index: number; 47 | 48 | constructor(public popoverCtrl: PopoverController) {} 49 | 50 | moreOptions($event) { 51 | const popover = this.popoverCtrl.create(TransactionPromoPopoverComponent, { buttons: this.buttons, item: this.item }); 52 | 53 | popover.present({ 54 | ev: $event 55 | }); 56 | } 57 | } 58 | 59 | @Component({ 60 | template: ` 61 | 62 | 63 | {{ button.text }} 64 | 65 | 66 | ` 67 | }) 68 | export class TransactionPromoPopoverComponent implements OnInit { 69 | public buttons: any[]; 70 | private item: InvoicePromo; 71 | 72 | constructor(private navParams: NavParams, private viewCtrl: ViewController) {} 73 | 74 | ngOnInit() { 75 | this.buttons = this.navParams.data.buttons; 76 | this.item = this.navParams.data.item; 77 | } 78 | 79 | doCallback(button) { 80 | button.callback(this.item); 81 | this.viewCtrl.dismiss(); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/server/routes/organizationalunit.ts: -------------------------------------------------------------------------------- 1 | 2 | import { OrganizationalUnit } from '../orm/organizationalunit'; 3 | import { Logger } from '../logger'; 4 | 5 | import { OrganizationalUnit as OrganizationalUnitModel } from '../../client/models/organizationalunit'; 6 | import { recordAuditMessage, recordErrorMessageFromServer, MESSAGE_CATEGORIES } from './_logging'; 7 | 8 | export default (app) => { 9 | app.get('/organizationalunit', (req, res) => { 10 | OrganizationalUnit 11 | .collection() 12 | .orderBy('name', 'ASC') 13 | .fetch() 14 | .then(collection => { 15 | res.json(collection.toJSON()); 16 | }) 17 | .catch(e => { 18 | recordErrorMessageFromServer(req, MESSAGE_CATEGORIES.OU, e); 19 | res.status(500).json(Logger.browserError(Logger.error('Route:OrganizationalUnit:GET', e))); 20 | }); 21 | }); 22 | 23 | app.put('/organizationalunit', (req, res) => { 24 | const ou = new OrganizationalUnitModel(req.body); 25 | 26 | OrganizationalUnit 27 | .forge(ou) 28 | .save() 29 | .then(item => { 30 | recordAuditMessage(req, MESSAGE_CATEGORIES.OU, `A category was added (${ou.name}).`, { id: item.id }); 31 | res.json(item); 32 | }) 33 | .catch(e => { 34 | recordErrorMessageFromServer(req, MESSAGE_CATEGORIES.OU, e); 35 | res.status(500).json({ formErrors: e.data || [] }); 36 | }); 37 | }); 38 | 39 | app.patch('/organizationalunit/:id', (req, res) => { 40 | const ou = new OrganizationalUnitModel(req.body); 41 | 42 | ou.name = ''; 43 | 44 | console.log(ou); 45 | 46 | OrganizationalUnit 47 | .forge({ id: req.params.id }) 48 | .save(ou, { patch: true }) 49 | .then(item => { 50 | recordAuditMessage(req, MESSAGE_CATEGORIES.OU, `A category was changed (${ou.name}).`, { id: item.id }); 51 | res.json(item); 52 | }) 53 | .catch(e => { 54 | recordErrorMessageFromServer(req, MESSAGE_CATEGORIES.OU, e); 55 | res.status(500).json({ formErrors: e.data || [] }); 56 | }); 57 | }); 58 | 59 | app.delete('/organizationalunit/:id', (req, res) => { 60 | if(+req.params.id === 1) { 61 | res.status(500).json({ flash: 'Cannot remove the first OU. '}); 62 | return; 63 | } 64 | 65 | OrganizationalUnit 66 | .forge({ id: req.params.id }) 67 | .destroy() 68 | .then(item => { 69 | recordAuditMessage(req, MESSAGE_CATEGORIES.OU, `A category was removed.`, { id: +req.params.id, oldId: +req.params.id }); 70 | res.json(item); 71 | }) 72 | .catch(e => { 73 | recordErrorMessageFromServer(req, MESSAGE_CATEGORIES.OU, e); 74 | const errorMessage = Logger.parseDatabaseError(e, 'OU'); 75 | res.status(500).json({ flash: errorMessage }); 76 | }); 77 | }); 78 | }; 79 | -------------------------------------------------------------------------------- /src/client/pages/invoices/invoices.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | import { ModalController } from 'ionic-angular'; 4 | import { Pagination } from 'ionic2-pagination'; 5 | import { LocalStorage } from 'ng2-webstorage'; 6 | 7 | import { InvoiceViewComponent } from './view/invoice.view'; 8 | 9 | import { InvoiceService } from '../../services/invoice.service'; 10 | import { Invoice } from '../../models/invoice'; 11 | 12 | @Component({ 13 | selector: 'my-page-invoices', 14 | templateUrl: 'invoices.html' 15 | }) 16 | export class InvoicesPageComponent implements OnInit { 17 | 18 | @LocalStorage() 19 | public earliestDate: string; 20 | 21 | @LocalStorage() 22 | public latestDate: string; 23 | 24 | @LocalStorage() 25 | public hideVoided: boolean; 26 | 27 | @LocalStorage() 28 | public hideHolds: boolean; 29 | 30 | @LocalStorage() 31 | public hideReturns: boolean; 32 | 33 | @LocalStorage() 34 | public hideCompleted: boolean; 35 | 36 | public currentInvoices: Invoice[] = []; 37 | public paginationInfo: Pagination; 38 | 39 | constructor(public modalCtrl: ModalController, public ivService: InvoiceService) {} 40 | 41 | ngOnInit() { 42 | this.changePage(1); 43 | } 44 | 45 | unsetDate(type: string) { 46 | this[type] = ''; 47 | this.toggleFilter(); 48 | } 49 | 50 | toggleFilter() { 51 | if(!this.paginationInfo) { return; } 52 | this.changePage(this.paginationInfo.page); 53 | } 54 | 55 | changePage(newPage) { 56 | this.ivService 57 | .getMany({ 58 | page: newPage, 59 | earliestDate: this.earliestDate, 60 | latestDate: this.latestDate, 61 | hideVoided: +this.hideVoided, 62 | hideHolds: +this.hideHolds, 63 | hideReturns: +this.hideReturns, 64 | hideCompleted: +this.hideCompleted 65 | }) 66 | .toPromise() 67 | .then(({ items, pagination }) => { 68 | this.currentInvoices = items; 69 | this.paginationInfo = pagination; 70 | }); 71 | } 72 | 73 | openItemModal(item?: Invoice, doSearch = true) { 74 | 75 | const openModal = (invoice: Invoice) => { 76 | const modal = this.modalCtrl.create(InvoiceViewComponent, { 77 | invoice: invoice 78 | }, { enableBackdropDismiss: false }); 79 | modal.onDidDismiss(() => { 80 | this.changePage(this.paginationInfo.page); 81 | }); 82 | modal.present(); 83 | }; 84 | 85 | if(!item) { 86 | return; 87 | } 88 | 89 | if(!doSearch) { 90 | openModal(item); 91 | return; 92 | } 93 | 94 | this.ivService 95 | .get(item) 96 | .toPromise() 97 | .then(invoice => { 98 | openModal(invoice); 99 | }); 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /src/client/pages/settings/audit/audit.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Audit Log 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | Filter By Location 15 | 16 | Nothing 17 | {{ loc.name }} 18 | 19 | 20 | 21 | 22 | 23 | Filter By Module 24 | 25 | Nothing 26 | {{ mod }} 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | Occurred At 39 | Message 40 | Module 41 | Location 42 | Terminal 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | No items match your search query. 63 | 64 | 65 | 66 | 67 | 68 | 75 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /src/client/pages/settings/error/error.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Error Log 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | Filter By Location 15 | 16 | Nothing 17 | {{ loc.name }} 18 | 19 | 20 | 21 | 22 | 23 | Filter By Module 24 | 25 | Nothing 26 | {{ mod }} 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | Occurred At 39 | Message 40 | Module 41 | Location 42 | Terminal 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | No items match your search query. 63 | 64 | 65 | 66 | 67 | 68 | 75 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /src/server/orm/promotion.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:only-arrow-functions no-invalid-this */ 2 | 3 | import { bookshelf } from '../server'; 4 | import { PromoItem } from './promoitem'; 5 | import { OrganizationalUnit } from './organizationalunit'; 6 | import { InvoicePromo } from './invoicepromo'; 7 | 8 | export const Promotion = bookshelf.Model.extend({ 9 | tableName: 'promo', 10 | hasTimestamps: true, 11 | softDelete: false, 12 | invoicePromos: function() { 13 | return this.hasMany(InvoicePromo, 'promoId'); 14 | }, 15 | promoItems: function() { 16 | return this.hasMany(PromoItem, 'promoId'); 17 | }, 18 | organizationalunit: function() { 19 | return this.belongsTo(OrganizationalUnit, 'organizationalunitId'); 20 | }, 21 | validations: { 22 | name: [ 23 | { 24 | method: 'isRequired', 25 | error: 'Name is required.' 26 | }, 27 | { 28 | method: 'isLength', 29 | args: { min: 1, max: 50 }, 30 | error: 'Your OU name must be between 1 and 50 characters.' 31 | } 32 | ], 33 | description: [ 34 | { 35 | method: 'isLength', 36 | args: { min: 1, max: 500 }, 37 | error: 'Your description must be between 1 and 500 characters.' 38 | } 39 | ], 40 | discountType: [ 41 | { 42 | method: 'isRequired', 43 | error: 'Discount type is required.' 44 | }, 45 | { 46 | method: 'isIn', 47 | args: { arr: ['Dollar', 'Percent'] }, 48 | error: 'Your discount type is invalid.' 49 | } 50 | ], 51 | discountGrouping: [ 52 | { 53 | method: 'isRequired', 54 | error: 'Discount grouping is required.' 55 | }, 56 | { 57 | method: 'isIn', 58 | args: { arr: ['SKU', 'Category'] }, 59 | error: 'Your discount grouping is invalid.' 60 | } 61 | ], 62 | itemReductionType: [ 63 | { 64 | method: 'isRequired', 65 | error: 'Promotion type is required.' 66 | }, 67 | { 68 | method: 'isIn', 69 | args: { arr: ['BuyXGetNext', 'All', 'SetTo'] }, 70 | error: 'Your promotion type is invalid.' 71 | } 72 | ], 73 | discountValue: [ 74 | { 75 | method: 'isNum', 76 | args: { min: 0 }, 77 | error: 'You must specify a value > 0.' 78 | } 79 | ], 80 | numItemsRequired: [ 81 | { 82 | method: 'isRequired', 83 | error: 'Item count is required.' 84 | }, 85 | { 86 | method: 'isNum', 87 | args: { min: 1 }, 88 | error: 'You must specify a value >= 1.' 89 | } 90 | ], 91 | organizationalunitId: [ 92 | { 93 | method: 'isNum', 94 | error: 'You must specify a valid OU.' 95 | } 96 | ], 97 | promoItems: [ 98 | { 99 | method: 'isLength', 100 | args: { min: 0 }, 101 | error: 'You must give an array of size > 0.' 102 | } 103 | ] 104 | } 105 | }); 106 | -------------------------------------------------------------------------------- /src/server/orm/stockitem.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:only-arrow-functions no-invalid-this */ 2 | 3 | import { bookshelf } from '../server'; 4 | import { OrganizationalUnit } from './organizationalunit'; 5 | import { StockItemVendor } from './stockitemvendor'; 6 | 7 | export const StockItem = bookshelf.Model.extend({ 8 | tableName: 'stockitem', 9 | hasTimestamps: true, 10 | softDelete: false, 11 | vendors: function() { 12 | return this.hasMany(StockItemVendor, 'stockitemId'); 13 | }, 14 | organizationalunit: function() { 15 | return this.belongsTo(OrganizationalUnit, 'organizationalunitId'); 16 | }, 17 | validations: { 18 | name: [ 19 | { 20 | method: 'isRequired', 21 | error: 'Name is required.' 22 | }, 23 | { 24 | method: 'isLength', 25 | args: { min: 1, max: 50 }, 26 | error: 'Your category name must be between 1 and 50 characters.' 27 | } 28 | ], 29 | sku: [ 30 | { 31 | method: 'isRequired', 32 | error: 'SKU is required.' 33 | }, 34 | { 35 | method: 'isLength', 36 | args: { min: 1, max: 50 }, 37 | error: 'Your SKU must be between 1 and 50 characters.' 38 | } 39 | ], 40 | stockCode: [ 41 | { 42 | method: 'isLength', 43 | args: { min: 1, max: 50 }, 44 | error: 'Your Stock ID must be between 1 and 50 characters.' 45 | } 46 | ], 47 | description: [ 48 | { 49 | method: 'isLength', 50 | args: { min: 1, max: 500 }, 51 | error: 'Your description must be between 1 and 500 characters.' 52 | } 53 | ], 54 | organizationalunitId: [ 55 | { 56 | method: 'isRequired', 57 | error: 'OU is required.' 58 | }, 59 | { 60 | method: 'isNum', 61 | error: 'You must specify a valid OU.' 62 | } 63 | ], 64 | cost: [ 65 | { 66 | method: 'isRequired', 67 | error: 'Cost is required.' 68 | }, 69 | { 70 | method: 'isNum', 71 | args: { min: 0 }, 72 | error: 'You must specify a positive cost.' 73 | } 74 | ], 75 | vendorPurchasePrice: [ 76 | { 77 | method: 'isNum', 78 | args: { min: 0 }, 79 | error: 'You must specify a positive vendor purchase price.' 80 | } 81 | ], 82 | quantity: [ 83 | { 84 | method: 'isRequired', 85 | error: 'Quantity is required.' 86 | }, 87 | { 88 | method: 'isNum', 89 | args: { min: 0 }, 90 | error: 'You must specify a quantity >= 0.' 91 | } 92 | ], 93 | reorderThreshold: [ 94 | { 95 | method: 'isNum', 96 | args: { min: 0 }, 97 | error: 'You must specify a threshold >= 0.' 98 | } 99 | ], 100 | reorderUpToAmount: [ 101 | { 102 | method: 'isNum', 103 | args: { min: 0 }, 104 | error: 'You must specify an amount >= 0.' 105 | } 106 | ] 107 | } 108 | }); 109 | -------------------------------------------------------------------------------- /hooks/after_prepare/010_add_platform_class.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // Add Platform Class 4 | // v1.0 5 | // Automatically adds the platform class to the body tag 6 | // after the `prepare` command. By placing the platform CSS classes 7 | // directly in the HTML built for the platform, it speeds up 8 | // rendering the correct layout/style for the specific platform 9 | // instead of waiting for the JS to figure out the correct classes. 10 | 11 | var fs = require('fs'); 12 | var path = require('path'); 13 | 14 | var rootdir = process.argv[2]; 15 | 16 | function addPlatformBodyTag(indexPath, platform) { 17 | // create the platform class to the body tag 18 | try { 19 | var platformClass = 'platform-' + platform; 20 | var cordovaClass = 'platform-cordova platform-webview'; 21 | 22 | var html = fs.readFileSync(indexPath, 'utf8'); 23 | 24 | var bodyTag = findBodyTag(html); 25 | if(!bodyTag) return; // no opening body tag, something's wrong 26 | 27 | if(bodyTag.indexOf(platformClass) > -1) return; // already added 28 | 29 | var newBodyTag = bodyTag; 30 | 31 | var classAttr = findClassAttr(bodyTag); 32 | if(classAttr) { 33 | // body tag has existing class attribute, create the classname 34 | var endingQuote = classAttr.substring(classAttr.length-1); 35 | var newClassAttr = classAttr.substring(0, classAttr.length-1); 36 | newClassAttr += ' ' + platformClass + ' ' + cordovaClass + endingQuote; 37 | newBodyTag = bodyTag.replace(classAttr, newClassAttr); 38 | 39 | } else { 40 | // create class attribute to the body tag 41 | newBodyTag = bodyTag.replace('>', ' class="' + platformClass + ' ' + cordovaClass + '">'); 42 | } 43 | 44 | html = html.replace(bodyTag, newBodyTag); 45 | 46 | fs.writeFileSync(indexPath, html, 'utf8'); 47 | 48 | process.stdout.write('create to body class: ' + platformClass + '\n'); 49 | } catch(e) { 50 | process.stdout.write(e); 51 | } 52 | } 53 | 54 | function findBodyTag(html) { 55 | // create the body tag 56 | try{ 57 | return html.match(/])(.*?)>/gi)[0]; 58 | }catch(e){} 59 | } 60 | 61 | function findClassAttr(bodyTag) { 62 | // create the body tag's class attribute 63 | try{ 64 | return bodyTag.match(/ class=["|'](.*?)["|']/gi)[0]; 65 | }catch(e){} 66 | } 67 | 68 | if (rootdir) { 69 | 70 | // go through each of the platform directories that have been prepared 71 | var platforms = (process.env.CORDOVA_PLATFORMS ? process.env.CORDOVA_PLATFORMS.split(',') : []); 72 | 73 | for(var x=0; x { 25 | return this.http.get(this.settings.buildAPIURL(`${this.url}/search`), { search: this.settings.buildSearchParams({ query }) }) 26 | .map((res: Response) => this.logger.observableUnwrap(res.json())) 27 | .catch(e => this.logger.observableError(e)); 28 | } 29 | 30 | getMany(args: any): Observable> { 31 | return this.http.get(this.settings.buildAPIURL(this.url), { search: this.settings.buildSearchParams(args) }) 32 | .map((res: Response) => this.logger.observableUnwrap(res.json())) 33 | .catch(e => this.logger.observableError(e)); 34 | } 35 | 36 | get(item: StockItem): Observable { 37 | return this.http.get(this.settings.buildAPIURL(this.url, item.id)) 38 | .map((res: Response) => this.logger.observableUnwrap(res.json())) 39 | .catch(e => this.logger.observableError(e)); 40 | } 41 | 42 | create(item: StockItem): Observable { 43 | return this.http.put(this.settings.buildAPIURL(this.url), item) 44 | .map((res: Response) => this.logger.observableUnwrap(res.json())) 45 | .catch(e => this.logger.observableError(e)); 46 | } 47 | 48 | update(item: StockItem): Observable { 49 | return this.http.patch(this.settings.buildAPIURL(this.url, item.id), item) 50 | .map((res: Response) => this.logger.observableUnwrap(res.json())) 51 | .catch(e => this.logger.observableError(e)); 52 | } 53 | 54 | remove(item: StockItem): Observable { 55 | return this.http.delete(this.settings.buildAPIURL(this.url, item.id)) 56 | .map((res: Response) => this.logger.observableUnwrap(res.json())) 57 | .catch(e => this.logger.observableError(e)); 58 | } 59 | 60 | private transformItemsToHash(items: StockItem[]): any { 61 | return _.reduce(items, (prev: any, cur: StockItem) => { 62 | prev[cur.sku] = prev[cur.sku] || 0; 63 | prev[cur.sku] += cur.quantity; 64 | return prev; 65 | }, {}); 66 | } 67 | 68 | importMany(items: StockItem[]): Observable { 69 | return this.http.post(this.settings.buildAPIURL(`${this.url}/import`), this.transformItemsToHash(items)) 70 | .map((res: Response) => this.logger.observableUnwrap(res.json())) 71 | .catch(e => this.logger.observableError(e)); 72 | } 73 | 74 | exportMany(items: StockItem[]): Observable { 75 | return this.http.post(this.settings.buildAPIURL(`${this.url}/import`), this.transformItemsToHash(items)) 76 | .map((res: Response) => this.logger.observableUnwrap(res.json())) 77 | .catch(e => this.logger.observableError(e)); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/client/pages/inventory/inventory.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash'; 2 | 3 | import { Component, OnInit } from '@angular/core'; 4 | import { ModalController } from 'ionic-angular'; 5 | 6 | import { InventoryMassManagementComponent } from './ivmanage/ivmanage'; 7 | import { InventoryManagerComponent } from './management/inventory.management'; 8 | import { OUManagerComponent } from './oumanage/ou.management'; 9 | import { QuickComponent } from './quick/quick'; 10 | 11 | import { StockItemService } from '../../services/stockitem.service'; 12 | 13 | import { StockItem } from '../../models/stockitem'; 14 | import { Pagination } from 'ionic2-pagination'; 15 | 16 | import { LocalStorage } from 'ng2-webstorage'; 17 | 18 | @Component({ 19 | selector: 'my-page-inventory', 20 | templateUrl: 'inventory.html' 21 | }) 22 | export class InventoryPageComponent implements OnInit { 23 | 24 | public currentInventoryItems: StockItem[] = []; 25 | public paginationInfo: Pagination; 26 | 27 | public hasSearchResults: boolean = false; 28 | public searchResults: StockItem[] = []; 29 | 30 | @LocalStorage() 31 | public hideOutOfStock: boolean; 32 | 33 | constructor(public modalCtrl: ModalController, 34 | public siService: StockItemService) {} 35 | 36 | ngOnInit() { 37 | this.changePage(1); 38 | } 39 | 40 | toggleOOS() { 41 | if(!this.paginationInfo) { return; } 42 | this.changePage(this.paginationInfo.page); 43 | } 44 | 45 | changePage(newPage) { 46 | this.siService 47 | .getMany({ page: newPage, hideOutOfStock: +this.hideOutOfStock }) 48 | .toPromise() 49 | .then(({ items, pagination }) => { 50 | this.currentInventoryItems = items; 51 | this.paginationInfo = pagination; 52 | }); 53 | } 54 | 55 | openItemModal(item?: StockItem) { 56 | 57 | const openModal = (stockItem: StockItem) => { 58 | const modal = this.modalCtrl.create(InventoryManagerComponent, { 59 | stockItem: stockItem 60 | }, { enableBackdropDismiss: false }); 61 | modal.onDidDismiss(() => { 62 | this.changePage(this.paginationInfo.page); 63 | }); 64 | modal.present(); 65 | }; 66 | 67 | if(!item) { return openModal(new StockItem()); } 68 | 69 | this.siService 70 | .get(item) 71 | .toPromise() 72 | .then(stockItem => { 73 | openModal(stockItem); 74 | }); 75 | } 76 | 77 | openOUModal() { 78 | const modal = this.modalCtrl.create(OUManagerComponent, {}, { enableBackdropDismiss: false }); 79 | modal.present(); 80 | } 81 | 82 | openQuickModal() { 83 | const modal = this.modalCtrl.create(QuickComponent, {}, { enableBackdropDismiss: false }); 84 | modal.present(); 85 | } 86 | 87 | toggleSearchResults(hasResults: boolean) { 88 | this.hasSearchResults = hasResults; 89 | } 90 | 91 | changeSearchResults(results: any) { 92 | this.searchResults = results.items; 93 | } 94 | 95 | importData() { 96 | const modal = this.modalCtrl.create(InventoryMassManagementComponent, { mode: 'Import' }, { enableBackdropDismiss: false }); 97 | modal.present(); 98 | } 99 | 100 | exportData() { 101 | const modal = this.modalCtrl.create(InventoryMassManagementComponent, { mode: 'Export' }, { enableBackdropDismiss: false }); 102 | modal.present(); 103 | } 104 | 105 | } 106 | -------------------------------------------------------------------------------- /src/client/pages/pointofsale/transactionitem.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core'; 3 | import { PopoverController, ViewController, NavParams } from 'ionic-angular'; 4 | 5 | import { StockItem } from '../../models/stockitem'; 6 | 7 | @Component({ 8 | selector: 'transaction-item', 9 | template: ` 10 | 11 | 12 | 13 | #{{ index }} 14 | 15 | 18 | 19 | 20 | 21 | {{ item.name | truncate:50 }} 22 | 23 | 24 | {{ item.description | truncate:50 }} 25 | 26 | 27 | 28 | {{ item.sku }} 29 | 30 | 31 | 34 | 35 | 36 | 37 | {{ item.cost | currencyFromSettings }} 38 | 39 | 40 | 41 | 42 | 43 | `, 44 | styles: [` 45 | [space-between-col] { 46 | justify-content: space-around; 47 | } 48 | `] 49 | }) 50 | export class TransactionItemComponent { 51 | @Input() item: StockItem; 52 | @Input() buttons: any[]; 53 | @Input() index: number; 54 | @Input() disableQuantity: boolean; 55 | 56 | @Output() quantityChange = new EventEmitter(); 57 | 58 | constructor(public popoverCtrl: PopoverController) {} 59 | 60 | updateQuantity(quantity, item) { 61 | item.quantity = quantity; 62 | this.quantityChange.next({ quantity, item }); 63 | } 64 | 65 | moreOptions($event) { 66 | const popover = this.popoverCtrl.create(TransactionItemPopoverComponent, { buttons: this.buttons, item: this.item }); 67 | 68 | popover.present({ 69 | ev: $event 70 | }); 71 | } 72 | } 73 | 74 | @Component({ 75 | template: ` 76 | 77 | 78 | {{ button.text }} 79 | 80 | 81 | ` 82 | }) 83 | export class TransactionItemPopoverComponent implements OnInit { 84 | public buttons: any[]; 85 | private item: StockItem; 86 | 87 | constructor(private navParams: NavParams, private viewCtrl: ViewController) {} 88 | 89 | ngOnInit() { 90 | this.buttons = this.navParams.data.buttons; 91 | this.item = this.navParams.data.item; 92 | } 93 | 94 | doCallback(button) { 95 | button.callback(this.item); 96 | this.viewCtrl.dismiss(); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/client/pages/inventory/management/inventory.management.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash'; 2 | 3 | import { Component } from '@angular/core'; 4 | import { ViewController, AlertController, NavParams } from 'ionic-angular'; 5 | import { StockItem } from '../../../models/stockitem'; 6 | import { StockItemVendor } from '../../../models/stockitemvendor'; 7 | import { OrganizationalUnit } from '../../../models/organizationalunit'; 8 | import { Observable, BehaviorSubject } from 'rxjs/Rx'; 9 | 10 | import { StockItemService } from '../../../services/stockitem.service'; 11 | import { OrganizationalUnitService } from '../../../services/organizationalunit.service'; 12 | 13 | @Component({ 14 | templateUrl: 'inventory.management.html' 15 | }) 16 | export class InventoryManagerComponent { 17 | public stockItem: StockItem; 18 | public allOU: Observable; 19 | 20 | public _formErrors: BehaviorSubject = new BehaviorSubject({}); 21 | public formErrors: Observable = this._formErrors.asObservable(); 22 | 23 | constructor(public viewCtrl: ViewController, 24 | public alertCtrl: AlertController, 25 | public params: NavParams, 26 | public siService: StockItemService, 27 | public ouService: OrganizationalUnitService) { 28 | 29 | this.stockItem = params.get('stockItem'); 30 | this.allOU = this.ouService.getAll(); 31 | } 32 | 33 | dismiss(item?: StockItem) { 34 | this.viewCtrl.dismiss(item); 35 | } 36 | 37 | create() { 38 | if(this.stockItem.temporary) { 39 | return this.dismiss(this.stockItem); 40 | } 41 | 42 | this.siService 43 | .create(this.stockItem) 44 | .subscribe(item => { 45 | this._formErrors.next({}); 46 | this.dismiss(item); 47 | }, e => this._formErrors.next(e.formErrors)); 48 | } 49 | 50 | update() { 51 | this.siService 52 | .update(this.stockItem) 53 | .subscribe(() => { 54 | this._formErrors.next({}); 55 | this.dismiss(this.stockItem); 56 | }, e => this._formErrors.next(e.formErrors)); 57 | } 58 | 59 | private confirmVendor(vendorInfo) { 60 | if(!this.stockItem.vendors) { 61 | this.stockItem.vendors = []; 62 | } 63 | this.stockItem.vendors.push(new StockItemVendor(vendorInfo)); 64 | } 65 | 66 | addVendor() { 67 | let alert = this.alertCtrl.create({ 68 | title: 'Add New Vendor', 69 | inputs: [ 70 | { 71 | name: 'name', 72 | placeholder: 'Vendor Name' 73 | }, 74 | { 75 | name: 'stockId', 76 | placeholder: 'Stock Identification Code' 77 | }, 78 | { 79 | name: 'cost', 80 | placeholder: 'Vendor Cost', 81 | type: 'number' 82 | } 83 | ], 84 | buttons: [ 85 | { 86 | text: 'Cancel' 87 | }, 88 | { 89 | text: 'Confirm', 90 | handler: (vendorData) => { 91 | this.confirmVendor(vendorData); 92 | } 93 | } 94 | ] 95 | }); 96 | 97 | alert.present(); 98 | } 99 | 100 | removeVendor(vendor) { 101 | this.stockItem.vendors = _.reject(this.stockItem.vendors, testVendor => testVendor === vendor); 102 | } 103 | 104 | togglePreferred(toggleVendor) { 105 | _.each(this.stockItem.vendors, vendor => { vendor.isPreferred = false; }); 106 | toggleVendor.isPreferred = true; 107 | } 108 | 109 | } 110 | -------------------------------------------------------------------------------- /src/server/routes/inventory.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as _ from 'lodash'; 3 | 4 | import { bookshelf } from '../server'; 5 | 6 | import { StockItem } from '../orm/stockitem'; 7 | import { OrganizationalUnit } from '../orm/organizationalunit'; 8 | import { OrganizationalUnit as OrganizationalUnitModel } from '../../client/models/organizationalunit'; 9 | import { Logger } from '../logger'; 10 | 11 | import { recordAuditMessage, recordErrorMessageFromServer, MESSAGE_CATEGORIES } from './_logging'; 12 | 13 | const getColumnsAndRelated = (columns) => { 14 | const withRelated = []; 15 | 16 | if(_.includes(columns, 'organizationalunit.name')) { 17 | columns.push('organizationalunitId'); 18 | withRelated.push('organizationalunit'); 19 | _.pull(columns, 'organizationalunit.name'); 20 | } 21 | 22 | return { columns, withRelated }; 23 | }; 24 | 25 | export default (app) => { 26 | app.post('/inventory/export', (req, res) => { 27 | 28 | const { columns, withRelated } = getColumnsAndRelated(req.body.columns); 29 | 30 | StockItem 31 | .collection() 32 | .fetch({ columns, withRelated }) 33 | .then(collection => { 34 | recordAuditMessage(req, MESSAGE_CATEGORIES.INVENTORY, `All inventory was exported.`); 35 | res.json(collection.toJSON()); 36 | }) 37 | .catch(e => { 38 | recordErrorMessageFromServer(req, MESSAGE_CATEGORIES.INVENTORY, e); 39 | res.status(500).json(Logger.browserError(Logger.error('Route:Inventory/export:POST', e))); 40 | }); 41 | }); 42 | 43 | app.post('/inventory/import', (req, res) => { 44 | const items = req.body; 45 | 46 | OrganizationalUnit 47 | .collection() 48 | .fetch() 49 | .then(collection => { 50 | const ous = collection.toJSON(); 51 | 52 | const ouHash = _.reduce(ous, (prev, cur: OrganizationalUnitModel) => { 53 | prev[cur.name] = cur.id; 54 | return prev; 55 | }, {}); 56 | 57 | _.each(items, item => { 58 | if(item.organizationalunit) { 59 | item.organizationalunitId = ouHash[item.organizationalunit.name]; 60 | } 61 | 62 | if(!item.organizationalunitId) { 63 | item.organizationalunitId = 1; 64 | } 65 | 66 | delete item.organizationalunit; 67 | 68 | bookshelf.transaction(t => { 69 | 70 | const errorHandler = (e) => { 71 | if(res.headersSent) { return; } 72 | recordErrorMessageFromServer(req, MESSAGE_CATEGORIES.INVENTORY, e); 73 | res.status(500).json({ flash: Logger.parseDatabaseError(e, 'Import') }); 74 | }; 75 | 76 | const insertPromises = _.map(items, newItem => { 77 | return StockItem 78 | .forge() 79 | .save(newItem, { transacting: t }) 80 | .catch(errorHandler); 81 | }); 82 | 83 | Promise 84 | .all(insertPromises) 85 | .then(t.commit, t.rollback) 86 | .then(() => { 87 | recordAuditMessage(req, MESSAGE_CATEGORIES.INVENTORY, `New inventory was imported.`); 88 | res.json({ flash: `Import successful.`, data: item }); 89 | }) 90 | .catch(errorHandler); 91 | }); 92 | }); 93 | }) 94 | .catch(e => { 95 | recordErrorMessageFromServer(req, MESSAGE_CATEGORIES.INVENTORY, e); 96 | res.status(500).json(Logger.browserError(Logger.error('Route:Inventory/import:POST', e))); 97 | }); 98 | }); 99 | }; 100 | -------------------------------------------------------------------------------- /src/client/services/promotion.service.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as _ from 'lodash'; 3 | 4 | import { PagedItems } from '../models/pageditems'; 5 | import { Promotion } from '../models/promotion'; 6 | import { StockItem } from '../models/stockitem'; 7 | import { InvoicePromo } from '../models/invoicepromo'; 8 | 9 | import { LoggerService } from './logger.service'; 10 | import { ApplicationSettingsService } from './settings.service'; 11 | 12 | import { Injectable } from '@angular/core'; 13 | import { Response } from '@angular/http'; 14 | import { HttpClient } from './override/http.custom'; 15 | import { Observable } from 'rxjs/Rx'; 16 | 17 | @Injectable() 18 | export class PromotionService { 19 | 20 | private url = 'promotion'; 21 | 22 | constructor(private http: HttpClient, 23 | private logger: LoggerService, 24 | private settings: ApplicationSettingsService) {} 25 | 26 | search(query: string): Observable { 27 | return this.http.get(this.settings.buildAPIURL(`${this.url}/search`), { search: this.settings.buildSearchParams({ query }) }) 28 | .map((res: Response) => this.logger.observableUnwrap(res.json())) 29 | .catch(e => this.logger.observableError(e)); 30 | } 31 | 32 | getMany(args: any): Observable> { 33 | return this.http.get(this.settings.buildAPIURL(this.url), { search: this.settings.buildSearchParams(args) }) 34 | .map((res: Response) => this.logger.observableUnwrap(res.json())) 35 | .catch(e => this.logger.observableError(e)); 36 | } 37 | 38 | get(item: Promotion): Observable { 39 | return this.http.get(this.settings.buildAPIURL(this.url, item.id)) 40 | .map((res: Response) => this.logger.observableUnwrap(res.json())) 41 | .catch(e => this.logger.observableError(e)); 42 | } 43 | 44 | create(item: Promotion): Observable { 45 | return this.http.put(this.settings.buildAPIURL(this.url), item) 46 | .map((res: Response) => this.logger.observableUnwrap(res.json())) 47 | .catch(e => this.logger.observableError(e)); 48 | } 49 | 50 | update(item: Promotion): Observable { 51 | return this.http.patch(this.settings.buildAPIURL(this.url, item.id), item) 52 | .map((res: Response) => this.logger.observableUnwrap(res.json())) 53 | .catch(e => this.logger.observableError(e)); 54 | } 55 | 56 | remove(item: Promotion): Observable { 57 | return this.http.delete(this.settings.buildAPIURL(this.url, item.id)) 58 | .map((res: Response) => this.logger.observableUnwrap(res.json())) 59 | .catch(e => this.logger.observableError(e)); 60 | } 61 | 62 | checkFor(items: StockItem[]): Observable { 63 | return this.http.post(this.settings.buildAPIURL(`${this.url}/check`), items) 64 | .map((res: Response) => { 65 | const invoicepromos = this.logger.observableUnwrap(res.json()); 66 | return _.map(invoicepromos, item => this.transformToInvoicePromo(item)); 67 | }) 68 | .catch(e => this.logger.observableError(e)); 69 | } 70 | 71 | createTemporary(promo: Promotion, item: StockItem): Observable { 72 | return this.http.post(this.settings.buildAPIURL(`${this.url}/temporary`), { promo, item }) 73 | .map((res: Response) => { 74 | return this.transformToInvoicePromo(this.logger.observableUnwrap(res.json())); 75 | }) 76 | .catch(e => this.logger.observableError(e)); 77 | } 78 | 79 | private transformToInvoicePromo({ totalDiscount, skus, promo, applyId }: any): InvoicePromo { 80 | const invoicePromo = new InvoicePromo({ 81 | cost: totalDiscount, 82 | promoId: promo.id, 83 | skus, 84 | promoData: promo, 85 | applyId 86 | }); 87 | 88 | invoicePromo.realData = promo; 89 | return invoicePromo; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/client/service-worker.js: -------------------------------------------------------------------------------- 1 | // tick this to make the cache invalidate and update 2 | const CACHE_VERSION = 1; 3 | const CURRENT_CACHES = { 4 | 'read-through': 'read-through-cache-v' + CACHE_VERSION 5 | }; 6 | 7 | self.addEventListener('activate', (event) => { 8 | // Delete all caches that aren't named in CURRENT_CACHES. 9 | // While there is only one cache in this example, the same logic will handle the case where 10 | // there are multiple versioned caches. 11 | const expectedCacheNames = Object.keys(CURRENT_CACHES).map((key) => { 12 | return CURRENT_CACHES[key]; 13 | }); 14 | 15 | event.waitUntil( 16 | caches.keys().then((cacheNames) => { 17 | return Promise.all( 18 | cacheNames.map((cacheName) => { 19 | if (expectedCacheNames.indexOf(cacheName) === -1) { 20 | // If this cache name isn't present in the array of "expected" cache names, then delete it. 21 | console.log('Deleting out of date cache:', cacheName); 22 | return caches.delete(cacheName); 23 | } 24 | }) 25 | ); 26 | }) 27 | ); 28 | }); 29 | 30 | // This sample illustrates an aggressive approach to caching, in which every valid response is 31 | // cached and every request is first checked against the cache. 32 | // This may not be an appropriate approach if your web application makes requests for 33 | // arbitrary URLs as part of its normal operation (e.g. a RSS client or a news aggregator), 34 | // as the cache could end up containing large responses that might not end up ever being accessed. 35 | // Other approaches, like selectively caching based on response headers or only caching 36 | // responses served from a specific domain, might be more appropriate for those use cases. 37 | self.addEventListener('fetch', (event) => { 38 | 39 | event.respondWith( 40 | caches.open(CURRENT_CACHES['read-through']).then((cache) => { 41 | return cache.match(event.request).then((response) => { 42 | if (response) { 43 | // If there is an entry in the cache for event.request, then response will be defined 44 | // and we can just return it. 45 | 46 | return response; 47 | } 48 | 49 | // Otherwise, if there is no entry in the cache for event.request, response will be 50 | // undefined, and we need to fetch() the resource. 51 | console.log(' No response for %s found in cache. ' + 52 | 'About to fetch from network...', event.request.url); 53 | 54 | // We call .clone() on the request since we might use it in the call to cache.put() later on. 55 | // Both fetch() and cache.put() "consume" the request, so we need to make a copy. 56 | // (see https://fetch.spec.whatwg.org/#dom-request-clone) 57 | return fetch(event.request.clone()).then((response) => { 58 | 59 | // Optional: create in extra conditions here, e.g. response.type == 'basic' to only cache 60 | // responses from the same domain. See https://fetch.spec.whatwg.org/#concept-response-type 61 | if (response.status < 400 && response.type === 'basic') { 62 | // We need to call .clone() on the response object to save a copy of it to the cache. 63 | // (https://fetch.spec.whatwg.org/#dom-request-clone) 64 | cache.put(event.request, response.clone()); 65 | } 66 | 67 | // Return the original response object, which will be used to fulfill the resource request. 68 | return response; 69 | }); 70 | }).catch((error) => { 71 | // This catch() will handle exceptions that arise from the match() or fetch() operations. 72 | // Note that a HTTP error response (e.g. 404) will NOT trigger an exception. 73 | // It will return a normal response object that has the appropriate error code set. 74 | console.error(' Read-through caching failed:', error); 75 | 76 | throw error; 77 | }); 78 | }) 79 | ); 80 | }); 81 | -------------------------------------------------------------------------------- /src/client/app/app.scss: -------------------------------------------------------------------------------- 1 | // http://ionicframework.com/docs/v2/theming/ 2 | 3 | 4 | // App Global Sass 5 | // -------------------------------------------------- 6 | // Put style rules here that you want to apply globally. These 7 | // styles are for the entire app and not just one component. 8 | // Additionally, this file can be also used as an entry point 9 | // to import other Sass files to be included in the output CSS. 10 | // 11 | // Shared Sass variables, which can be used to adjust Ionic's 12 | // default Sass variables, belong in "theme/variables.scss". 13 | // 14 | // To declare rules for a specific mode, create a child rule 15 | // for the .md, .ios, or .wp mode classes. The mode class is 16 | // automatically applied to the element in the app. 17 | 18 | [full-height] { 19 | height: 100%; 20 | } 21 | 22 | ion-grid[vertical-center] { 23 | align-items: center; 24 | justify-content: center; 25 | height: 100%; 26 | } 27 | 28 | [main-button] { 29 | 30 | height: 30%; 31 | 32 | button { 33 | height: 100%; 34 | } 35 | } 36 | 37 | [no-border-top] { 38 | border-top: none !important; 39 | 40 | & .item-inner { 41 | border-top: none !important; 42 | } 43 | } 44 | 45 | [no-border-bottom] { 46 | border-bottom: none !important; 47 | 48 | & .item-inner { 49 | border-bottom: none !important; 50 | } 51 | } 52 | 53 | [no-margin-bottom] { 54 | margin-bottom: 0 !important; 55 | } 56 | 57 | ion-item[no-margin] .label { 58 | margin: 0 !important; 59 | } 60 | 61 | [full-height] { 62 | height: 100%; 63 | } 64 | 65 | [true-center] { 66 | display: flex; 67 | align-items: center; 68 | justify-content: center; 69 | } 70 | 71 | [flex-right] { 72 | justify-content: flex-end; 73 | } 74 | 75 | [vertical-center] { 76 | display: flex; 77 | align-items: center; 78 | } 79 | 80 | [vertical-bottom] { 81 | display: flex; 82 | align-items: flex-end; 83 | } 84 | 85 | [big-text] { 86 | font-size: 300%; 87 | } 88 | 89 | [background-text] { 90 | color: #999; 91 | } 92 | 93 | [list-header] { 94 | border-bottom: 1px solid #dedede; 95 | } 96 | 97 | [shrunk-item-checkbox] { 98 | padding: 0 !important; 99 | min-height: 0 !important; 100 | 101 | ion-checkbox { 102 | margin: 0 !important; 103 | } 104 | 105 | .item-inner { 106 | 107 | .label { 108 | text-align: left; 109 | margin-left: 5px !important; 110 | } 111 | } 112 | 113 | &[label-right] { 114 | .item-inner { 115 | .label { 116 | text-align: right; 117 | } 118 | } 119 | } 120 | } 121 | 122 | [form-item] { 123 | &[required] > .item-inner > .input-wrapper > .label::after { 124 | content: ' *' 125 | } 126 | } 127 | 128 | [small-item] { 129 | font-size: 1.2rem; 130 | min-height: 2.2rem; 131 | 132 | .item-inner { 133 | border-bottom: none !important; 134 | } 135 | 136 | &:not([small-form-item-header]) .label { 137 | margin: 0; 138 | } 139 | } 140 | 141 | [noScroll] .scroll-content { 142 | overflow: hidden; 143 | } 144 | 145 | [text-center] { 146 | justify-content: center; 147 | } 148 | 149 | [text-right] { 150 | justify-content: flex-end; 151 | text-align: right !important; 152 | } 153 | 154 | [column-headers] { 155 | ion-col { 156 | font-weight: bold; 157 | } 158 | } 159 | 160 | [scroll-grid] { 161 | height: 100%; 162 | 163 | [scroll-row] { 164 | overflow: auto; 165 | flex: 1; 166 | } 167 | } 168 | 169 | [flex] { 170 | flex: 1; 171 | } 172 | 173 | [flex-wrap] { 174 | flex-wrap: wrap; 175 | } 176 | 177 | [invisible] { 178 | visibility: hidden !important; 179 | } 180 | 181 | .datetime-md { 182 | padding-top: 8px; 183 | padding-bottom: 8px; 184 | } 185 | 186 | [width-40] { 187 | flex: 0 0 40%; 188 | max-width: 40%; 189 | } 190 | 191 | [width-30] { 192 | flex: 0 0 30%; 193 | max-width: 30%; 194 | } 195 | 196 | .scroll-content { 197 | overflow-y: auto; 198 | } 199 | -------------------------------------------------------------------------------- /src/client/pages/promotions/promotions.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash'; 2 | 3 | import { Component, OnInit } from '@angular/core'; 4 | 5 | import { ModalController, AlertController } from 'ionic-angular'; 6 | 7 | import { Pagination } from 'ionic2-pagination'; 8 | import { LocalStorage } from 'ng2-webstorage'; 9 | 10 | import { Promotion } from '../../models/promotion'; 11 | import { PromotionService } from '../../services/promotion.service'; 12 | import { PromotionsManagerComponent } from './management/promotions.management'; 13 | 14 | @Component({ 15 | selector: 'my-page-promotions', 16 | templateUrl: 'promotions.html' 17 | }) 18 | export class PromotionsPageComponent implements OnInit { 19 | 20 | @LocalStorage() 21 | public hideCurrent: boolean; 22 | 23 | @LocalStorage() 24 | public hideFuture: boolean; 25 | 26 | @LocalStorage() 27 | public hidePast: boolean; 28 | 29 | public hasResults: boolean; 30 | 31 | public promotions = { past: [], current: [], future: [] }; 32 | public paginationInfo: Pagination; 33 | 34 | constructor(public modalCtrl: ModalController, 35 | public alertCtrl: AlertController, 36 | public prService: PromotionService) {} 37 | 38 | ngOnInit() { 39 | this.changePage(1); 40 | } 41 | 42 | toggleHide() { 43 | if(!this.paginationInfo) { return; } 44 | this.changePage(this.paginationInfo.page); 45 | } 46 | 47 | categorizePromotions(promotions: Promotion[]) { 48 | const now = new Date(); 49 | 50 | const pastPromotions = _.filter(promotions, promo => { 51 | return new Date(promo.endDate) < now; 52 | }); 53 | 54 | const currentPromotions = _.filter(promotions, promo => { 55 | return new Date(promo.startDate) < now && new Date(promo.endDate) > now; 56 | }); 57 | 58 | const futurePromotions = _.filter(promotions, promo => { 59 | return new Date(promo.startDate) > now; 60 | }); 61 | 62 | this.hasResults = pastPromotions.length !== 0 || currentPromotions.length !== 0 || futurePromotions.length !== 0; 63 | 64 | this.promotions = { 65 | past: pastPromotions, 66 | current: currentPromotions, 67 | future: futurePromotions 68 | }; 69 | } 70 | 71 | changePage(newPage) { 72 | this.prService 73 | .getMany({ page: newPage, hidePast: +this.hidePast, hideCurrent: +this.hideCurrent, hideFuture: +this.hideFuture }) 74 | .toPromise() 75 | .then(({ items, pagination }) => { 76 | this.categorizePromotions(items); 77 | this.paginationInfo = pagination; 78 | }); 79 | } 80 | 81 | openPromoModal(promo?: Promotion) { 82 | 83 | const openModal = (promoItem: Promotion) => { 84 | const modal = this.modalCtrl.create(PromotionsManagerComponent, { 85 | promotion: promoItem 86 | }, { enableBackdropDismiss: false }); 87 | modal.onDidDismiss(() => { 88 | this.changePage(this.paginationInfo.page); 89 | }); 90 | modal.present(); 91 | }; 92 | 93 | if(!promo) { return openModal(new Promotion()); } 94 | 95 | this.prService 96 | .get(promo) 97 | .toPromise() 98 | .then(promotion => { 99 | openModal(promotion); 100 | }); 101 | } 102 | 103 | removePromo(item) { 104 | const confirm = this.alertCtrl.create({ 105 | title: `Remove Promotion "${item.name}"?`, 106 | message: 'This is irreversible and unrecoverable. This promotion will be removed.', 107 | buttons: [ 108 | { 109 | text: 'Cancel' 110 | }, 111 | { 112 | text: 'Confirm', 113 | handler: () => { 114 | this.prService 115 | .remove(item) 116 | .toPromise() 117 | .then(() => { 118 | this.changePage(this.paginationInfo.page); 119 | }); 120 | } 121 | } 122 | ] 123 | }); 124 | confirm.present(); 125 | } 126 | 127 | } 128 | -------------------------------------------------------------------------------- /src/client/pages/promotions/promotions.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Posys Promotions 4 | 5 | 6 | 7 | 8 | 9 | 12 | 13 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | Hide Current 25 | 26 | 27 | 28 | 29 | 30 | 31 | Hide Future 32 | 33 | 34 | 35 | 36 | 37 | 38 | Hide Past 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | Name 51 | Start 52 | End 53 | Discount 54 | Affected 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | No search results. 68 | 69 | 70 | Current Promotions 71 | 72 | 73 | 74 | 75 | Future Promotions 76 | 77 | 78 | 79 | 80 | Past Promotions 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 97 | 98 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /src/client/pages/inventory/inventory.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Posys Inventory 4 | 5 | 6 | 7 | 8 | 9 | 12 | 13 | 16 | 19 | 22 | 25 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 40 | 41 | 42 | 43 | Hide Out of Stock Items 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | Product 56 | Category 57 | SKU 58 | Quantity 59 | Consumer Cost 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | No items match your search query. 93 | 94 | 95 | 96 | 97 | 98 | 105 | 106 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /src/client/pages/invoices/view/invoice.view.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash'; 2 | 3 | import { Component } from '@angular/core'; 4 | import { ViewController, AlertController, LoadingController, App, NavParams } from 'ionic-angular'; 5 | import { Invoice } from '../../../models/invoice'; 6 | 7 | import { PointOfSalePageComponent } from '../../pointofsale/pointofsale'; 8 | 9 | import { ApplicationSettingsService } from '../../../services/settings.service'; 10 | import { InvoiceService } from '../../../services/invoice.service'; 11 | 12 | @Component({ 13 | templateUrl: 'invoice.view.html' 14 | }) 15 | export class InvoiceViewComponent { 16 | public invoice: Invoice; 17 | 18 | constructor(public viewCtrl: ViewController, 19 | public appCtrl: App, 20 | public alertCtrl: AlertController, 21 | public loadingCtrl: LoadingController, 22 | public params: NavParams, 23 | public ivService: InvoiceService, 24 | public settings: ApplicationSettingsService) { 25 | 26 | this.invoice = params.get('invoice'); 27 | _.each(this.invoice.stockitems, item => item.realData = this.invoiceItemData(item)); 28 | _.each(this.invoice.promotions, item => item.realData = this.invoicePromoData(item)); 29 | _.each(this.invoice.invoices, invoice => { 30 | _.each(invoice.stockitems, item => item.realData = this.invoiceItemData(item)); 31 | }); 32 | } 33 | 34 | printReceipt() { 35 | this.ivService 36 | .print(this.invoice, true) 37 | .toPromise(); 38 | } 39 | 40 | invoiceItemData(item) { 41 | if(!_.isEmpty(item.stockitemData)) { return item.stockitemData; } 42 | return item._stockitemData; 43 | } 44 | 45 | invoicePromoData(item) { 46 | if(!_.isEmpty(item.promoData)) { return item.promoData; } 47 | return item._promoData; 48 | } 49 | 50 | totalItems() { 51 | return _.reduce(this.invoice.stockitems, (prev, cur) => prev + cur.quantity, 0); 52 | } 53 | 54 | resumeTransaction(isReturn = false) { 55 | const rootNav = this.appCtrl.getRootNav(); 56 | rootNav 57 | .popToRoot() 58 | .then(() => { 59 | this.dismiss(); 60 | rootNav.push(PointOfSalePageComponent, { prevInvoice: this.invoice, isReturn }); 61 | }); 62 | } 63 | 64 | dismiss(item?: Invoice) { 65 | this.viewCtrl.dismiss(item); 66 | } 67 | 68 | taxForItem(item): number { 69 | return (this.settings.taxRate / 100) * item.realData.cost * item.quantity; 70 | } 71 | 72 | totalCostForItem(item): number { 73 | return (item.realData.cost * item.quantity) + (item.taxable ? this.taxForItem(item) : 0); 74 | } 75 | 76 | canReturn() { 77 | const itemCount = _.sumBy(this.invoice.stockitems, 'quantity'); 78 | const returnedItemCount = _.reduce(this.invoice.invoices, (prev, cur) => { 79 | return prev + _.sumBy(cur.stockitems, 'quantity'); 80 | }, 0); 81 | 82 | return itemCount !== returnedItemCount; 83 | } 84 | 85 | returnedItems() { 86 | return _.compact(_.flattenDeep(_.map(this.invoice.invoices, 'stockitems'))); 87 | } 88 | 89 | toggleVoid() { 90 | 91 | const voidText = 'Are you sure you want to void this transaction? It will re-stock the items listed in the invoice.'; 92 | const unvoidText = 'Are you sure you want to un-void this transaction? It will again deduct the items listed in the invoice.'; 93 | 94 | const loading = this.loadingCtrl.create({ 95 | content: 'Please wait...' 96 | }); 97 | 98 | const confirm = this.alertCtrl.create({ 99 | title: `${this.invoice.isVoided ? 'Un-void' : 'Void'} Invoice?`, 100 | message: this.invoice.isVoided ? unvoidText : voidText, 101 | buttons: [ 102 | { 103 | text: 'Cancel' 104 | }, 105 | { 106 | text: 'Confirm', 107 | handler: () => { 108 | loading.present(); 109 | 110 | this.ivService 111 | .toggleVoid(this.invoice) 112 | .toPromise() 113 | .then(res => { 114 | this.invoice.isVoided = res.isVoided; 115 | loading.dismiss(); 116 | }); 117 | } 118 | } 119 | ] 120 | }); 121 | 122 | confirm.present(); 123 | } 124 | 125 | } 126 | --------------------------------------------------------------------------------