├── .dockerignore ├── config ├── search │ ├── wordforms.txt │ └── sphinx.conf ├── php-fpm-log.conf └── php-fpm-extra.conf ├── ui ├── pos │ ├── print │ │ ├── return-policy.html │ │ └── inventory.html │ ├── report │ │ ├── kit-items.html │ │ ├── backordered-items.html │ │ ├── empty-products.html │ │ ├── dogs.html │ │ ├── inventory-by-brand.html │ │ ├── shipments.html │ │ ├── inventory-value.html │ │ └── drop-by-drop.html │ ├── layout │ │ ├── print.css │ │ ├── dialog.html │ │ └── print.html │ ├── email │ │ ├── invoice.html │ │ ├── gift-card.html │ │ ├── abandoned-cart.html │ │ ├── delivery.html │ │ ├── out_for_delivery.html │ │ └── available_for_pickup.html │ ├── 404.html │ ├── ad.twig │ ├── push │ │ └── service-worker.js │ ├── catalog │ │ ├── whatsnew.html │ │ └── products.twig │ ├── settings │ │ ├── nav.twig │ │ ├── shipping.html │ │ ├── wordforms.html │ │ └── messages.html │ ├── dialog │ │ ├── file-upload.html │ │ ├── item-shipping-estimates.html │ │ ├── pay-discount.html │ │ ├── report-quick.html │ │ ├── pay-other.html │ │ ├── pay-cash.html │ │ ├── wordform.html │ │ ├── item-google-history.html │ │ ├── punch.html │ │ ├── tracker.html │ │ ├── dropship.html │ │ ├── person-loyalty.html │ │ └── email-gift-card.html │ ├── person │ │ ├── searchform.twig │ │ ├── backorders.html │ │ └── index.html │ ├── gift-card │ │ └── index.html │ ├── shipping │ │ └── shipment.html │ └── quickbooks │ │ └── accounts.html ├── shared │ └── logo.png └── web │ ├── robots.txt │ ├── layout │ ├── tracking-body.twig │ ├── userway.twig │ ├── tracking.twig │ ├── newsletter.twig │ └── google-rating.twig │ ├── catalog │ ├── sitemap.xml │ ├── whatsnew.html │ ├── searchresults.html │ ├── product.twig │ └── brand.html │ ├── email │ ├── login-link.html │ ├── get-help.html │ └── contact.html │ ├── cart │ ├── backorder-warning.twig │ ├── add-to-cart-dialog.html │ └── get-help.html │ ├── sitemap.xml │ ├── backroom │ └── ads.html │ ├── sale │ └── sale.html │ ├── 404.html │ ├── paymarks │ ├── cc-visa.svg │ ├── cc-discover.svg │ └── cc-paypal.svg │ ├── ad.twig │ ├── index.html │ ├── edit.html │ └── wishlist │ └── shared.html ├── lib ├── Scat │ ├── Model │ │ ├── Page.php │ │ ├── CcTrace.php │ │ ├── Config.php │ │ ├── Signup.php │ │ ├── AuthToken.php │ │ ├── Redirect.php │ │ ├── Wordform.php │ │ ├── CannedMessage.php │ │ ├── ImageItem.php │ │ ├── ImageProduct.php │ │ ├── InternalAdProduct.php │ │ ├── InternalAdDepartment.php │ │ ├── Brand.php │ │ ├── LoyaltyReward.php │ │ ├── TimeclockAudit.php │ │ ├── Loyalty.php │ │ ├── KitItem.php │ │ ├── Timeclock.php │ │ ├── Shipment.php │ │ ├── Wishlist.php │ │ ├── Device.php │ │ ├── WebPushSubscription.php │ │ ├── Note.php │ │ ├── InternalAd.php │ │ ├── Address.php │ │ ├── VendorItem.php │ │ └── PriceOverride.php │ ├── Exception │ │ ├── HttpConflictException.php │ │ └── FileUploadException.php │ ├── Middleware │ │ ├── NoCache.php │ │ └── NoIndex.php │ ├── Controller │ │ └── Scale.php │ ├── Service │ │ ├── PoleDisplay.php │ │ ├── Fraud.php │ │ ├── Config.php │ │ ├── Giftcard.php │ │ ├── Cart.php │ │ ├── Dejavoo.php │ │ └── Newsletter.php │ ├── Distance.php │ ├── Search │ │ └── Lexer.php │ └── JsonErrorRenderer.php ├── dummy-fraud-checker.php ├── dummy-cc-terminal.php └── fraud-checker.phpc ├── static ├── icon.iconset │ ├── icon_16x16.png │ ├── icon_32x32.png │ ├── icon_128x128.png │ ├── icon_16x16@2x.png │ ├── icon_256x256.png │ ├── icon_32x32@2x.png │ ├── icon_512x512.png │ ├── icon_128x128@2x.png │ └── icon_256x256@2x.png ├── limited-quantity-label.png ├── css │ └── bootstrap-icons-1.8.2 │ │ └── fonts │ │ ├── bootstrap-icons.woff │ │ └── bootstrap-icons.woff2 ├── limited-quantity.svg ├── js │ └── dummy-zaraz.js ├── error.html └── sitemap-pages.xml ├── .gitignore ├── sample.env ├── .gitattributes ├── extern ├── x-editable-1.5.1 │ ├── bootstrap3-editable │ │ └── img │ │ │ ├── clear.png │ │ │ └── loading.gif │ ├── inputs-ext │ │ ├── address │ │ │ └── address.css │ │ ├── wysihtml5 │ │ │ └── bootstrap-wysihtml5-0.0.2 │ │ │ │ └── wysiwyg-color.css │ │ └── typeaheadjs │ │ │ └── lib │ │ │ └── typeahead.js-bootstrap.css │ └── LICENSE-MIT ├── mousetrap-1.6.2 │ └── plugins │ │ └── global-bind │ │ ├── mousetrap-global-bind.min.js │ │ └── README.md └── select2-bootstrap-theme-0.1.0-beta.10 │ └── LICENSE ├── db ├── init │ └── 001-init.sh ├── migrations │ ├── 20190526003715_remove_txn_note.php │ ├── 20200416210541_fix_item_brand.php │ ├── 20200416211304_fix_payment_txn.php │ ├── 20191116175729_longer_image_uuid.php │ ├── 20211002194125_fix_timeclock_audit_entry.php │ ├── 20200417043645_add_brand_fulltext_index.php │ ├── 20200927224555_add_image_fulltext_index.php │ ├── 20220526184322_remove_item_drophip_fee.php │ ├── 20200416221719_fix_txn_line_foreign_keys.php │ ├── 20200417001852_fix_txn_foreign_keys.php │ ├── 20211202195401_make_vendor_item_vendor_sku_unique_key.php │ ├── 20200416214009_fix_vendor_item_foreign_keys.php │ ├── 20200517042944_fix_shipment_id_fields.php │ ├── 20200527203740_add_media_b2_file_id.php │ ├── 20200510002927_add_txn_online_sale_id.php │ ├── 20230606201015_add_internal_ad_fulltext_index.php │ ├── 20210924183832_fix_shipment_handling_instructions.php │ ├── 20210924174456_add_shipment_ship_district_method.php │ ├── 20210924175202_add_shipment_handling_instructions.php │ ├── 20200512182114_add_timezone_to_address.php │ ├── 20200516014830_add_payment_data.php │ ├── 20220121182700_add_cc_to_person.php │ ├── 20200501033926_add_note_type.php │ ├── 20201005213557_add_kit_quantity.php │ ├── 20210927172226_add_note_full_content.php │ ├── 20231010222501_add_expires_at_to_internal_ad.php │ ├── 20200409223318_default_tax_rate.php │ ├── 20210210003751_add_tax_exemption_to_person.php │ ├── 20210925173629_add_person_gift_card.php │ ├── 20191120204658_add_brand_description.php │ ├── 20200502225317_add_image_publitio_id.php │ ├── 20210306183903_add_no_backorder_to_item.php │ ├── 20220108014155_add_salsify_to_person.php │ ├── 20200830005627_add_shipping_method.php │ ├── 20210207234729_add_txn_line_returned_from_id.php │ ├── 20200110001431_add_address_to_txn.php │ ├── 20200208201034_add_qb_ids.php │ ├── 20220524195406_add_captured_to_payment.php │ ├── 20210307221349_add_warning_to_brand.php │ ├── 20200125194502_add_config_table.php │ ├── 20200429051210_config_type_password.php │ ├── 20221118191824_add_item_no_online_sale.php │ ├── 20191104010342_add_price_override_in_stock.php │ ├── 20200209003421_fix_qb_je_id.php │ ├── 20200406234429_fix_timeclock_person.php │ ├── 20211109210154_add_item_dropship_fee.php │ ├── 20210207215627_add_tax_captured_to_txn.php │ ├── 20220816201340_add_item_description.php │ ├── 20221005183725_add_packaged_for_shipping_to_item.php │ ├── 20230916233339_add_image_use_as_swatch.php │ ├── 20211020024653_add_product_importance.php │ ├── 20220611214355_add_featured_to_department.php │ ├── 20200501232539_add_image_caption_and_data.php │ ├── 20200517011542_add_shipment_status_pending.php │ ├── 20210624190738_add_shipping_cancelling_state.php │ ├── 20200523233953_create_item_to_image.php │ ├── 20190602205056_add_image_times.php │ ├── 20200505180239_add_venmo_pay_method.php │ ├── 20200415005639_add_barcode_id.php │ ├── 20210115220541_add_postmates_pay_method.php │ ├── 20200416204639_barcode_item_id_and_dates.php │ ├── 20200720190934_add_more_txn_status.php │ ├── 20220803231224_add_google_product_categories.php │ ├── 20200206191248_add_device_table.php │ ├── 20191114022946_add_eventbrite_pay_method.php │ ├── 20200927005057_add_rewards_payment_type.php │ ├── 20200428205907_add_config_type.php │ ├── 20210923174040_add_address_lat_long_verified.php │ ├── 20230925230029_add_web_push_notications.php │ ├── 20220621190107_add_image_id_indexes.php │ ├── 20200506191152_add_shipments_table.php │ ├── 20191210235333_add_active_default.php │ ├── 20200508230440_add_txn_status.php │ ├── 20200824230213_add_canned_emails.php │ ├── 20200411021256_add_giftcard_times.php │ ├── 20190525204400_initial_functions.php │ ├── 20200430172852_add_person_rewards_plus_newsletter.php │ ├── 20191210233744_add_brand_department_active_and_times.php │ ├── 20201005192908_add_kits.php │ └── 20200110000112_add_address_table.php └── seeds │ └── DemoSeeder.php ├── phpstan.neon ├── bin ├── count-loc ├── export-sql-structure ├── encrypt-code └── decrypt-code ├── phinx.yml ├── deploy.php ├── LICENSE ├── docker-compose.yml ├── Dockerfile └── README.md /.dockerignore: -------------------------------------------------------------------------------- 1 | scratch 2 | -------------------------------------------------------------------------------- /config/search/wordforms.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/pos/print/return-policy.html: -------------------------------------------------------------------------------- 1 | All sales are final as of November 3, 2023. 2 | -------------------------------------------------------------------------------- /ui/shared/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jimwins/scat/HEAD/ui/shared/logo.png -------------------------------------------------------------------------------- /lib/Scat/Model/Page.php: -------------------------------------------------------------------------------- 1 | has_many('Product'); 7 | } 8 | } 9 | 10 | -------------------------------------------------------------------------------- /lib/Scat/Model/LoyaltyReward.php: -------------------------------------------------------------------------------- 1 | belongs_to('Item')->find_one(); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /db/init/001-init.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | mysqladmin -uroot -p${MYSQL_ROOT_PASSWORD} create scat 4 | mysql -uroot -p${MYSQL_ROOT_PASSWORD} \ 5 | -e "GRANT ALL PRIVILEGES ON scat.* TO '${MYSQL_USER}'@'%';" 6 | -------------------------------------------------------------------------------- /extern/x-editable-1.5.1/inputs-ext/address/address.css: -------------------------------------------------------------------------------- 1 | .editable-address { 2 | display: block; 3 | margin-bottom: 5px; 4 | } 5 | 6 | .editable-address span { 7 | width: 70px; 8 | display: inline-block; 9 | } -------------------------------------------------------------------------------- /lib/Scat/Model/TimeclockAudit.php: -------------------------------------------------------------------------------- 1 | belongs_to('Timeclock')->find_one(); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | bootstrapFiles: 3 | - lib/dummy-cc-terminal.php 4 | - lib/dummy-fraud-checker.php 5 | universalObjectCratesClasses: 6 | - Scat\Model 7 | level: 1 8 | paths: 9 | - app 10 | - lib 11 | -------------------------------------------------------------------------------- /lib/dummy-fraud-checker.php: -------------------------------------------------------------------------------- 1 | 4 | {% endif %} 5 | 6 | -------------------------------------------------------------------------------- /lib/Scat/Model/Loyalty.php: -------------------------------------------------------------------------------- 1 | belongs_to('Person'); 7 | } 8 | public function txn() { 9 | return $this->belongs_to('Txn'); 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /lib/Scat/Model/KitItem.php: -------------------------------------------------------------------------------- 1 | belongs_to('Item', 'kit_id')->find_one(); 7 | } 8 | 9 | public function item() { 10 | return $this->belongs_to('Item')->find_one(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /lib/Scat/Model/Timeclock.php: -------------------------------------------------------------------------------- 1 | belongs_to('Person')->find_one(); 7 | } 8 | 9 | public function changes() { 10 | return $this->has_many('TimeclockAudit')->find_many(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /config/php-fpm-extra.conf: -------------------------------------------------------------------------------- 1 | [global] 2 | ;access.format = "%{%FT%T%z}t %{REMOTE_ADDR}e %m %{REQUEST_URI_PATH}e %s %{mili}d" 3 | 4 | [www] 5 | catch_workers_output = On 6 | decorate_workers_output = On 7 | chdir = /app 8 | php_admin_value[upload_max_filesize] = 100M 9 | php_admin_value[post_max_size] = 100M 10 | ping.path = /ping 11 | -------------------------------------------------------------------------------- /lib/Scat/Model/Shipment.php: -------------------------------------------------------------------------------- 1 | belongs_to('Txn')->find_one(); 7 | } 8 | 9 | public function dimensions() { 10 | return "{$this->length}x{$this->width}x{$this->height}"; 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /bin/count-loc: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | dirs="$@" 4 | if [ -z "$dirs" ]; then 5 | dirs="app css db js lib static ui" 6 | fi 7 | 8 | find $dirs \ 9 | -type f \ 10 | -not -name '*.png' \ 11 | -not -name '*.svg' \ 12 | -not -path '*bootstrap-icons*' \ 13 | -not -name 'normalize.css' \ 14 | -not -name 'jquery*' \ 15 | | xargs wc -l 16 | -------------------------------------------------------------------------------- /db/migrations/20190526003715_remove_txn_note.php: -------------------------------------------------------------------------------- 1 | table('txn_note')->drop()->save(); 10 | } 11 | 12 | // No down() since we never really want to recreate txn_note 13 | } 14 | -------------------------------------------------------------------------------- /extern/mousetrap-1.6.2/plugins/global-bind/mousetrap-global-bind.min.js: -------------------------------------------------------------------------------- 1 | (function(a){var c={},d=a.prototype.stopCallback;a.prototype.stopCallback=function(e,b,a,f){return this.paused?!0:c[a]||c[f]?!1:d.call(this,e,b,a)};a.prototype.bindGlobal=function(a,b,d){this.bind(a,b,d);if(a instanceof Array)for(b=0;b{{ block('title') }} 10 | 11 |

An error occured. Something obviously went wrong.

12 | 13 |

Try it again from the top.

14 | 15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /bin/export-sql-structure: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | mysqldump --routines --no-data \ 3 | --ignore-table=scat.vendor_order \ 4 | --ignore-table=scat.vendor_upload \ 5 | scat | \ 6 | sed -e '1,2d' | \ 7 | sed -e 's/Host: .*Data/Data/' | \ 8 | sed -e 's/\(Dump completed \).*/\1/' | \ 9 | sed -e 's/ AUTO_INCREMENT=[0-9]*//' \ 10 | > scat.sql 11 | -------------------------------------------------------------------------------- /db/migrations/20200416210541_fix_item_brand.php: -------------------------------------------------------------------------------- 1 | table('item', [ 'signed' => false ]); 10 | $table 11 | ->renameColumn('brand', 'brand_id') 12 | ->save(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /db/migrations/20200416211304_fix_payment_txn.php: -------------------------------------------------------------------------------- 1 | table('payment', [ 'signed' => false ]); 10 | $table 11 | ->renameColumn('txn', 'txn_id') 12 | ->save(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /ui/web/catalog/sitemap.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% for product in products %} 4 | 5 | {{ full_url_for('catalog', product.url_params) }} 6 | {{ product.modified|date('Y-m-d\\Th:i:s\\Z') }} 7 | 8 | {% endfor %} 9 | 10 | -------------------------------------------------------------------------------- /ui/web/email/login-link.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout/email.html' %} 2 | 3 | {% block title -%} 4 | Log in to your account 5 | {%- endblock %} 6 | 7 | {% block content_top %} 8 |

Here is a link to log in to your account on our website:

9 | {% endblock %} 10 | 11 | {% set call_to_action_url= link %} 12 | 13 | {% block call_to_action %} 14 | Log in 15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /db/migrations/20191116175729_longer_image_uuid.php: -------------------------------------------------------------------------------- 1 | table('image', [ 'signed' => false ]); 10 | $table 11 | ->changeColumn('uuid', 'string', [ 'limit' => 255 ]) 12 | ->update(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /bin/encrypt-code: -------------------------------------------------------------------------------- 1 | #!/usr/bin/php 2 | table('timeclock_audit', [ 'signed' => false ]); 10 | $table 11 | ->renameColumn('entry', 'timeclock_id') 12 | ->update(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /ui/web/cart/backorder-warning.twig: -------------------------------------------------------------------------------- 1 |
2 | There are items in your cart that are not in stock. You can 3 | still place your order, but your order will not ship until all of the 4 | items are ready to go. We will email you to let you know the status of 5 | your order. (If you are picking up your order, you will be able to 6 | pick up items as they are available.) 7 |
8 | -------------------------------------------------------------------------------- /db/migrations/20200417043645_add_brand_fulltext_index.php: -------------------------------------------------------------------------------- 1 | table('brand', [ 'signed' => false ]); 10 | $table 11 | ->addIndex([ 'name', 'slug' ], [ 'type' => 'fulltext' ]) 12 | ->save(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /ui/web/layout/userway.twig: -------------------------------------------------------------------------------- 1 | {% if config('userway.key') %} 2 | 3 | {% endif %} 4 | -------------------------------------------------------------------------------- /bin/decrypt-code: -------------------------------------------------------------------------------- 1 | #!/usr/bin/php 2 | table('image', [ 'signed' => false ]); 10 | $table 11 | ->addIndex([ 'name', 'alt_text', 'caption' ], [ 'type' => 'fulltext' ]) 12 | ->save(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /db/migrations/20220526184322_remove_item_drophip_fee.php: -------------------------------------------------------------------------------- 1 | table('item', [ 'signed' => false ]); 11 | $table 12 | ->removeColumn('dropship_fee') 13 | ->update(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lib/Scat/Exception/HttpConflictException.php: -------------------------------------------------------------------------------- 1 | table('txn_line', [ 'signed' => false ]); 10 | $table 11 | ->renameColumn('txn', 'txn_id') 12 | ->renameColumn('item', 'item_id') 13 | ->save(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /db/migrations/20200417001852_fix_txn_foreign_keys.php: -------------------------------------------------------------------------------- 1 | table('txn', [ 'signed' => false ]); 10 | $table 11 | ->renameColumn('person', 'person_id') 12 | ->renameColumn('returned_from', 'returned_from_id') 13 | ->save(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /db/migrations/20211202195401_make_vendor_item_vendor_sku_unique_key.php: -------------------------------------------------------------------------------- 1 | table('vendor_item', [ 'signed' => false ]); 10 | $table 11 | ->addIndex(['vendor_id','vendor_sku'], [ 'unique' => true ]) 12 | ->update(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /db/migrations/20200416214009_fix_vendor_item_foreign_keys.php: -------------------------------------------------------------------------------- 1 | table('vendor_item', [ 'signed' => false ]); 10 | $table 11 | ->renameColumn('vendor', 'vendor_id') 12 | ->renameColumn('item', 'item_id') 13 | ->save(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /db/migrations/20200517042944_fix_shipment_id_fields.php: -------------------------------------------------------------------------------- 1 | table('shipment'); 10 | $table 11 | ->changeColumn('tracker_id', 'string', [ 12 | 'limit' => 50, 13 | 'null' => true, 14 | ]) 15 | ->save(); 16 | 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/Scat/Model/Wishlist.php: -------------------------------------------------------------------------------- 1 | has_many('WishlistItem'); 7 | } 8 | } 9 | 10 | class WishlistItem extends \Scat\Model { 11 | public function wishlist() { 12 | return $this->belongs_to('Wishlist')->find_one(); 13 | } 14 | 15 | public function item() { 16 | return $this->belongs_to('Item')->find_one(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /ui/web/cart/add-to-cart-dialog.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |

{{ quantity }} × {{ item.title }} added to cart.

5 | 6 |

7 | View Cart & Checkout 8 | 9 |

10 |
11 |
12 |
13 |
14 | -------------------------------------------------------------------------------- /ui/web/layout/tracking.twig: -------------------------------------------------------------------------------- 1 | {% if config('google.tag_manager') %} 2 | 7 | {% endif %} 8 | -------------------------------------------------------------------------------- /ui/web/sitemap.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ full_url_for('page', { param: 'sitemap-pages.xml' }) }} 5 | 6 | 7 | {{ full_url_for('catalog-sitemap') }} 8 | 9 | 10 | {{ full_url_for('page', { param: 'blog/sitemap.xml' }) }} 11 | 12 | 13 | -------------------------------------------------------------------------------- /db/migrations/20200527203740_add_media_b2_file_id.php: -------------------------------------------------------------------------------- 1 | table('image'); 10 | $table 11 | ->addColumn('b2_file_id', 'string', [ 12 | 'limit' => 200, 13 | 'default' => '', 14 | 'after' => 'uuid', 15 | ]) 16 | ->update(); 17 | 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lib/Scat/Model/Device.php: -------------------------------------------------------------------------------- 1 | create(); 7 | $device->token= $token; 8 | $device->save(); 9 | 10 | return $device; 11 | } 12 | 13 | public static function forget($token) { 14 | $device= self::factory('Device')->where('token', $token)->find_one(); 15 | $device->delete(); 16 | } 17 | 18 | } 19 | 20 | -------------------------------------------------------------------------------- /db/migrations/20200510002927_add_txn_online_sale_id.php: -------------------------------------------------------------------------------- 1 | table('txn'); 10 | $table 11 | ->addColumn('online_sale_id', 'integer', [ 12 | 'signed' => false, 13 | 'null' => true, 14 | 'after' => 'uuid', 15 | ]) 16 | ->save(); 17 | 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /db/migrations/20230606201015_add_internal_ad_fulltext_index.php: -------------------------------------------------------------------------------- 1 | table('internal_ad', [ 'signed' => false ]); 11 | $table->addIndex([ 'tag', 'headline', 'caption', 'button_label' ], ['type' => 'fulltext']) 12 | ->update(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /ui/web/email/get-help.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout/email.html' %} 2 | 3 | {% block title -%} 4 | Help with Cart {{ cart.id }} 5 | {%- endblock %} 6 | 7 | {% block content_top %} 8 |

9 | {{ txn.name }} <{{ txn.email }}> requested help with their cart. 10 |

11 | 12 | {% if comment %} 13 |

14 | {{ txn.name }} said: 15 |

16 |
{{ comment }}
17 | {% endif %} 18 | {% endblock %} 19 | 20 | {% block content_bottom %} 21 | {% endblock %} 22 | -------------------------------------------------------------------------------- /lib/dummy-cc-terminal.php: -------------------------------------------------------------------------------- 1 | table('shipment', [ 'signed' => false ]); 10 | $table 11 | ->changeColumn('handling_instructions', 'string', [ 12 | 'limit' => 255, 13 | 'null' => true 14 | ]) 15 | ->update(); 16 | 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /db/migrations/20210924174456_add_shipment_ship_district_method.php: -------------------------------------------------------------------------------- 1 | table('shipment', [ 'signed' => false ]); 10 | $table 11 | ->changeColumn('method', 'enum', [ 12 | 'values' => [ 13 | 'easypost', 'shipdistrict' 14 | ], 15 | ]) 16 | ->update(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /db/migrations/20210924175202_add_shipment_handling_instructions.php: -------------------------------------------------------------------------------- 1 | table('shipment', [ 'signed' => false ]); 10 | $table 11 | ->addColumn('handling_instructions', 'string', [ 12 | 'limit' => 255, 13 | 'after' => 'insurance' 14 | ]) 15 | ->update(); 16 | 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /db/migrations/20200512182114_add_timezone_to_address.php: -------------------------------------------------------------------------------- 1 | table('address'); 10 | $table 11 | ->addColumn('timezone', 'string', [ 12 | 'limit' => 128, 13 | 'null' => true, 14 | 'default' => null, 15 | 'after' => 'phone', 16 | ]) 17 | ->save(); 18 | 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /db/migrations/20200516014830_add_payment_data.php: -------------------------------------------------------------------------------- 1 | table('payment'); 11 | $table 12 | ->addColumn('data', 'blob', [ 13 | 'limit' => MysqlAdapter::BLOB_MEDIUM, 14 | 'null' => true, 15 | ]) 16 | ->save(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /db/migrations/20220121182700_add_cc_to_person.php: -------------------------------------------------------------------------------- 1 | table('person'); 10 | $table 11 | ->addColumn('cc_email', 'string', [ 12 | 'limit' => 255, 13 | 'null' => true, 14 | 'after' => 'salsify_url', 15 | ]) 16 | ->update(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /db/migrations/20200501033926_add_note_type.php: -------------------------------------------------------------------------------- 1 | table('note'); 10 | $table 11 | ->addColumn('source', 'enum', [ 12 | 'values' => [ 'internal', 'sms', 'email' ], 13 | 'default' => 'internal', 14 | 'null' => true, 15 | 'after' => 'attach_id', 16 | ]) 17 | ->save(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /db/migrations/20201005213557_add_kit_quantity.php: -------------------------------------------------------------------------------- 1 | table('kit_item', [ 'signed' => false ]); 10 | $table 11 | ->addColumn('quantity', 'integer', [ 12 | 'after' => 'item_id', 13 | 'signed' => false, 14 | 'null' => false, 15 | 'default' => 1, 16 | ]) 17 | ->save(); 18 | 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /db/migrations/20210927172226_add_note_full_content.php: -------------------------------------------------------------------------------- 1 | table('note'); 11 | $table 12 | ->addColumn('full_content', 'text', [ 13 | 'limit' => MysqlAdapter::TEXT_MEDIUM, 14 | 'null' => true, 15 | 'after' => 'content', 16 | ]) 17 | ->update(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /db/migrations/20231010222501_add_expires_at_to_internal_ad.php: -------------------------------------------------------------------------------- 1 | table('internal_ad', [ 'signed' => false ]); 11 | $table 12 | ->addColumn('expires_at', 'datetime', [ 13 | 'null' => true, 14 | 'after' => 'button_label', 15 | ]) 16 | ->update(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /phinx.yml: -------------------------------------------------------------------------------- 1 | paths: 2 | migrations: '%%PHINX_CONFIG_DIR%%/db/migrations' 3 | seeds: '%%PHINX_CONFIG_DIR%%/db/seeds' 4 | 5 | environments: 6 | default_migration_table: phinxlog 7 | default_database: db 8 | 9 | db: 10 | adapter: mysql 11 | host: '%%PHINX_HOST_NAME%%' 12 | name: '%%PHINX_DATABASE%%' 13 | user: '%%PHINX_USER%%' 14 | pass: '%%PHINX_PASSWORD%%' 15 | port: 3306 16 | charset: utf8mb4 17 | collation: utf8mb4_0900_ai_ci 18 | 19 | version_order: creation 20 | -------------------------------------------------------------------------------- /db/migrations/20200409223318_default_tax_rate.php: -------------------------------------------------------------------------------- 1 | table('txn', [ 'signed' => false ]); 10 | $table 11 | ->changeColumn('tax_rate', 'decimal', [ 12 | 'precision' => 9, 13 | 'scale' => 3, 14 | 'default' => 0.0, 15 | ]) 16 | ->save(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /ui/web/layout/newsletter.twig: -------------------------------------------------------------------------------- 1 | {% if config('mailerlite.account_id') %} 2 | 3 | 10 | 11 | {% endif %} 12 | -------------------------------------------------------------------------------- /db/migrations/20210210003751_add_tax_exemption_to_person.php: -------------------------------------------------------------------------------- 1 | table('person'); 10 | $table 11 | ->addColumn('exemption_certificate_id', 'string', [ 12 | 'null' => true, 13 | 'limit' => 50, 14 | 'after' => 'tax_id', 15 | ]) 16 | ->save(); 17 | 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /db/migrations/20210925173629_add_person_gift_card.php: -------------------------------------------------------------------------------- 1 | table('person', [ 'signed' => false ]); 11 | $table 12 | ->addColumn('giftcard_id', 'integer', [ 13 | 'signed' => false, 14 | 'null' => true, 15 | 'after' => 'vendor_rebate', 16 | ]) 17 | ->update(); 18 | 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /db/migrations/20191120204658_add_brand_description.php: -------------------------------------------------------------------------------- 1 | table('brand', [ 'signed' => false ]); 11 | $table 12 | ->addColumn('description', 'text', [ 13 | 'limit' => MysqlAdapter::TEXT_MEDIUM, 14 | 'null' => true, 15 | ]) 16 | ->update(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /db/migrations/20200502225317_add_image_publitio_id.php: -------------------------------------------------------------------------------- 1 | table('image'); 10 | $table 11 | ->addColumn('publitio_id', 'string', [ 12 | 'limit' => 12, 13 | 'default' => '', 14 | 'null' => true, 15 | 'after' => 'uuid', 16 | ]) 17 | ->save(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /db/migrations/20210306183903_add_no_backorder_to_item.php: -------------------------------------------------------------------------------- 1 | table('item', [ 'signed' => false ]); 11 | $table 12 | ->addColumn('no_backorder', 'integer', [ 13 | 'limit' => MysqlAdapter::INT_TINY, 14 | 'null' => true 15 | ]) 16 | ->update(); 17 | 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /ui/pos/ad.twig: -------------------------------------------------------------------------------- 1 |
2 | {% if ad.tag %} 3 |
4 | {{ ad.tag }} 5 |
6 | {% endif %} 7 |

8 | {{ ad.headline }} 9 |

10 | 11 | {{ include('carousel.twig', { images: [ ad.image ]}) }} 12 | 13 | {% if ad.caption %} 14 |

{{ ad.caption }}

15 | {% endif %} 16 | 17 | {{ ad.button_label }} 18 | 19 |
20 | -------------------------------------------------------------------------------- /db/migrations/20220108014155_add_salsify_to_person.php: -------------------------------------------------------------------------------- 1 | table('person'); 11 | $table 12 | ->addColumn('salsify_url', 'string', [ 13 | 'limit' => 255, 14 | 'null' => true, 15 | 'after' => 'vendor_rebate', 16 | ]) 17 | ->update(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /ui/pos/push/service-worker.js: -------------------------------------------------------------------------------- 1 | /* Based on https://github.com/Minishlink/web-push-php-example/ */ 2 | self.addEventListener('push', function (event) { 3 | if (!(self.Notification && self.Notification.permission === 'granted')) { 4 | return; 5 | } 6 | 7 | const sendNotification= body => { 8 | const title = "Scat POS"; 9 | 10 | return self.registration.showNotification(title, { 11 | body, 12 | }); 13 | }; 14 | 15 | if (event.data) { 16 | const message= event.data.text(); 17 | event.waitUntil(sendNotification(message)); 18 | } 19 | }); 20 | -------------------------------------------------------------------------------- /db/migrations/20200830005627_add_shipping_method.php: -------------------------------------------------------------------------------- 1 | table('shipment', [ 'signed' => false ]); 10 | $table 11 | ->addColumn('method', 'enum', [ 12 | 'values' => [ 13 | 'easypost', 'shippo' 14 | ], 15 | 'after' => 'txn_id', 16 | ]) 17 | ->renameColumn('easypost_id', 'method_id') 18 | ->update(); 19 | 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /db/migrations/20210207234729_add_txn_line_returned_from_id.php: -------------------------------------------------------------------------------- 1 | table('txn_line', [ 'signed' => false ]); 10 | $table 11 | ->addColumn('returned_from_id', 'integer', [ 12 | 'signed' => false, 13 | 'null' => true, 14 | 'after' => 'txn_id', 15 | ]) 16 | ->save(); 17 | 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lib/Scat/Model/WebPushSubscription.php: -------------------------------------------------------------------------------- 1 | create(); 7 | $sub->endpoint= $endpoint; 8 | $sub->data= json_encode($keys); 9 | $sub->save(); 10 | 11 | return $sub; 12 | } 13 | 14 | public static function forget($endpoint) { 15 | $sub= self::factory('WebPushSubscription')->where('endpoint', $endpoint)->find_one(); 16 | if ($sub) $sub->delete(); 17 | } 18 | } 19 | 20 | 21 | -------------------------------------------------------------------------------- /db/migrations/20200110001431_add_address_to_txn.php: -------------------------------------------------------------------------------- 1 | table('txn', [ 'signed' => false ]); 11 | $table 12 | ->addColumn('shipping_address_id', 'integer', [ 13 | 'signed' => 'false', 14 | 'null' => 'true', 15 | 'after' => 'person', 16 | ]) 17 | ->save(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lib/Scat/Middleware/NoCache.php: -------------------------------------------------------------------------------- 1 | handle($request); 16 | return $response->withHeader('Cache-control', 'private'); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/Scat/Middleware/NoIndex.php: -------------------------------------------------------------------------------- 1 | handle($request); 16 | return $response->withHeader('X-Robots-Tag', 'noindex'); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /db/migrations/20200208201034_add_qb_ids.php: -------------------------------------------------------------------------------- 1 | table('payment', [ 'signed' => false ]); 11 | $table 12 | ->addColumn('qb_je_id', 'string', [ 'limit' => 512 ]) 13 | ->save(); 14 | 15 | $table= $this->table('txn', [ 'signed' => false ]); 16 | $table 17 | ->addColumn('qb_je_id', 'string', [ 'limit' => 512 ]) 18 | ->save(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /deploy.php: -------------------------------------------------------------------------------- 1 | table('payment', [ 'signed' => false ]); 11 | $table 12 | ->addColumn('captured', 'datetime', [ 'null' => true ]) 13 | ->update(); 14 | 15 | // Make captured be the same as processed for existing payments 16 | $this->execute("UPDATE payment SET captured = processed"); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /db/migrations/20210307221349_add_warning_to_brand.php: -------------------------------------------------------------------------------- 1 | table('brand', [ 'signed' => false ]); 11 | $table 12 | ->addColumn('warning', 'text', [ 13 | 'limit' => MysqlAdapter::TEXT_MEDIUM, 14 | 'null' => true, 15 | 'after' => 'description', 16 | ]) 17 | ->update(); 18 | 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /db/migrations/20200125194502_add_config_table.php: -------------------------------------------------------------------------------- 1 | table('config', [ 'signed' => false ]); 11 | $table 12 | ->addColumn('name', 'string', [ 'limit' => 255 ]) 13 | ->addColumn('value', 'text', [ 14 | 'limit' => MysqlAdapter::TEXT_MEDIUM, 15 | ]) 16 | ->addIndex(['name'], [ 'unique' => true ]) 17 | ->create(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /db/migrations/20200429051210_config_type_password.php: -------------------------------------------------------------------------------- 1 | table('config'); 10 | $table 11 | ->changeColumn('type', 'enum', [ 12 | 'values' => [ 'string', 'password', 'text', 'blob' ], 13 | 'default' => 'string' 14 | ]) 15 | ->save(); 16 | } 17 | 18 | public function down() { 19 | // We don't actually undo this, no harm in leaving it 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /db/migrations/20221118191824_add_item_no_online_sale.php: -------------------------------------------------------------------------------- 1 | table('item', [ 'signed' => false ]); 12 | $table 13 | ->addColumn( 14 | 'no_online_sale', 'integer', [ 15 | 'limit' => MysqlAdapter::INT_TINY, 16 | 'null' => true, 17 | 'after' => 'no_backorder', 18 | ]) 19 | ->update(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /ui/web/layout/google-rating.twig: -------------------------------------------------------------------------------- 1 | {% if config('google.merchant_center_id') %} 2 | 3 | 4 | 15 | {% endif %} 16 | -------------------------------------------------------------------------------- /db/migrations/20191104010342_add_price_override_in_stock.php: -------------------------------------------------------------------------------- 1 | table('price_override', [ 'signed' => false ]); 11 | $table 12 | ->addColumn('in_stock', 'integer', [ 13 | 'limit' => MysqlAdapter::INT_TINY, 14 | 'default' => 0, 15 | 'after' => 'minimum_quantity', 16 | ]) 17 | ->update(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /db/migrations/20200209003421_fix_qb_je_id.php: -------------------------------------------------------------------------------- 1 | table('payment', [ 'signed' => false ]); 11 | $table 12 | ->changeColumn('qb_je_id', 'string', [ 'limit' => 512, 'default' => '' ]) 13 | ->save(); 14 | 15 | $table= $this->table('txn', [ 'signed' => false ]); 16 | $table 17 | ->changeColumn('qb_je_id', 'string', [ 'limit' => 512, 'default' => '' ]) 18 | ->save(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ui/web/catalog/whatsnew.html: -------------------------------------------------------------------------------- 1 | {% extends 'catalog/page.html' %} 2 | 3 | {% block title %} 4 | New Products 5 | {% endblock %} 6 | 7 | {% block catalog_crumb %} 8 | 16 | {% endblock %} 17 | 18 | {% block catalog_content %} 19 | 20 |

What's New

21 | 22 | {% if products %} 23 | {% include 'catalog/products.twig' %} 24 | {% endif %} 25 | 26 | {% endblock %} 27 | -------------------------------------------------------------------------------- /db/migrations/20200406234429_fix_timeclock_person.php: -------------------------------------------------------------------------------- 1 | table('timeclock', [ 'signed' => false ]); 11 | $table 12 | ->renameColumn('person', 'person_id') 13 | ->save(); 14 | } 15 | 16 | public function down() 17 | { 18 | $table= $this->table('timeclock', [ 'signed' => false ]); 19 | $table 20 | ->renameColumn('person_id', 'person') 21 | ->save(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /db/migrations/20211109210154_add_item_dropship_fee.php: -------------------------------------------------------------------------------- 1 | table('item', [ 'signed' => false ]); 11 | $table 12 | ->addColumn('dropship_fee', 'decimal', [ 13 | 'precision' => 9, 14 | 'scale' => 2, 15 | 'null' => true, 16 | 'after' => 'no_backorder' 17 | ]) 18 | ->update(); 19 | 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /db/migrations/20210207215627_add_tax_captured_to_txn.php: -------------------------------------------------------------------------------- 1 | table('txn'); 10 | $table 11 | ->addColumn('tax_captured', 'datetime', [ 12 | 'null' => true, 13 | 'after' => 'paid', 14 | ]) 15 | ->save(); 16 | 17 | $this->execute("UPDATE txn SET tax_captured = paid 18 | WHERE type = 'customer' 19 | AND (paid < '2021-01-01' OR online_sale_id)"); 20 | 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /db/migrations/20220816201340_add_item_description.php: -------------------------------------------------------------------------------- 1 | table('item', [ 'signed' => false ]); 12 | $table 13 | ->addColumn('description', 'text', [ 14 | 'limit' => MysqlAdapter::TEXT_MEDIUM, 15 | 'null' => true, 16 | 'after' => 'name', 17 | ]) 18 | ->update(); 19 | 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /extern/mousetrap-1.6.2/plugins/global-bind/README.md: -------------------------------------------------------------------------------- 1 | # Global Bind 2 | 3 | This extension allows you to specify keyboard events that will work anywhere including inside textarea/input fields. 4 | 5 | Usage looks like: 6 | 7 | ```javascript 8 | Mousetrap.bindGlobal('ctrl+s', function() { 9 | _save(); 10 | }); 11 | ``` 12 | 13 | This means that a keyboard event bound using ``Mousetrap.bind`` will only work outside of form input fields, but using ``Moustrap.bindGlobal`` will work in both places. 14 | 15 | If you wanted to create keyboard shortcuts that only work when you are inside a specific textarea you can do that too by creating your own extension. 16 | -------------------------------------------------------------------------------- /static/limited-quantity.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Layer 1 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /db/migrations/20221005183725_add_packaged_for_shipping_to_item.php: -------------------------------------------------------------------------------- 1 | table('item', [ 'signed' => false ]); 12 | $table 13 | ->addColumn('packaged_for_shipping', 'integer', [ 14 | 'limit' => MysqlAdapter::INT_TINY, 15 | 'null' => true, 16 | 'after' => 'weight' 17 | ]) 18 | ->update(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /db/migrations/20230916233339_add_image_use_as_swatch.php: -------------------------------------------------------------------------------- 1 | table('image'); 12 | $table 13 | ->addColumn('use_as_swatch', 'integer', [ 14 | 'limit' => MysqlAdapter::INT_TINY, 15 | 'default' => 0, 16 | 'null' => false, 17 | 'after' => 'ext', 18 | ]) 19 | ->update(); 20 | 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /db/migrations/20211020024653_add_product_importance.php: -------------------------------------------------------------------------------- 1 | table('product', [ 'signed' => false ]); 11 | $table 12 | ->addColumn('importance', 'integer', [ 13 | 'signed' => 'false', 14 | 'limit' => MysqlAdapter::INT_TINY, 15 | 'default' => 0, 16 | 'after' => 'slug', 17 | ]) 18 | ->removeColumn('variation_style') 19 | ->update(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /db/migrations/20220611214355_add_featured_to_department.php: -------------------------------------------------------------------------------- 1 | table('department', [ 'signed' => false ]); 12 | $table 13 | ->addColumn('featured', 'integer', [ 14 | 'signed' => 'false', 15 | 'limit' => MysqlAdapter::INT_TINY, 16 | 'default' => 0, 17 | 'after' => 'slug', 18 | ]) 19 | ->update(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /ui/pos/catalog/whatsnew.html: -------------------------------------------------------------------------------- 1 | {% extends 'catalog/page.html' %} 2 | 3 | {% block title %} 4 | New Products 5 | {% endblock %} 6 | 7 | {% block catalog_crumb %} 8 |
9 |
10 | 16 |
17 |
18 | {% endblock %} 19 | 20 | {% block catalog_content %} 21 | 22 |

What's New

23 | 24 | {% if products %} 25 | {% include 'catalog/products.twig' %} 26 | {% endif %} 27 | 28 | {% endblock %} 29 | 30 | -------------------------------------------------------------------------------- /db/migrations/20200501232539_add_image_caption_and_data.php: -------------------------------------------------------------------------------- 1 | table('image'); 11 | $table 12 | ->addColumn('caption', 'text', [ 13 | 'limit' => MysqlAdapter::TEXT_MEDIUM, 14 | 'null' => true, 15 | ]) 16 | ->addColumn('data', 'blob', [ 17 | 'limit' => MysqlAdapter::BLOB_MEDIUM, 18 | 'null' => true, 19 | ]) 20 | ->save(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /ui/pos/settings/nav.twig: -------------------------------------------------------------------------------- 1 | {% set pages = [ 2 | { path: '/settings', label: 'Basic' }, 3 | { path: '/settings/message', label: 'Canned Messages' }, 4 | { path: '/settings/printing', label: 'Printing' }, 5 | { path: '/settings/shipping', label: 'Shipping' }, 6 | { path: '/settings/wordform', label: 'Wordforms' }, 7 | { path: '/settings/advanced', label: 'Advanced' }, 8 | ] %} 9 | 14 | 21 | 22 | -------------------------------------------------------------------------------- /db/migrations/20200517011542_add_shipment_status_pending.php: -------------------------------------------------------------------------------- 1 | table('shipment', [ 'signed' => false ]); 10 | $table 11 | ->changeColumn('status', 'enum', [ 12 | 'values' => [ 13 | 'pending', 14 | 'unknown', 'pre_transit', 'in_transit', 'out_for_delivery', 15 | 'delivered', 'available_for_pickup', 'return_to_sender', 16 | 'failure', 'cancelled', 'error' 17 | ], 18 | 'default' => 'unknown' 19 | ]) 20 | ->save(); 21 | 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /db/migrations/20210624190738_add_shipping_cancelling_state.php: -------------------------------------------------------------------------------- 1 | table('shipment', [ 'signed' => false ]); 10 | $table 11 | ->changeColumn('status', 'enum', [ 12 | 'values' => [ 13 | 'pending', 14 | 'unknown', 'pre_transit', 'in_transit', 'out_for_delivery', 15 | 'delivered', 'available_for_pickup', 'return_to_sender', 16 | 'failure', 'cancelling', 'cancelled', 'error' 17 | ], 18 | 'default' => 'unknown' 19 | ]) 20 | ->save(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /db/migrations/20200523233953_create_item_to_image.php: -------------------------------------------------------------------------------- 1 | table('item_to_image', [ 10 | 'id' => false, 11 | 'primary_key' => ['item_id', 'image_id' ] 12 | ]); 13 | $table 14 | ->addColumn('item_id', 'integer', [ 'signed' => false ]) 15 | ->addColumn('image_id', 'integer', [ 'signed' => false ]) 16 | ->addColumn('priority', 'integer', [ 17 | 'signed' => false, 18 | 'default' => '0' 19 | ]) 20 | ->create(); 21 | 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /db/migrations/20190602205056_add_image_times.php: -------------------------------------------------------------------------------- 1 | table('image', [ 'signed' => false ]); 10 | $table 11 | /* Don't use ->addTimestamps() because we use DATETIME */ 12 | ->addColumn('created_at', 'datetime', [ 13 | 'default' => 'CURRENT_TIMESTAMP', 14 | ]) 15 | ->addColumn('updated_at', 'datetime', [ 16 | 'update' => 'CURRENT_TIMESTAMP', 17 | 'default' => 'CURRENT_TIMESTAMP', 18 | ]) 19 | ->addIndex(['created_at']) 20 | ->save(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /static/js/dummy-zaraz.js: -------------------------------------------------------------------------------- 1 | /* Just a simple Zaraz dummy that spits stuff to console */ 2 | "use strict"; 3 | 4 | class Zaraz { 5 | set (key, value, options= null) { 6 | console.log("Zaraz Set: %s = %s %O", key, value, options) 7 | } 8 | 9 | track (event, parameters= null) { 10 | console.log("Zaraz Track: %s %O", event, parameters) 11 | } 12 | 13 | ecommerce (event, parameters) { 14 | console.log("Zaraz Ecommerce: %s %O", event, parameters) 15 | } 16 | } 17 | 18 | /* Dummy Zaraz */ 19 | let zaraz= new Zaraz() 20 | window.zaraz= new Zaraz() 21 | 22 | /* Dummy Microsoft */ 23 | window.uetq= []; 24 | 25 | /* Dummy Pinterest */ 26 | window.pintrk= (action, event, parameters) => { 27 | console.log("Pinterest %s: %s %O", action, event, parameters) 28 | } 29 | -------------------------------------------------------------------------------- /db/migrations/20200505180239_add_venmo_pay_method.php: -------------------------------------------------------------------------------- 1 | table('payment'); 10 | $table 11 | ->changeColumn('method', 'enum', [ 12 | 'values' => [ 13 | 'cash', 'change', 'credit', 'square', 'stripe', 14 | 'gift', 'check', 'dwolla', 'paypal', 'amazon', 15 | 'eventbrite', 'venmo', 16 | 'discount', 'withdrawal', 'bad', 'donation', 17 | 'internal' 18 | ], 19 | ]) 20 | ->save(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /ui/pos/dialog/file-upload.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout/dialog.html' %} 2 | {% import 'macros.twig' as scat %} 3 | 4 | {% block size 'modal-sm' %} 5 | {% block modal_options 'data-backdrop="static" data-keyboard="false"' %} 6 | {% block closebutton "" %} 7 | 8 | {% block title %} 9 | File Upload 10 | {% endblock %} 11 | 12 | {% block body %} 13 | 23 | {% endblock %} 24 | 25 | {% block footer %} 26 | {% endblock %} 27 | -------------------------------------------------------------------------------- /db/migrations/20200415005639_add_barcode_id.php: -------------------------------------------------------------------------------- 1 | execute($q); 15 | } 16 | 17 | public function down() 18 | { 19 | $q= "ALTER TABLE barcode DROP PRIMARY KEY, DROP COLUMN `id`, 20 | DROP KEY `item`, 21 | ADD PRIMARY KEY (item, code)"; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/Scat/Controller/Scale.php: -------------------------------------------------------------------------------- 1 | config= $config; 15 | } 16 | 17 | function home(Request $request, Response $response) { 18 | $client= new \GuzzleHttp\Client(); 19 | 20 | $path= $this->config->get('scale.url'); 21 | 22 | $res= $client->get($path); 23 | $body= (string)$res->getBody(); 24 | 25 | $response->getBody()->write($body); 26 | 27 | return $response->withHeader('Content-type', 'text/plain'); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/Scat/Service/PoleDisplay.php: -------------------------------------------------------------------------------- 1 | host= $config->get('poledisplay.host'); 12 | $this->port= $config->get('poledisplay.port') ?: 1888; 13 | } 14 | 15 | function displayPrice($label, $price) { 16 | if ($GLOBALS['DEBUG']) { 17 | error_log(sprintf("POLE: %-19.19s - $%18.2f", $label, $price)); 18 | } 19 | 20 | if (!$this->host) { 21 | error_log("No pole display configured"); 22 | return; 23 | } 24 | 25 | $sock= @fsockopen($this->host, $this->port, $errno, $errstr, 1); 26 | if ($sock) { 27 | fwrite($sock, sprintf("\x0a\x0d%-19.19s\x0a\x0d$%18.2f ", $label, $price)); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /db/migrations/20210115220541_add_postmates_pay_method.php: -------------------------------------------------------------------------------- 1 | table('payment', [ 'signed' => false ]); 10 | $table 11 | ->changeColumn('method', 'enum', [ 12 | 'values' => [ 13 | 'cash', 'change', 'credit', 'square', 'stripe', 14 | 'gift', 'check', 'dwolla', 'paypal', 'amazon', 15 | 'eventbrite', 'venmo', 'loyalty', 'postmates', 16 | 'discount', 'withdrawal', 'bad', 'donation', 17 | 'internal' 18 | ], 19 | ]) 20 | ->save(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /db/migrations/20200416204639_barcode_item_id_and_dates.php: -------------------------------------------------------------------------------- 1 | table('barcode', [ 'signed' => false ]); 10 | $table 11 | ->renameColumn('item', 'item_id') 12 | /* Don't use ->addTimestamps() because we use DATETIME */ 13 | ->addColumn('created_at', 'datetime', [ 14 | 'default' => 'CURRENT_TIMESTAMP', 15 | ]) 16 | ->addColumn('updated_at', 'datetime', [ 17 | 'update' => 'CURRENT_TIMESTAMP', 18 | 'default' => 'CURRENT_TIMESTAMP', 19 | ]) 20 | ->addIndex(['created_at']) 21 | ->save(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ui/pos/email/gift-card.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout/email.html' %} 2 | 3 | {% block title -%} 4 | Gift Card for {{ to_name }} 5 | {%- endblock %} 6 | 7 | {% block preheader %} 8 | You have a gift card for Raw Materials Art Supplies! 9 | {% endblock %} 10 | 11 | {% block content_top %} 12 |

Hi,

13 | 14 |

15 | You've been sent a gift card for Raw Materials Art Supplies 16 | {{- from_name ? (' from ' ~ from_name) }}. The PDF is attached, just print 17 | it out and bring it to the store (or show us the card on your phone). 18 |

19 | 20 | {% if message %} 21 |

We were asked to pass along this message with the card:

22 | 23 |

24 |

25 | {{ message|nl2br }} 26 |
27 |

28 | {% endif %} 29 | 30 |

Thanks!

31 | {% endblock %} 32 | -------------------------------------------------------------------------------- /ui/pos/dialog/item-shipping-estimates.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout/dialog.html' %} 2 | {% import 'macros.twig' as scat %} 3 | 4 | {% block title %} 5 | Shipping Estimates for {{ item.name }} 6 | {% endblock %} 7 | 8 | {% block body %} 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | {% for estimate in data %} 19 | 20 | 21 | 22 | 23 | 24 | {% endfor %} 25 | 26 |
DestinationRateService
{{ estimate.address }}{{ scat.amount(estimate.rate[0]) }}{{ estimate.rate[1] }}
27 | {% endblock %} 28 | 29 | {% block submit %} 30 | {% endblock %} 31 | 32 | {% block script %} 33 | {% endblock %} 34 | -------------------------------------------------------------------------------- /db/migrations/20200720190934_add_more_txn_status.php: -------------------------------------------------------------------------------- 1 | table('txn'); 10 | $table 11 | ->changeColumn('status', 'enum', [ 12 | 'values' => [ 13 | 'new', 14 | 'filled', 15 | 'paid', 16 | 'processing', 17 | 'waitingforitems', 18 | 'readyforpickup', 19 | 'shipping', 20 | 'shipped', 21 | 'complete', 22 | 'template', 23 | ], 24 | 'default' => 'new', 25 | 'null' => false, 26 | 'after' => 'number', 27 | ]) 28 | ->addIndex([ 'status' ]) 29 | ->save(); 30 | 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /db/migrations/20220803231224_add_google_product_categories.php: -------------------------------------------------------------------------------- 1 | table('google_product_category', [ 'signed' => false ]); 11 | $table 12 | ->addColumn('name', 'string', [ 'limit' => 255 ]) 13 | ->addIndex(['name'], [ 'unique' => true ]) 14 | ->create(); 15 | 16 | $table= $this->table('item', [ 'signed' => false ]); 17 | $table 18 | ->addColumn('google_product_category_id', 'integer', [ 19 | 'signed' => false, 20 | 'default' => 0, 21 | 'after' => 'tic', 22 | ]) 23 | ->update(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /ui/pos/report/empty-products.html: -------------------------------------------------------------------------------- 1 | {% extends 'catalog/page.html' %} 2 | 3 | {% block title %} 4 | Empty Products 5 | {% endblock %} 6 | 7 | {% block catalog_crumb %} 8 |

9 | Set All Inactive 10 |

11 | {% endblock %} 12 | 13 | {% block script %} 14 | 30 | {% endblock %} 31 | -------------------------------------------------------------------------------- /lib/Scat/Model/Note.php: -------------------------------------------------------------------------------- 1 | kind == 'txn') { 7 | return $this->belongs_to('Txn', 'attach_id')->find_one(); 8 | } 9 | } 10 | 11 | public function about() { 12 | if ($this->kind == 'txn') { 13 | return $this->belongs_to('Txn', 'attach_id')->find_one()->person(); 14 | } 15 | if ($this->kind == 'person') { 16 | return $this->belongs_to('Person', 'attach_id')->find_one(); 17 | } 18 | } 19 | 20 | public function item() { 21 | if ($this->kind == 'item') { 22 | return $this->belongs_to('Item', 'attach_id')->find_one(); 23 | } 24 | } 25 | 26 | public function person() { 27 | return $this->belongs_to('Person'); 28 | } 29 | 30 | public function parent() { 31 | return $this->belongs_to('Note', 'parent_id')->find_one(); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /db/migrations/20200206191248_add_device_table.php: -------------------------------------------------------------------------------- 1 | table('device', [ 'signed' => false ]); 11 | $table 12 | ->addColumn('person_id', 'integer', [ 13 | 'signed' => 'false', 14 | 'null' => 'true' 15 | ]) 16 | ->addColumn('token', 'string', [ 'limit' => 255, 'null' => true ]) 17 | ->addColumn('created', 'datetime', [ 18 | 'default' => 'CURRENT_TIMESTAMP' 19 | ]) 20 | ->addColumn('modified', 'datetime', [ 21 | 'default' => 'CURRENT_TIMESTAMP', 22 | 'update' => 'CURRENT_TIMESTAMP' 23 | ]) 24 | ->create(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /db/migrations/20191114022946_add_eventbrite_pay_method.php: -------------------------------------------------------------------------------- 1 | table('payment', [ 'signed' => false ]); 11 | $table 12 | ->changeColumn('method', 'enum', [ 13 | 'values' => [ 14 | 'cash', 'change', 'credit', 'square', 'stripe', 15 | 'gift', 'check', 'dwolla', 'paypal', 'amazon', 16 | 'eventbrite', 17 | 'discount', 'withdrawal', 'bad', 'donation', 18 | 'internal' 19 | ], 20 | ]) 21 | ->save(); 22 | } 23 | 24 | public function down() { 25 | // We don't actually undo this, no harm in leaving it 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /ui/web/backroom/ads.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout/page.html' %} 2 | {% import 'macros.twig' as scat %} 3 | 4 | {% block title -%} 5 | Internal Ads @ Raw Materials Art Supplies 6 | {%- endblock %} 7 | 8 | {% block content %} 9 |

Internal Ads

10 | 11 |

Hero Ads

12 | 13 |
14 | {% for ad in heros %} 15 |
16 | {{ include('ad.twig') }} 17 |
18 | {% endfor %} 19 |
20 | 21 |

Basic Ads

22 | 23 |
24 | {% for ad in basics %} 25 |
26 | {{ include('ad.twig') }} 27 |
28 | {% endfor %} 29 |
30 | {% endblock %} 31 | -------------------------------------------------------------------------------- /db/migrations/20200927005057_add_rewards_payment_type.php: -------------------------------------------------------------------------------- 1 | table('payment', [ 'signed' => false ]); 11 | $table 12 | ->changeColumn('method', 'enum', [ 13 | 'values' => [ 14 | 'cash', 'change', 'credit', 'square', 'stripe', 15 | 'gift', 'check', 'dwolla', 'paypal', 'amazon', 16 | 'eventbrite', 'venmo', 'loyalty', 17 | 'discount', 'withdrawal', 'bad', 'donation', 18 | 'internal' 19 | ], 20 | ]) 21 | ->save(); 22 | } 23 | 24 | public function down() { 25 | // We don't actually undo this, no harm in leaving it 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /db/migrations/20200428205907_add_config_type.php: -------------------------------------------------------------------------------- 1 | table('config'); 11 | $table 12 | ->addColumn('type', 'enum', [ 13 | 'values' => [ 'string', 'text', 'blob' ], 14 | 'default' => 'string' 15 | ]) 16 | /* Don't use ->addTimestamps() because we use DATETIME */ 17 | ->addColumn('created_at', 'datetime', [ 18 | 'default' => 'CURRENT_TIMESTAMP', 19 | ]) 20 | ->addColumn('updated_at', 'datetime', [ 21 | 'update' => 'CURRENT_TIMESTAMP', 22 | 'default' => 'CURRENT_TIMESTAMP', 23 | ]) 24 | ->addIndex(['created_at']) 25 | ->save(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /config/search/sphinx.conf: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | cat < 10 | {{ block('title') }} 11 | 12 | 13 |
14 | We will email updates about your order, you may want to add info@rawmaterialsla.com 16 | to your address book to prevent them being miscategorized as spam. 17 | The emails may also end up in the "Promotions" tab for Gmail users. 18 |
19 | 20 |

21 | We will let you know when your order 22 | {% if sale.shipping_address_id == 1 %} 23 | is ready for pickup, 24 | {% else %} 25 | has shipped, 26 | {% endif %} 27 | or contact you if there are any issues. 28 |

29 | 30 | {% embed 'cart/cart.twig' with { cart: sale } %} 31 | {% endembed %} 32 | {% endblock %} 33 | -------------------------------------------------------------------------------- /ui/web/email/contact.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout/email.html' %} 2 | 3 | {% block title -%} 4 | {{ subject }} 5 | {%- endblock %} 6 | 7 | {% block content_top %} 8 |

{{ message }}

9 | 10 | {% if request.donation %} 11 |
12 |
Organization
13 |
{{ request.organization }}
14 |
Location
15 |
{{ request.location }}
16 |
Website
17 |
{{ request.website }}
18 |
Type of support
19 |
{{ request.type_of_support }}
20 |
Needed by
21 |
{{ request.needed_by }}
22 |
Who benefits
23 |
{{ request.benefit }}
24 |
Acknowledgement
25 |
{{ request.how_ack }}
26 |
27 | {% endif %} 28 | 29 |
30 | {% endblock %} 31 | 32 | {% block content_bottom %} 33 |
34 |
Name
35 |
{{ name }}
36 |
Email
37 |
{{ email }}
38 |
39 | {% endblock %} 40 | -------------------------------------------------------------------------------- /lib/Scat/Service/Fraud.php: -------------------------------------------------------------------------------- 1 | get('fraud.key'); 13 | 14 | // This pulls in FraudChecker class which is encrypted for reasons. 15 | $enc= file_get_contents('../lib/fraud-checker.phpc'); 16 | $dec= Cryptor::Decrypt($enc, $key); 17 | eval('?>' . $dec); 18 | } 19 | 20 | public function checkForFraud(\Scat\Model\Cart $cart, \Scat\Service\Stripe $stripe) : void { 21 | $checker= new \FraudChecker(); 22 | 23 | $action= $checker->checkForFraud($cart, $stripe); 24 | 25 | if ($action && $action != 'already detected') { 26 | $this->email->send( 27 | $this->email->default_from_address(), 28 | "Fraud detected by {$cart->email}", 29 | "Fraud detected for cart {$cart->uuid}\n\nAction: $action" 30 | ); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /db/migrations/20210923174040_add_address_lat_long_verified.php: -------------------------------------------------------------------------------- 1 | table('address', [ 'signed' => false ]); 11 | $table 12 | ->addColumn('latitude', 'decimal', [ 13 | 'precision' => 9, 14 | 'scale' => 5, 15 | 'default' => '0.0', 16 | 'null' => true, 17 | 'after' => 'residential', 18 | ]) 19 | ->addColumn('longitude', 'decimal', [ 20 | 'precision' => 9, 21 | 'scale' => 5, 22 | 'default' => '0.0', 23 | 'null' => true, 24 | 'after' => 'latitude', 25 | ]) 26 | ->addColumn('verified', 'integer', [ 27 | 'limit' => MysqlAdapter::INT_TINY, 28 | 'default' => 0, 29 | 'after' => 'longitude', 30 | ]) 31 | ->update(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /db/migrations/20230925230029_add_web_push_notications.php: -------------------------------------------------------------------------------- 1 | table('web_push_subscription', [ 'signed' => false ]); 12 | $table 13 | ->addColumn('endpoint', 'string', [ 'limit' => 255, 'null' => true ]) 14 | ->addColumn('data', 'blob', [ 15 | 'limit' => MysqlAdapter::BLOB_MEDIUM, 16 | 'null' => true, 17 | ]) 18 | ->addColumn('created_at', 'datetime', [ 19 | 'default' => 'CURRENT_TIMESTAMP' 20 | ]) 21 | ->addColumn('updated_at', 'datetime', [ 22 | 'default' => 'CURRENT_TIMESTAMP', 23 | 'update' => 'CURRENT_TIMESTAMP' 24 | ]) 25 | ->create(); 26 | 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /ui/web/404.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout/page.html' %} 2 | {% import 'macros.twig' as scat %} 3 | 4 | {% block title -%} 5 | Page Not Found 6 | {%- endblock %} 7 | 8 | {% block content %} 9 | 10 | {% if DEBUG %} 11 | 23 | 29 | {% endif %} 30 | 31 |

Oops!

32 | 33 |

Sorry, but something went awry here at this website for Raw Materials Art Supplies. It appears that that page you are looking for no longer exists.

34 | 35 | 36 | Start from the top » 37 | 38 | 39 | {% endblock %} 40 | -------------------------------------------------------------------------------- /ui/pos/person/searchform.twig: -------------------------------------------------------------------------------- 1 |
2 | 6 |
7 |
8 |
9 | 13 | 14 | 17 | 18 |
19 |
20 |
21 | 30 | -------------------------------------------------------------------------------- /db/migrations/20220621190107_add_image_id_indexes.php: -------------------------------------------------------------------------------- 1 | table('item_to_image'); 22 | $table 23 | ->addIndex(['image_id']) 24 | ->update(); 25 | 26 | $table= $this->table('product_to_image'); 27 | $table 28 | ->addIndex(['image_id']) 29 | ->update(); 30 | 31 | $table= $this->table('internal_ad'); 32 | $table 33 | ->addIndex(['image_id']) 34 | ->update(); 35 | 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /extern/x-editable-1.5.1/inputs-ext/wysihtml5/bootstrap-wysihtml5-0.0.2/wysiwyg-color.css: -------------------------------------------------------------------------------- 1 | .wysiwyg-color-black { 2 | color: black; 3 | } 4 | 5 | .wysiwyg-color-silver { 6 | color: silver; 7 | } 8 | 9 | .wysiwyg-color-gray { 10 | color: gray; 11 | } 12 | 13 | .wysiwyg-color-white { 14 | color: white; 15 | } 16 | 17 | .wysiwyg-color-maroon { 18 | color: maroon; 19 | } 20 | 21 | .wysiwyg-color-red { 22 | color: red; 23 | } 24 | 25 | .wysiwyg-color-purple { 26 | color: purple; 27 | } 28 | 29 | .wysiwyg-color-fuchsia { 30 | color: fuchsia; 31 | } 32 | 33 | .wysiwyg-color-green { 34 | color: green; 35 | } 36 | 37 | .wysiwyg-color-lime { 38 | color: lime; 39 | } 40 | 41 | .wysiwyg-color-olive { 42 | color: olive; 43 | } 44 | 45 | .wysiwyg-color-yellow { 46 | color: yellow; 47 | } 48 | 49 | .wysiwyg-color-navy { 50 | color: navy; 51 | } 52 | 53 | .wysiwyg-color-blue { 54 | color: blue; 55 | } 56 | 57 | .wysiwyg-color-teal { 58 | color: teal; 59 | } 60 | 61 | .wysiwyg-color-aqua { 62 | color: aqua; 63 | } 64 | 65 | .wysiwyg-color-orange { 66 | color: orange; 67 | } -------------------------------------------------------------------------------- /ui/web/paymarks/cc-visa.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /ui/pos/print/inventory.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout/print.html' %} 2 | {% import 'macros.twig' as scat %} 3 | 4 | {% block title %} 5 | {% if product %} 6 | {{ product.name }} 7 | {% else %} 8 | Inventory 9 | {% endif %} 10 | {% endblock %} 11 | 12 | {% block content %} 13 | 14 | 15 | 16 | 17 | {% if use_short_name and use_variation %} 18 | 19 | {% endif %} 20 | 21 | 22 | 23 | 24 | 25 | 26 | {% for item in items %} 27 | 28 | 29 | {% if use_short_name and use_variation %} 30 | 31 | {% endif %} 32 | 35 | 36 | 37 | 38 | {% endfor %} 39 | 40 |
CodeVariationNameStockCount
{{ item.code }}{{ item.variation }} 33 | {{ use_short_name and item.short_name ? item.short_name : item.name }} 34 | {{ item.stock }} 
41 | {% endblock %} 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010-2023 Jim Winstead Jr. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /ui/web/ad.twig: -------------------------------------------------------------------------------- 1 | {% if ad.link_type == 'product' %} 2 | {% set link= url_for('catalog', ad.product.url_params) %} 3 | {% elseif ad.link_type == 'item' %} 4 | {% set link= url_for('catalog', ad.item.url_params) %} 5 | {% else %} 6 | {% set link= ad.link_url %} 7 | {% endif %} 8 | 9 | {% set image= ad.image %} 10 | 11 | {% if ad.tag == 'hero' %} 12 |
13 |
14 |

{{ ad.headline }}

15 |

{{ ad.caption }}

16 | {{ ad.button_label }} 17 |
18 | {{ image.alt_text ?: ad.headline }} 19 |
20 | {% elseif ad.tag == 'basic' %} 21 |
  • 22 |

    {{ ad.headline }}

    23 | {{ image.alt_text ?: ad.headline }} 24 |

    {{ ad.caption }}

    25 | {{ ad.button_label }} 26 |
  • 27 | {% else %} 28 | {{ ad | json_encode }} 29 | {% endif %} 30 | -------------------------------------------------------------------------------- /static/error.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Oh no! 7 | 8 | 9 | 10 |
    11 |
    12 |
    13 |
    14 |

    15 | Sorry, something went wrong! 16 |

    17 |

    18 | This site is currently unavailable. 19 |

    20 |

    21 | We are currently working to correct the problem. 22 |

    23 |

    24 | Thank you for your patience. 25 |

    26 |
    27 |
    28 | Wonton 29 |
    30 |
    31 |
    32 |
    33 | 34 | 35 | -------------------------------------------------------------------------------- /ui/web/index.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout/page.html' %} 2 | {% import 'macros.twig' as scat %} 3 | 4 | {% block title -%} 5 | {{ content.title }} 6 | {%- endblock %} 7 | 8 | {% block extra_header %} 9 | {% if content.description %} 10 | 11 | {% endif %} 12 | {% endblock %} 13 | 14 | {% block content %} 15 | 16 | {% if DEBUG %} 17 | 29 | 35 | {% endif %} 36 | 37 | {% if content.format == 'markdown_to_html' %} 38 | {{ include(template_from_string(content.content)) | markdown_to_html }} 39 | {% else %} 40 | {{ include(template_from_string(content.content)) | raw }} 41 | {% endif %} 42 | 43 | {% endblock %} 44 | -------------------------------------------------------------------------------- /ui/pos/dialog/pay-discount.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout/dialog.html' %} 2 | {% import 'macros.twig' as scat %} 3 | 4 | {% block title %} 5 | Discount 6 | {% endblock %} 7 | 8 | {% block size 'modal-sm' %} 9 | 10 | {% block body %} 11 | 20 | {% endblock %} 21 | 22 | {% block submit %} 23 | 26 | {% endblock %} 27 | 28 | {% block script %} 29 | form.onsubmit= (event) => { 30 | event.preventDefault() 31 | 32 | let form= dialog.getElementsByTagName('form')[0] 33 | let formData= new FormData(form) 34 | 35 | return scat.post("/sale/{{ txn.id }}/payment", formData) 36 | .then((res) => res.json()) 37 | .then((data) => { 38 | dialog.resolution= data 39 | $(dialog).modal('hide') 40 | }) 41 | } 42 | {% endblock %} 43 | -------------------------------------------------------------------------------- /lib/Scat/Service/Config.php: -------------------------------------------------------------------------------- 1 | factory('Config')->find_many(); 11 | foreach ($entries as $entry) { 12 | $this->_config[$entry->name]= $entry->value; 13 | } 14 | } 15 | 16 | public function get($name) { 17 | return $this->_config[$name] ?? null; 18 | } 19 | 20 | public function set($name, $value, $type= null) { 21 | $row= $this->data->factory('Config')->where('name', $name)->find_one(); 22 | 23 | if (!$row) { 24 | $row= $this->data->factory('Config')->create(); 25 | $row->name= $name; 26 | } 27 | 28 | $row->value= $value; 29 | if ($type) { 30 | $row->type= $type; 31 | } 32 | 33 | $row->save(); 34 | 35 | $this->_config[$row->name]= $row->value; 36 | 37 | return $row; 38 | } 39 | 40 | public function forget($name) { 41 | $row= $this->data->factory('Config')->where('name', $name)->find_one(); 42 | if ($row) { 43 | $row->delete(); 44 | } 45 | unset($this->_cache[$name]); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /extern/x-editable-1.5.1/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Vitaliy Potapov 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | 3 | services: 4 | 5 | scat-pos: 6 | build: . 7 | links: 8 | - db 9 | - search 10 | env_file: 11 | - sample.env 12 | working_dir: /app/app 13 | command: [ php, "-S", "0.0.0.0:5080", "pos.php" ] 14 | ports: 15 | - 5080:5080 16 | volumes: 17 | # - .:/app 18 | - ./config/php-fpm-extra.conf:/usr/local/etc/php-fpm.d/php-fpm-extra.conf 19 | restart: always 20 | 21 | db: 22 | image: mysql:8.0.32 23 | env_file: 24 | - sample.env 25 | # old password auth so sphinx can access it 26 | command: [ mysqld, --local-infile=1, --default_authentication_plugin=mysql_native_password ] 27 | expose: 28 | - "3306" 29 | volumes: 30 | - ./db/init:/docker-entrypoint-initdb.d 31 | - data:/var/lib/mysql 32 | restart: always 33 | 34 | search: 35 | image: macbre/sphinxsearch:3.4.1 36 | links: 37 | - db 38 | expose: 39 | - "9306" 40 | env_file: 41 | - sample.env 42 | volumes: 43 | - ./config/search:/opt/sphinx/conf 44 | - searchdata:/var/data 45 | restart: always 46 | 47 | volumes: 48 | data: 49 | searchdata: 50 | -------------------------------------------------------------------------------- /ui/pos/dialog/report-quick.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout/dialog.html' %} 2 | {% import 'macros.twig' as scat %} 3 | 4 | {% block size '' %} 5 | 6 | {% block title %} 7 | Quick Report 8 | {% endblock %} 9 | 10 | {% block body %} 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | {% for day in sales %} 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | {% endfor %} 33 | 34 |
    DaySalesAverageIn PersonOnlineTotal
    {{ day.raw_date | date("l, F j") }}{{ day.transactions }}{{ scat.amount(day.total / day.transactions) }}{{ scat.amount(day.in_person) }}{{ scat.amount(day.online) }}{{ scat.amount(day.total) }}
    35 | {% endblock %} 36 | 37 | {% block submit %} 38 | {% endblock %} 39 | -------------------------------------------------------------------------------- /ui/pos/settings/shipping.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout/page.html' %} 2 | 3 | {% block title %} 4 | Shipping Address 5 | {% endblock %} 6 | 7 | {% block content %} 8 | 9 |

    Shipping Address

    10 | 11 | {% include 'settings/nav.twig' %} 12 | 13 |
    14 |
    {{ address.name }}
    15 |
    {{ address.company }}
    16 |
    {{ address.email }}
    17 | {% if address.phone %} 18 |
    {{ address.phone | phone_number_format }}
    19 | {% endif %} 20 |
    {{ address.street1 }}
    21 |
    {{ address.street2 }}
    22 |
    23 | {% if address.city %} 24 | {{ address.city }}, 25 | {% endif %} 26 | {{ address.state }} 27 | {{ address.zip }} 28 |
    29 |
    30 | 31 | 34 | 35 | {% endblock %} 36 | 37 | {% block script %} 38 | 45 | {% endblock %} 46 | -------------------------------------------------------------------------------- /ui/web/catalog/searchresults.html: -------------------------------------------------------------------------------- 1 | {% extends 'catalog/page.html' %} 2 | 3 | {% block title %} 4 | Search results for '{{ q }}' 5 | {% endblock %} 6 | 7 | {% block catalog_crumb %} 8 | 16 | {% endblock %} 17 | 18 | {% block catalog_content %} 19 |

    20 | Search results for '{{ q }}' 21 |

    22 | 23 | {% if original_q %} 24 |

    25 | No results were found for your original search terms. 26 | We changed it to {{ q }} 27 | and got these results. 28 |

    29 | {% endif %} 30 | 31 | {% if products %} 32 | {% include 'catalog/products.twig' %} 33 | {% else %} 34 | {% if error %} 35 |

    Sorry, there was an error handling your query.

    36 | {% if DEBUG %} 37 |
    {{ error }}
    38 | {% endif %} 39 | {% else %} 40 |

    Sorry, we didn't find anything for that search.

    41 | {% endif %} 42 | {% endif %} 43 | 44 | {% endblock %} 45 | -------------------------------------------------------------------------------- /extern/select2-bootstrap-theme-0.1.0-beta.10/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2012-2016 Florian Kissling and contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /ui/pos/email/abandoned-cart.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout/email.html' %} 2 | 3 | {% block title -%} 4 | It looks like you left something behind... 5 | {%- endblock %} 6 | 7 | {% block preheader %} 8 | Still looking? How can we help? 9 | {% endblock %} 10 | 11 | {% block content_top %} 12 |

    Thanks for stopping by RawMaterialsLA.com.

    13 |

    Looks like you forgot something in your shipping cart. Let's go make it yours!

    14 | {% endblock %} 15 | 16 | {% block call_to_action %} 17 | return to your cart 18 | {% endblock %} 19 | 20 | {% block content_bottom %} 21 |

    22 | Have any questions? Just reply to this email and we'll get back to you with 23 | some answers! 24 |

    25 | {% endblock %} 26 | 27 | {% block content_very_bottom %} 28 |
    29 |

    30 | Let’s be besties. 31 |

    32 | 33 | 39 | {% endblock %} 40 | -------------------------------------------------------------------------------- /ui/pos/dialog/pay-other.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout/dialog.html' %} 2 | {% import 'macros.twig' as scat %} 3 | 4 | {% block title %} 5 | Other Payment 6 | {% endblock %} 7 | 8 | {% block size 'modal-sm' %} 9 | 10 | {% block body %} 11 | 21 | {% endblock %} 22 | 23 | {% block submit %} 24 | 27 | {% endblock %} 28 | 29 | {% block script %} 30 | form.onsubmit= (event) => { 31 | event.preventDefault() 32 | 33 | let form= dialog.getElementsByTagName('form')[0] 34 | let formData= new FormData(form) 35 | 36 | return scat.post("/sale/{{ txn.id }}/payment", formData) 37 | .then((res) => res.json()) 38 | .then((data) => { 39 | dialog.resolution= data 40 | $(dialog).modal('hide') 41 | }) 42 | } 43 | {% endblock %} 44 | -------------------------------------------------------------------------------- /ui/pos/report/dogs.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout/page.html' %} 2 | {% import 'macros.twig' as scat %} 3 | 4 | {% block title %} 5 | Dogs 6 | {% endblock %} 7 | 8 | {% block content %} 9 | 10 |

    These are in-stock items that have not moved in more than a year.

    11 |
    12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | {% for item in items %} 24 | 25 | 26 | 31 | 34 | 37 | 40 | 41 | {% endfor %} 42 | 43 |
    #CodeNameLastStocked
    {{ loop.index }} 27 | 28 | {{ item.code }} 29 | 30 | 32 | {{ item.name }} 33 | 35 | {{ item.last_sale }} 36 | 38 | {{ item.stocked }} 39 |
    44 |
    45 | 46 | {% endblock %} 47 | -------------------------------------------------------------------------------- /db/migrations/20200506191152_add_shipments_table.php: -------------------------------------------------------------------------------- 1 | table('shipment', [ 'signed' => false ]); 10 | $table 11 | ->addColumn('txn_id', 'integer', [ 12 | 'signed' => false, 13 | 'null' => false, 14 | ]) 15 | ->addColumn('easypost_id', 'string', [ 16 | 'limit' => 50, 17 | 'null' => true, 18 | ]) 19 | ->addColumn('status', 'enum', [ 20 | 'values' => [ 21 | 'unknown', 'pre_transit', 'in_transit', 'out_for_delivery', 22 | 'delivered', 'available_for_pickup', 'return_to_sender', 23 | 'failure', 'cancelled', 'error' 24 | ], 25 | 'default' => 'unknown' 26 | ]) 27 | ->addColumn('tracker_id', 'string', [ 'limit' => 50 ]) 28 | ->addColumn('created', 'datetime', [ 29 | 'default' => 'CURRENT_TIMESTAMP' 30 | ]) 31 | ->addColumn('modified', 'datetime', [ 32 | 'default' => 'CURRENT_TIMESTAMP', 33 | 'update' => 'CURRENT_TIMESTAMP' 34 | ]) 35 | ->create(); 36 | 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/Scat/Model/InternalAd.php: -------------------------------------------------------------------------------- 1 | link_type) { 7 | case 'item': 8 | return '/catalog/' . $this->item()->code; 9 | 10 | case 'product': 11 | return '/catalog/' . $this->product()->full_slug(); 12 | 13 | case 'url': 14 | return $this->link_url; 15 | 16 | default: 17 | throw new \Exception("Don't know href for '{$this->link_type}'"); 18 | } 19 | } 20 | 21 | public function item() { 22 | if ($this->link_type == 'item') { 23 | return $this->belongs_to('Item', 'link_id')->find_one(); 24 | } 25 | return null; // Should this be an exception? 26 | } 27 | 28 | public function product() { 29 | if ($this->link_type == 'product') { 30 | return $this->belongs_to('Product', 'link_id')->find_one(); 31 | } 32 | return null; // Should this be an exception? 33 | } 34 | 35 | public function image() { 36 | return $this->belongs_to('Image')->find_one(); 37 | } 38 | 39 | public function departmentsUsedBy() { 40 | return $this->has_many_through('Department'); 41 | } 42 | 43 | public function productsUsedBy() { 44 | return $this->has_many_through('Product'); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lib/Scat/Model/Address.php: -------------------------------------------------------------------------------- 1 | easypost_id= $easypost_address->id; 11 | $this->name= $easypost_address->name; 12 | $this->company= $easypost_address->company; 13 | $this->street1= $easypost_address->street1; 14 | $this->street2= $easypost_address->street2; 15 | $this->city= $easypost_address->city; 16 | $this->state= $easypost_address->state; 17 | $this->zip= $easypost_address->zip; 18 | $this->country= $easypost_address->country; 19 | $this->phone= $easypost_address->phone; 20 | $this->verified= $easypost_address->verifications->delivery->success ? '1' : '0'; 21 | $this->residential= $easypost_address->residential ? '1' : '0'; 22 | if ($this->verified) { 23 | $this->timezone= 24 | $easypost_address->verifications->delivery->details->time_zone; 25 | $this->latitude= 26 | $easypost_address->verifications->delivery->details->latitude; 27 | $this->longitude= 28 | $easypost_address->verifications->delivery->details->longitude; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /ui/pos/gift-card/index.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout/page.html' %} 2 | {% import 'macros.twig' as scat %} 3 | 4 | {% block title %} 5 | Gift Cards 6 | {% endblock %} 7 | 8 | {% block content %} 9 | {% include 'gift-card/form.twig' %} 10 | 11 |
    12 |
    13 |

    Recent Cards

    14 |
    15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | {% for card in cards %} 27 | 28 | 33 | 34 | 35 | 36 | 37 | 38 | {% endfor %} 39 | 40 |
    NumberCreatedLast SeenBalanceExpires
    29 | 30 | {{- card.id -}} 31 | 32 | {{ card.created }}{{ card.last_seen }}{{ scat.amount(card.balance) }}{{ card.expires }}
    41 |
    42 | 43 | {% endblock %} 44 | 45 | {% block script %} 46 | 50 | {% endblock %} 51 | -------------------------------------------------------------------------------- /db/migrations/20191210235333_add_active_default.php: -------------------------------------------------------------------------------- 1 | table('brand', [ 'signed' => false ]); 11 | $table 12 | ->changeColumn('active', 'integer', [ 13 | 'signed' => 'false', 14 | 'limit' => MysqlAdapter::INT_TINY, 15 | 'default' => 1, 16 | ]) 17 | ->save(); 18 | 19 | $table= $this->table('department', [ 'signed' => false ]); 20 | $table 21 | ->changeColumn('active', 'integer', [ 22 | 'signed' => 'false', 23 | 'limit' => MysqlAdapter::INT_TINY, 24 | 'default' => 1, 25 | ]) 26 | ->save(); 27 | 28 | $table= $this->table('item', [ 'signed' => false ]); 29 | $table 30 | ->changeColumn('active', 'integer', [ 31 | 'signed' => 'false', 32 | 'limit' => MysqlAdapter::INT_TINY, 33 | 'default' => 1, 34 | ]) 35 | ->save(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/Scat/Distance.php: -------------------------------------------------------------------------------- 1 | table('txn'); 10 | $table 11 | ->addColumn('status', 'enum', [ 12 | 'values' => [ 13 | 'new', 14 | 'filled', 15 | 'paid', 16 | 'processing', 17 | 'shipped', 18 | 'complete', 19 | 'template', 20 | ], 21 | 'default' => 'new', 22 | 'null' => false, 23 | 'after' => 'number', 24 | ]) 25 | ->addIndex([ 'status' ]) 26 | ->save(); 27 | 28 | $q="UPDATE `txn` 29 | SET `status` = IF(`paid` IS NOT NULL AND `filled` IS NOT NULL, 30 | 'complete', 31 | IF(`paid` IS NOT NULL, 'paid', 32 | IF(`filled` IS NOT NULL, 'filled', 33 | `status`)))"; 34 | $count= $this->execute($q); 35 | 36 | } 37 | 38 | public function down() { 39 | $table= $this->table('txn'); 40 | $table 41 | ->removeColumn('status') 42 | ->removeIndex([ 'status' ]) 43 | ->save(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:8.2.10-fpm-alpine 2 | 3 | LABEL maintainer="Jim Winstead " 4 | 5 | RUN apk add --no-cache -X http://dl-cdn.alpinelinux.org/alpine/edge/testing \ 6 | freetype-dev \ 7 | gifsicle \ 8 | git \ 9 | gmp-dev \ 10 | jpegoptim \ 11 | libjpeg-turbo-dev \ 12 | libpng-dev \ 13 | libzip-dev \ 14 | optipng \ 15 | mpdecimal-dev \ 16 | pngquant \ 17 | mysql-client \ 18 | tzdata \ 19 | zip \ 20 | zlib-dev \ 21 | ${PHPIZE_DEPS} \ 22 | && pecl install decimal \ 23 | && docker-php-ext-enable decimal \ 24 | && docker-php-ext-install \ 25 | bcmath \ 26 | gd \ 27 | gmp \ 28 | mysqli \ 29 | pdo \ 30 | pdo_mysql \ 31 | zip \ 32 | && apk del -dev ${PHPIZE_DEPS} 33 | 34 | WORKDIR /app 35 | 36 | COPY . /app 37 | 38 | COPY config/php-fpm-log.conf /usr/local/etc/php-fpm.d/ 39 | 40 | RUN curl -sS https://getcomposer.org/installer | php \ 41 | && mv composer.phar /usr/local/bin/ \ 42 | && ln -s /usr/local/bin/composer.phar /usr/local/bin/composer 43 | 44 | RUN composer install \ 45 | --no-dev --no-interaction --no-progress \ 46 | --optimize-autoloader --classmap-authoritative 47 | -------------------------------------------------------------------------------- /db/migrations/20200824230213_add_canned_emails.php: -------------------------------------------------------------------------------- 1 | table('canned_message', [ 'signed' => false ]); 11 | $table 12 | ->addColumn('slug', 'string', [ 'limit' => 50 ]) 13 | ->addColumn('subject', 'string', [ 'limit' => 255, 'null' => true ]) 14 | ->addColumn('content', 'text', [ 15 | 'limit' => MysqlAdapter::TEXT_MEDIUM, 16 | 'null' => true, 17 | ]) 18 | /* Ideally would be same enum as txn table, but oh well */ 19 | ->addColumn('new_status', 'string', [ 'limit' => 50 ]) 20 | ->addColumn('created', 'datetime', [ 21 | 'default' => 'CURRENT_TIMESTAMP' 22 | ]) 23 | ->addColumn('modified', 'datetime', [ 24 | 'default' => 'CURRENT_TIMESTAMP', 25 | 'update' => 'CURRENT_TIMESTAMP' 26 | ]) 27 | ->addColumn('active', 'integer', [ 28 | 'limit' => MysqlAdapter::INT_TINY, 29 | 'default' => 1, 30 | ]) 31 | ->create(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /ui/pos/email/delivery.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout/email.html' %} 2 | {% import 'macros.twig' as scat %} 3 | 4 | {% set no_details=1 %} 5 | 6 | {% set vehicle_sizes= { 7 | 'sm': 'Small', 8 | 'md': 'Medium', 9 | 'lg': 'Large', 10 | 'xl': 'XL', 11 | 'xxl': 'XXL', 12 | }%} 13 | 14 | {% block title -%} 15 | {{ subject }} 16 | {%- endblock %} 17 | 18 | {% block content_top %} 19 | {% set address= txn.shipping_address() %} 20 |

    21 |

    {{ address.name }}
    22 |
    {{ address.company }}
    23 |
    {{ address.email }}
    24 | {% if address.phone %} 25 |
    {{ address.phone | phone_number_format }}
    26 | {% endif %} 27 |
    {{ address.street1 }}
    28 |
    {{ address.street2 }}
    29 |
    30 | {% if address.city %} 31 | {{ address.city }}, 32 | {% endif %} 33 | {{ address.state }} 34 | {{ address.zip }} 35 |
    36 |

    37 | 38 |

    39 |

    Rate: {{ scat.amount(delivery.rate) }}
    40 |
    Vehicle: {{ vehicle_sizes[delivery.service] }}
    41 |

    42 | 43 |

    44 | {{ delivery.handling_instructions }} 45 |

    46 | 47 |

    48 | Ready to go now. 49 |

    50 | 51 |

    52 | Thanks! 53 |

    54 | {% endblock %} 55 | 56 | {% block content_bottom %} 57 | {% endblock %} 58 | -------------------------------------------------------------------------------- /lib/Scat/Exception/FileUploadException.php: -------------------------------------------------------------------------------- 1 | codeToMessage($code); 8 | parent::__construct($message, $code); 9 | } 10 | 11 | private function codeToMessage($code) 12 | { 13 | switch ($code) { 14 | case UPLOAD_ERR_INI_SIZE: 15 | $message= "The uploaded file exceeds the upload_max_filesize directive in php.ini"; 16 | break; 17 | case UPLOAD_ERR_FORM_SIZE: 18 | $message= "The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form"; 19 | break; 20 | case UPLOAD_ERR_PARTIAL: 21 | $message= "The uploaded file was only partially uploaded"; 22 | break; 23 | case UPLOAD_ERR_NO_FILE: 24 | $message= "No file was uploaded"; 25 | break; 26 | case UPLOAD_ERR_NO_TMP_DIR: 27 | $message= "Missing a temporary folder"; 28 | break; 29 | case UPLOAD_ERR_CANT_WRITE: 30 | $message= "Failed to write file to disk"; 31 | break; 32 | case UPLOAD_ERR_EXTENSION: 33 | $message= "File upload stopped by extension"; 34 | break; 35 | 36 | default: 37 | $message= "Unknown upload error"; 38 | break; 39 | } 40 | 41 | return $message; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /ui/pos/dialog/pay-cash.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout/dialog.html' %} 2 | {% import 'macros.twig' as scat %} 3 | 4 | {% block title %} 5 | Cash Payment 6 | {% endblock %} 7 | 8 | {% block size 'modal-sm' %} 9 | 10 | {% block body %} 11 | 20 | {% endblock %} 21 | 22 | {% block submit %} 23 | 26 | {% endblock %} 27 | 28 | {% block script %} 29 | form.onsubmit= (event) => { 30 | event.preventDefault() 31 | 32 | let form= dialog.getElementsByTagName('form')[0] 33 | let formData= new FormData(form) 34 | 35 | if (form.disabled) return; 36 | form.disabled= true 37 | 38 | return scat.post("/sale/{{ txn.id }}/payment", formData) 39 | .then((res) => res.json()) 40 | .then((data) => { 41 | dialog.resolution= data 42 | $(dialog).modal('hide') 43 | }) 44 | .catch((err) => { 45 | form.disabled= false 46 | scat.alert('danger', err.message) 47 | }) 48 | } 49 | {% endblock %} 50 | 51 | -------------------------------------------------------------------------------- /ui/pos/email/out_for_delivery.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout/email.html' %} 2 | {% import 'macros.twig' as scat %} 3 | 4 | {% block title -%} 5 | Your order is out for delivery! 6 | {%- endblock %} 7 | 8 | {% block content_top %} 9 |

    10 | {% if txn.person.name %} 11 | Dear {{ txn.person.name }}, 12 | {% else %} 13 | Dear, 14 | {% endif %} 15 |

    16 | 17 |

    18 | Thanks again for your order from Raw Materials Art Supplies! 19 |

    20 |

    21 | We wanted to let you know that your order ({{ txn.formatted_number }}) was 22 | reported to be out for delivery by {{ scat.format_shipping_carrier(tracker.carrier) }} on 23 | {{ (out_for_delivery ?: tracker.updated_at) | date("c", "UTC") | date("l, F j", txn.shipping_address.timezone) }}. 24 | {% if out_for_delivery and out_for_delivery|date("Y-m-d") != tracker.updated_at|date("Y-m-d") %} 25 | (Sorry for the slow update, we just got the news!) 26 | {% endif %} 27 |

    28 |

    29 | If there are any problems with receiving your package, please let us know right away so we can get it sorted out! 30 |

    31 | 32 | {% endblock %} 33 | 34 | {% set call_to_action_url= tracker.public_url %} 35 | {% block call_to_action "Track Your Shipment" %} 36 | 37 | {% block content_bottom %} 38 |

    39 | Thank you for your business! 40 |

    41 | {% endblock %} 42 | -------------------------------------------------------------------------------- /db/migrations/20200411021256_add_giftcard_times.php: -------------------------------------------------------------------------------- 1 | table('giftcard', [ 'signed' => false ]); 10 | $table 11 | /* Don't use ->addTimestamps() because we use DATETIME */ 12 | ->addColumn('created_at', 'datetime', [ 13 | 'default' => 'CURRENT_TIMESTAMP', 14 | ]) 15 | ->addColumn('updated_at', 'datetime', [ 16 | 'update' => 'CURRENT_TIMESTAMP', 17 | 'default' => 'CURRENT_TIMESTAMP', 18 | ]) 19 | ->addIndex(['created_at']) 20 | ->save(); 21 | 22 | $this->execute("UPDATE giftcard SET 23 | created_at = IFNULL((SELECT MIN(entered) 24 | FROM giftcard_txn 25 | WHERE card_id = giftcard.id), 26 | created_at), 27 | updated_at= IFNULL((SELECT MAX(entered) 28 | FROM giftcard_txn 29 | WHERE card_id = giftcard.id), 30 | updated_at)"); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /ui/pos/email/available_for_pickup.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout/email.html' %} 2 | {% import 'macros.twig' as scat %} 3 | 4 | {% block title -%} 5 | Your order is ready for pickup! 6 | {%- endblock %} 7 | 8 | {% block content_top %} 9 |

    10 | {% if txn.person.name %} 11 | Dear {{ txn.person.name }}, 12 | {% else %} 13 | Dear, 14 | {% endif %} 15 |

    16 | 17 |

    18 | Thank you for your order from Raw Materials Art Supplies! 19 |

    20 |

    21 | We wanted to let you know that your order ({{ txn.formatted_number }}) was 22 | reported to be ready for pickup from {{ scat.format_shipping_carrier(tracker.carrier) }} on 23 | {{ (available_for_pickup ?: tracker.updated_at) | date("c", "UTC") | date("l, F j", txn.shipping_address.timezone) }}. 24 | {% if available_for_pickup and available_for_pickup|date("Y-m-d") != tracker.updated_at|date("Y-m-d") %} 25 | (Sorry for the slow update, we just got the news!) 26 | {% endif %} 27 |

    28 |

    29 | If there are any problems with picking up your package, please let us know right away so we can get it sorted out! 30 |

    31 | 32 | {% endblock %} 33 | 34 | {% set call_to_action_url= tracker.public_url %} 35 | {% block call_to_action "Track Your Shipment" %} 36 | 37 | {% block content_bottom %} 38 |

    39 | Thank you for your business! 40 |

    41 | {% endblock %} 42 | -------------------------------------------------------------------------------- /ui/web/paymarks/cc-discover.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /db/migrations/20190525204400_initial_functions.php: -------------------------------------------------------------------------------- 1 | execute($query); 22 | 23 | $query= <<execute($query); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /static/sitemap-pages.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | https://rawmaterialsla.com/ 6 | daily 7 | 0.8 8 | 9 | 10 | https://rawmaterialsla.com/art-supplies 11 | 12 | 13 | https://rawmaterialsla.com/framing 14 | 15 | 16 | https://rawmaterialsla.com/buy-gift-card 17 | 18 | 19 | https://rawmaterialsla.com/easel-rental 20 | 21 | 22 | https://rawmaterialsla.com/blog/ 23 | 24 | 25 | https://rawmaterialsla.com/workshops 26 | 27 | 28 | https://rawmaterialsla.com/jobs 29 | 30 | 31 | https://rawmaterialsla.com/newsletter 32 | 33 | 34 | https://rawmaterialsla.com/windowbox 35 | 36 | 37 | https://rawmaterialsla.com/terms 38 | 39 | 40 | https://rawmaterialsla.com/privacy-policy 41 | 42 | 43 | https://rawmaterialsla.com/shipping 44 | 45 | 46 | https://rawmaterialsla.com/sales-tax-policy 47 | 48 | 49 | -------------------------------------------------------------------------------- /ui/pos/report/inventory-by-brand.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout/page.html' %} 2 | {% import 'macros.twig' as scat %} 3 | 4 | {% block title %} 5 | Inventory By Brand 6 | {% endblock %} 7 | 8 | {% block content %} 9 |
    11 |
    12 | 15 |
    16 | 17 |
    18 |
    19 |
    20 |
    21 | 22 |
    23 |
    24 |
    25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | {% for brand in brands %} 35 | 36 | 37 | 38 | 39 | {% endfor %} 40 | 41 |
    BrandTotal
    {{ brand.name ?? '(unknown)' }}{{ scat.amount(brand.total) }}
    42 | {% endblock %} 43 | -------------------------------------------------------------------------------- /lib/Scat/Model/VendorItem.php: -------------------------------------------------------------------------------- 1 | belongs_to('Item')->find_one(); 7 | } 8 | 9 | public function vendor() { 10 | return $this->belongs_to('Person', 'vendor_id')->find_one(); 11 | } 12 | 13 | public function set($name, $value= null) { 14 | if ($name == 'promo_price') { 15 | if (!strlen($value)) $value= null; 16 | } 17 | if ($name == 'dimensions') { 18 | return $this->setDimensions($value); 19 | } 20 | if ($name == 'promo_quantity' || $name == 'weight') { 21 | if ($value == '') $value= null; 22 | } 23 | 24 | return parent::set($name, $value); 25 | } 26 | 27 | public function dimensions() { 28 | if ($this->length && $this->width && $this->height) 29 | return $this->length . 'x' . 30 | $this->width . 'x' . 31 | $this->height; 32 | } 33 | 34 | public function setDimensions($dimensions) { 35 | if ($dimensions == '') { 36 | list($l, $w, $h)= [ null, null, null ]; 37 | } else { 38 | list($l, $w, $h)= preg_split('/[^\d.]+/', trim($dimensions)); 39 | } 40 | $this->length= $l; 41 | $this->width= $w; 42 | $this->height= $h; 43 | return $this; 44 | } 45 | 46 | public function getFields() { 47 | $fields= parent::getFields(); 48 | $fields[]= 'dimensions'; 49 | return $fields; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /lib/Scat/Search/Lexer.php: -------------------------------------------------------------------------------- 1 | regex= '~(' . implode(')|(', array_keys($tokenMap)) . ')~Ai'; 11 | $this->offsetToToken= array_values($tokenMap); 12 | } 13 | 14 | public function lex($string) { 15 | $tokens= array(); 16 | 17 | $offset= 0; 18 | while (isset($string[$offset])) { 19 | if (!preg_match($this->regex, $string, $matches, 0, $offset)) { 20 | throw new LexingException( 21 | sprintf('Unexpected character "%s" at %d', $string[$offset], $offset) 22 | ); 23 | } 24 | 25 | // find the first non-empty element using a quick for loop 26 | for ($i= 1; '' === $matches[$i]; ++$i); 27 | 28 | // only saved named tokens (rest is just fluff, like whitespace) 29 | if ($this->offsetToToken[$i - 1]) { 30 | $tokens[]= new Token($this->offsetToToken[$i - 1],$matches[$i]); 31 | } 32 | 33 | $offset+= strlen($matches[$i]); 34 | } 35 | 36 | return $tokens; 37 | } 38 | } 39 | 40 | class Token { 41 | public $type, $value; 42 | public function __construct($type, $value) { 43 | $this->type= $type; 44 | $this->value= $value; 45 | } 46 | } 47 | 48 | class LexingException extends \Exception { 49 | } 50 | -------------------------------------------------------------------------------- /lib/Scat/Model/PriceOverride.php: -------------------------------------------------------------------------------- 1 | pattern_type == 'product' ? 7 | $this->belongs_to('Product', 'pattern')->find_one() : 8 | null); 9 | } 10 | 11 | public function setDiscount($discount) { 12 | $discount= preg_replace('/^\\$/', '', $discount); 13 | if (preg_match('/^(\d*)(\/|%)( off)?$/', $discount, $m)) { 14 | $discount = (float)$m[1]; 15 | $discount_type = "percentage"; 16 | } 17 | elseif (preg_match('/^\+(\d*)(\/|%)( off)?$/', $discount, $m)) { 18 | $discount = (float)$m[1]; 19 | $discount_type = "additional_percentage"; 20 | } elseif (preg_match('/^(\d*\.?\d*)$/', $discount, $m)) { 21 | $discount = (float)$m[1]; 22 | $discount_type = "fixed"; 23 | } elseif (preg_match('/^\$?(\d*\.?\d*)( off)?$/', $discount, $m)) { 24 | $discount = (float)$m[1]; 25 | $discount_type = "relative"; 26 | } elseif (preg_match('/^-\$?(\d*\.?\d*)$/', $discount, $m)) { 27 | $discount = (float)$m[1]; 28 | $discount_type = "relative"; 29 | } elseif (preg_match('/^(def|\.\.\.)$/', $discount)) { 30 | $discount= null; 31 | $discount_type= null; 32 | } else { 33 | throw new \Exception("Did not understand discount."); 34 | } 35 | $this->discount= $discount; 36 | $this->discount_type= $discount_type; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/Scat/Service/Giftcard.php: -------------------------------------------------------------------------------- 1 | data= $data; 10 | } 11 | 12 | public function create($expires= null) { 13 | $card= $this->data->factory('Giftcard')->create(); 14 | 15 | $card->set_expr('pin', 'SUBSTR(RAND(), 5, 4)'); 16 | if ($expires) { 17 | $card->expires= $expires . ' 23:59:59'; 18 | } 19 | $card->active= 1; 20 | 21 | $card->save(); 22 | 23 | return $card; 24 | } 25 | 26 | public function check_balance($card) { 27 | $card= preg_replace('/^RAW-/', '', $card); 28 | 29 | $id= substr($card, 0, 7); 30 | $pin= substr($card, -4); 31 | 32 | $card= $this->data->factory('Giftcard') 33 | ->where('id', $id) 34 | ->where('pin', $pin) 35 | ->find_one(); 36 | 37 | if (!$card) { 38 | throw new \Exception("Unable to find info for that card."); 39 | } 40 | 41 | return $card; 42 | } 43 | 44 | public function add_txn($card, $amount, $txn_id= 0) { 45 | if (!$amount) { 46 | throw new \Exception("No amount specified."); 47 | } 48 | 49 | $card= $this->check_balance($card); 50 | 51 | $txn= $card->txns()->create(); 52 | $txn->amount= $amount; 53 | $txn->card_id= $card->id; 54 | if ($txn_id) $txn->txn_id= $txn_id; 55 | $txn->save(); 56 | 57 | return $card; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /ui/pos/dialog/wordform.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout/dialog.html' %} 2 | {% import 'macros.twig' as scat %} 3 | 4 | {% block title %} 5 | Wordform 6 | {% endblock %} 7 | 8 | {% block body %} 9 | 26 | {% endblock %} 27 | 28 | {% block submit %} 29 | 32 | {% endblock %} 33 | 34 | {% block script %} 35 | form.onsubmit= (event) => { 36 | event.preventDefault() 37 | 38 | let form= dialog.getElementsByTagName('form')[0] 39 | let formData= new FormData(form) 40 | scat.post("/settings/wordform{{ wordform.id ? '/' ~ wordform.id }}", formData) 41 | .then((res) => { 42 | if (res.redirected) { 43 | window.location.href= res.url 44 | } else { 45 | window.location.reload() 46 | } 47 | }) 48 | } 49 | {% endblock %} 50 | -------------------------------------------------------------------------------- /ui/pos/layout/dialog.html: -------------------------------------------------------------------------------- 1 | 46 | -------------------------------------------------------------------------------- /db/migrations/20200430172852_add_person_rewards_plus_newsletter.php: -------------------------------------------------------------------------------- 1 | table('person'); 11 | $table 12 | ->removeColumn('payment_account_id') 13 | ->removeColumn('sms_ok') 14 | ->removeColumn('email_ok') 15 | ->addColumn('mailerlite_id', 'string', [ 16 | 'limit' => 32, 17 | 'null' => true, 18 | 'after' => 'email', 19 | ]) 20 | ->addColumn('preferred_contact', 'enum', [ 21 | 'values' => [ 'any', 'call', 'text', 'email', 'none' ], 22 | 'null' => true, 23 | 'default' => 'any', 24 | 'after' => 'phone', 25 | ]) 26 | ->addColumn('rewardsplus', 'integer', [ 27 | 'signed' => 'false', 28 | 'limit' => MysqlAdapter::INT_TINY, 29 | 'null' => true, 30 | 'default' => 0, 31 | 'after' => 'suppress_loyalty', 32 | ]) 33 | ->addColumn('subscriptions', 'json', [ 34 | 'null' => true, 35 | 'after' => 'mailerlite_id', 36 | ]) 37 | ->save(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/Scat/Service/Cart.php: -------------------------------------------------------------------------------- 1 | data->Factory('Cart')->create(); 13 | if ($data) { 14 | $cart->hydrate($data); 15 | } 16 | 17 | // Could use real UUID() but this is shorter. Hardcoded '1' could be 18 | // replaced with a server-id to further avoid collisions 19 | $cart->uuid= sprintf("%08x%02x%s", time(), 1, bin2hex(random_bytes(8))); 20 | 21 | return $cart; 22 | } 23 | 24 | public function findByUuid($uuid) { 25 | return $this->data->Factory('Cart')->where('uuid', $uuid)->find_one(); 26 | } 27 | 28 | public function findByStatus($status, $yesterday= null, $limit= null) { 29 | $query= $this->data->Factory('Cart'); 30 | if (is_array($status)) { 31 | $query= $query->where_in('status', $status); 32 | } elseif ($status) { 33 | $query= $query->where('status', $status); 34 | } 35 | if ($yesterday) { 36 | $query= $query->where_raw("DATE(created) = DATE(NOW() - INTERVAL 1 DAY)"); 37 | } 38 | 39 | // filter out annoying Google tests 40 | $query= $query->where_raw("(email is NULL OR email NOT RLIKE '^(fake.*@fakemail.com|johnsmithstore.*@gmail.com|johnsmith.*@storebotmail\.joonix\.net)$')"); 41 | 42 | if ($limit) { 43 | $query= $query->limit($limit)->order_by_desc('id'); 44 | } 45 | return $query->find_many(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /ui/pos/person/backorders.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout/page.html' %} 2 | {% import 'macros.twig' as scat %} 3 | 4 | {% block title %} 5 | Backorders for {{ person.friendly_name }} 6 | {% endblock %} 7 | 8 | {% block content %} 9 | {% if not backorders|length %} 10 |

    No backorders for this vendor.

    11 | {% endif %} 12 | 13 | {% for backorder in backorders %} 14 | 15 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | {% for item in backorder.items %} 33 | 34 | 35 | 40 | 41 | 42 | 43 | {% endfor %} 44 | 45 |
    16 |

    17 | 18 | {{ backorder.txn.friendly_type }} 19 | {{ backorder.txn.formatted_number }} 20 | 21 |

    22 |
    SKUCodeNameQuantity
    {{ item.vendor_sku(person.id) }} 36 | 37 | {{ item.code }} 38 | 39 | {{ item.name }}{{ item.ordered - item.allocated }}
    46 | {% endfor %} 47 | {% endblock %} 48 | -------------------------------------------------------------------------------- /ui/web/cart/get-help.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout/page.html' %} 2 | {% import 'macros.twig' as scat %} 3 | 4 | {% block title %} 5 | Get Help @ Raw Materials Art Supplies 6 | {% endblock %} 7 | 8 | {% block notice %} 9 | {% endblock %} 10 | 11 | {% block content %} 12 |

    13 | Get Help with your Cart 14 |

    15 | 16 |

    17 | Need help completing your purchase? Sorry, sometimes the system 18 | isn't able to calculate shipping costs due to incomplete information in 19 | our system or other technical problems. We try to respond to all requests 20 | for help within one business day. 21 |

    22 | 23 |

    24 | Please note that we do not currently ship canvases or panels larger than 25 | 30" × 40" and rolls of seamless backdrop paper wider than 86" 26 | outside of our local Los Angeles delivery area. 27 |

    28 | 29 |
    30 | 33 | 34 | 35 | 39 |
    40 | 41 | {% embed 'cart/cart.twig' %} 42 | {% block buttons %} 43 | {% endblock %} 44 | {% endembed %} 45 | 46 | {% endblock %} 47 | 48 | {% block script %} 49 | {% endblock %} 50 | -------------------------------------------------------------------------------- /ui/pos/settings/wordforms.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout/page.html' %} 2 | 3 | {% block title %} 4 | Wordforms 5 | {% endblock %} 6 | 7 | {% block content %} 8 | 9 |

    Wordforms

    10 | 11 | {% include 'settings/nav.twig' %} 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | {% for wordform in wordforms %} 23 | 24 | 27 | 30 | 33 | 34 | {% endfor %} 35 | 36 | 37 | 42 |
    SourceDestinationLast Modified
    25 | {{- wordform.source -}} 26 | 28 | {{- wordform.dest -}} 29 | 31 | {{- wordform.modified ?: wordform.added -}} 32 |
    38 | 41 |
    43 | 44 | {% endblock %} 45 | 46 | {% block script %} 47 | 59 | {% endblock %} 60 | -------------------------------------------------------------------------------- /ui/web/edit.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout/page.html' %} 2 | {% import 'macros.twig' as scat %} 3 | 4 | {% block title -%} 5 | {{ content.title }} 6 | {%- endblock %} 7 | 8 | {% block content %} 9 | 17 | 18 | {# avoid trailing / problem when editing front page #} 19 | {% set url= current_url() %} 20 | {% if url == '/' %} 21 | {% set url = '' %} 22 | {% endif %} 23 | 24 |
    25 | 26 | 27 | 28 | 29 |
    30 | 38 | 39 |
    40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 51 | 52 |
    53 | 54 | {% endblock %} 55 | -------------------------------------------------------------------------------- /ui/web/catalog/product.twig: -------------------------------------------------------------------------------- 1 | {# Product details #} 2 |

    3 | {{ product.name }} 4 | 5 | {% set brand = product.brand %} 6 | 7 | {{ brand.name }} 8 | 9 | 10 |

    11 | 12 |
    13 | 14 | 40 | 41 |
    42 |
    43 | {{ product.description | replace({'{{ @STATIC }}' : STATIC}) | markdown_to_html }} 44 | 45 | {% if variations|length > 1 %} 46 |

    Jump to:

    47 | 55 | {% endif %} 56 |
    57 |
    58 | {% set media = product.media %} 59 | {% if media is not empty %} 60 | {{ include('carousel.twig', { images: product.media }) }} 61 | {% endif %} 62 |
    63 |
    64 | -------------------------------------------------------------------------------- /ui/pos/catalog/products.twig: -------------------------------------------------------------------------------- 1 | {% for p in products %} 2 | {% set slug = url_for('catalog') ~ '/' ~ p.full_slug %} 3 |
    4 | 16 |
    17 |

    18 | 19 | {{- p.name -}} 20 | 21 | {% set brand = p.brand %} 22 | 23 | 24 | {{- brand.name -}} 25 | 26 | 27 | {% if not p.stocked %} 28 | 29 | Special Order Only 30 | 31 | {% endif %} 32 |

    33 | {# Just extract the first paragraph #} 34 | {% set paragraphs= p.description|markdown_to_html|split('

    ', 2) %} 35 | {% set firstParagraph= paragraphs|first ~ '

    ' %} 36 | {{ firstParagraph | raw }} 37 |
    38 |
    39 |
    40 | {% endfor %} 41 | 46 | -------------------------------------------------------------------------------- /ui/pos/shipping/shipment.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout/page.html' %} 2 | {% import 'macros.twig' as scat %} 3 | 4 | {% block title %} 5 | Shipment: {{ shipment.id }} 6 | {% endblock %} 7 | 8 | {% block content %} 9 |

    10 | {{ block('title') }} 11 |

    12 | 13 |
    14 |
    15 |
    16 |
    17 |

    Package

    18 |
    19 |
    20 |
    21 | 24 |
    25 |

    27 | {{ shipment.weight }} 28 |

    29 |
    30 |
    31 | 32 |
    33 | 36 |
    37 |

    39 | {{ shipment.dimensions }} 40 |

    41 |
    42 |
    43 | 44 |
    45 |
    46 | 47 |
    48 | 49 |
    50 | {% endblock %} 51 | -------------------------------------------------------------------------------- /ui/pos/report/shipments.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout/page.html' %} 2 | {% import 'macros.twig' as scat %} 3 | 4 | {% block title %} 5 | Shipments 6 | {% endblock %} 7 | 8 | {% block content %} 9 |

    Shipments in last 30 days.

    10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | {% for shipment in shipments %} 24 | 25 | 28 | 33 | 39 | 42 | 47 | 51 | 54 | 55 | {% endfor %} 56 | 57 |
    #CreatedDimensions (")Weight (lbs)DestinationCarrier / ServiceRate
    26 | {{ loop.index }} 27 | 29 | 30 | {{ shipment.created | date('Y-m-d') }} 31 | 32 | 34 | {% if shipment.width %} 35 | {{ shipment.width }} x {{ shipment.length }} x {{ shipment.height }} 36 | {% else %} 37 | {% endif %} 38 | 40 | {{ shipment.weight }} 41 | 43 | {{ shipment.txn.shipping_address.city }}, 44 | {{ shipment.txn.shipping_address.state }} 45 | {{ shipment.txn.shipping_address.zip }} 46 | 48 | {{ scat.format_shipping_carrier(shipment.carrier) }} / 49 | {{ shipment.service }} 50 | 52 | {{ scat.amount(shipment.rate) }} 53 |
    58 | {% endblock %} 59 | -------------------------------------------------------------------------------- /lib/Scat/Service/Dejavoo.php: -------------------------------------------------------------------------------- 1 | url= $config->get('dejavoo.url'); 14 | $this->key= $config->get('dejavoo.key'); 15 | 16 | /* This pulls in CC_Terminal class which is encrypted due to NDA. */ 17 | $enc= file_get_contents('../lib/cc-terminal.phpc'); 18 | $dec= Cryptor::Decrypt($enc, $this->key); 19 | eval('?>' . $dec); 20 | } 21 | 22 | public function runTransaction($txn, $amount) { 23 | $cc= new \CC_Terminal($this->url); 24 | 25 | $label= $txn->formatted_number; 26 | 27 | $abs_amt= sprintf('%.02f', abs($amount)); 28 | 29 | list($captured, $extra)= 30 | $cc->transaction($amount < 0 ? 'Return' : 'Sale', $abs_amt, $label); 31 | 32 | if ($amount < 0) { 33 | $captured= bcmul($captured, -1); 34 | } 35 | 36 | /* 37 | * We want to record the trace, but we ignore any errors because 38 | * we don't want to roll back the payment if it has a problem. 39 | */ 40 | try { 41 | $trace= $this->data->factory('CcTrace')->create(); 42 | $trace->txn_id= $txn->id; 43 | $trace->request= $cc->raw_request; 44 | $trace->response= $cc->raw_response; 45 | $trace->info= $cc->raw_curlinfo; 46 | $trace->save(); 47 | } catch (\Exception $e) { 48 | error_log("Failed to capture trace: " . $e->getMessage()); 49 | } 50 | 51 | return [ 52 | 'amount' => $captured, 53 | 'data' => $extra 54 | ]; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /ui/web/wishlist/shared.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout/page.html' %} 2 | {% import 'macros.twig' as scat %} 3 | 4 | {% block title %} 5 | Shared Wishlist @ Raw Materials Art Supplies 6 | {% endblock %} 7 | 8 | {% block content %} 9 | 10 | {% set items= wishlist.items.find_many() %} 11 | {% if not items|length %} 12 |
    13 | There is nothing in this wishlist. Sorry, there's nothing to show! 14 |
    15 | {% else %} 16 | {% if person and person.id == wishlist.person_id %} 17 |
    18 | This is the shared view of your wishlist. 19 |
    20 | 21 | Edit the Wishlist 22 | 23 |
    24 | {% else %} 25 | {# guidance on creating your own wishlist? #} 26 | {% endif %} 27 | 28 | {{ include('wishlist/links.twig') }} 29 | 30 |
    31 | {% for item in items %} 32 | {% set i= item.item %} 33 |
    34 | {{ i.name }} 35 |

    {{ i.name }}

    36 | {% if not i.active or not i.no_backorder or i.stock > 0 %} 37 | {{ include('add-to-cart.twig', { item: i, no_add_to_wishlist: true, fixed_quantity: true }) }} 38 | {% else %} 39 | 42 | {% endif %} 43 |
    44 | {% endfor %} 45 |
    46 | {% endif %} 47 | {% endblock %} 48 | -------------------------------------------------------------------------------- /extern/x-editable-1.5.1/inputs-ext/typeaheadjs/lib/typeahead.js-bootstrap.css: -------------------------------------------------------------------------------- 1 | .twitter-typeahead .tt-query, 2 | .twitter-typeahead .tt-hint { 3 | margin-bottom: 0; 4 | } 5 | 6 | .tt-dropdown-menu { 7 | min-width: 160px; 8 | margin-top: 2px; 9 | padding: 5px 0; 10 | background-color: #fff; 11 | border: 1px solid #ccc; 12 | border: 1px solid rgba(0,0,0,.2); 13 | *border-right-width: 2px; 14 | *border-bottom-width: 2px; 15 | -webkit-border-radius: 6px; 16 | -moz-border-radius: 6px; 17 | border-radius: 6px; 18 | -webkit-box-shadow: 0 5px 10px rgba(0,0,0,.2); 19 | -moz-box-shadow: 0 5px 10px rgba(0,0,0,.2); 20 | box-shadow: 0 5px 10px rgba(0,0,0,.2); 21 | -webkit-background-clip: padding-box; 22 | -moz-background-clip: padding; 23 | background-clip: padding-box; 24 | } 25 | 26 | .tt-suggestion { 27 | display: block; 28 | padding: 3px 20px; 29 | } 30 | 31 | .tt-suggestion.tt-is-under-cursor { 32 | color: #fff; 33 | background-color: #0081c2; 34 | background-image: -moz-linear-gradient(top, #0088cc, #0077b3); 35 | background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0077b3)); 36 | background-image: -webkit-linear-gradient(top, #0088cc, #0077b3); 37 | background-image: -o-linear-gradient(top, #0088cc, #0077b3); 38 | background-image: linear-gradient(to bottom, #0088cc, #0077b3); 39 | background-repeat: repeat-x; 40 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc', endColorstr='#ff0077b3', GradientType=0) 41 | } 42 | 43 | .tt-suggestion.tt-is-under-cursor a { 44 | color: #fff; 45 | } 46 | 47 | .tt-suggestion p { 48 | margin: 0; 49 | } 50 | -------------------------------------------------------------------------------- /ui/pos/settings/messages.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout/page.html' %} 2 | 3 | {% block title %} 4 | Canned Messages 5 | {% endblock %} 6 | 7 | {% block content %} 8 | 9 |

    Canned Messages

    10 | 11 | {% include 'settings/nav.twig' %} 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | {% for message in messages %} 24 | 25 | 28 | 31 | 34 | 37 | 38 | {% endfor %} 39 | 40 | 41 | 46 |
    SlugSubjectNew StatusLast Modified
    26 | {{- message.slug -}} 27 | 29 | {{- message.subject -}} 30 | 32 | {{- message.new_status -}} 33 | 35 | {{- message.modified -}} 36 |
    42 | 45 |
    47 | 48 | {% endblock %} 49 | 50 | {% block script %} 51 | 63 | {% endblock %} 64 | -------------------------------------------------------------------------------- /db/seeds/DemoSeeder.php: -------------------------------------------------------------------------------- 1 | fetchRow("SELECT value FROM config WHERE name = 'tax.default_rate'"); 10 | /* Should not execute if tax.default_rate is set */ 11 | return is_array($row) && !array_key_exists('value', $row); 12 | } 13 | 14 | public function run() 15 | { 16 | /* In case we're running an older Phinx without shouldExecute() */ 17 | $row= $this->fetchRow("SELECT value FROM config WHERE name = 'tax.default_rate'"); 18 | if (is_array($row) && array_key_exists('value', $row)) { 19 | return; 20 | } 21 | 22 | /* Default config: a tax rate */ 23 | $config= $this->table('config'); 24 | $config->insert([ 25 | [ 'name' => 'tax.default_rate', 'value' => '7.5' ], 26 | ])->saveData(); 27 | 28 | $items= $this->table('item'); 29 | $items->insert([ 30 | [ 'code' => 'ZZ-GIFTCARD', 'name' => 'Gift Card', 'tic' => '10005', 'taxfree' => 1 ], 31 | [ 'code' => 'ACME-001', 'name' => 'Anvil', 'retail_price' => 100.00 ], 32 | [ 'code' => 'ACME-002', 'name' => 'Toaster', 'retail_price' => 49.99 ], 33 | [ 'code' => 'ACME-003', 'name' => 'Super Outfit', 'retail_price' => 24.99 ], 34 | [ 'code' => 'ACME-004', 'name' => 'Aspirin', 'retail_price' => 2.99 ], 35 | [ 'code' => 'ACME-005', 'name' => 'Matches', 'retail_price' => 1.99 ], 36 | [ 'code' => 'ACME-006', 'name' => 'Rocket-Powered Roller Skates', 'retail_price' => 19.99 ], 37 | [ 'code' => 'ACME-007', 'name' => 'Bird Seed', 'retail_price' => 4.99 ], 38 | ])->saveData(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /ui/pos/report/inventory-value.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout/page.html' %} 2 | {% import 'macros.twig' as scat %} 3 | 4 | {% block title %} 5 | Inventory Value 6 | {% endblock %} 7 | 8 | {% block content %} 9 |
    11 |
    12 | 15 |
    16 | 17 |
    18 |
    19 |
    20 |
    21 | 22 |
    23 |
    24 |
    25 | 26 |
    27 |
    28 |
    29 |
    30 |

    At Sale Price

    31 |
    32 |
    33 | 34 | {{ scat.amount(at_sale_price) }} 35 | 36 |
    37 |
    38 |
    39 |
    40 |
    41 |
    42 |

    At Cost

    43 |
    44 |
    45 | 46 | {{ scat.amount(at_cost) }} 47 | 48 |
    49 |
    50 |
    51 |
    52 | {% endblock %} 53 | -------------------------------------------------------------------------------- /ui/pos/dialog/item-google-history.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout/dialog.html' %} 2 | {% import 'macros.twig' as scat %} 3 | 4 | {% block title %} 5 | Google History for {{ product ? product.name : item.name }} 6 | {% endblock %} 7 | 8 | {% block body %} 9 | 14 | {% endblock %} 15 | 16 | {% block submit %} 17 | {% endblock %} 18 | 19 | {% block script %} 20 | { 21 | import('/extern/chart.js-4.3.3/chart.umd.js') 22 | .then((module) => { 23 | const labels = [ 24 | {% for date in history %} 25 | '{{ date.segments.date.year ~ '-' ~ date.segments.date.month ~ '-' ~ date.segments.date.day }}', 26 | {% endfor %} 27 | ]; 28 | 29 | const data = { 30 | labels: labels, 31 | datasets: [ 32 | { 33 | label: 'Impressions', 34 | backgroundColor: 'rgb(132, 99, 132)', 35 | borderColor: 'rgb(132, 99, 132)', 36 | data: [ 37 | {% for date in history %} 38 | {{ date.metrics.impressions }}, 39 | {% endfor %} 40 | ], 41 | }, 42 | { 43 | label: 'Clicks', 44 | backgroundColor: 'rgb(99, 132, 132)', 45 | borderColor: 'rgb(99, 132, 132)', 46 | data: [ 47 | {% for date in history %} 48 | {{ date.metrics.clicks }}, 49 | {% endfor %} 50 | ], 51 | } 52 | ] 53 | }; 54 | 55 | const config = { 56 | type: 'line', 57 | data: data, 58 | options: {} 59 | }; 60 | 61 | const myChart = new Chart( 62 | document.getElementById('my-chart'), 63 | config 64 | ); 65 | 66 | }) 67 | } 68 | {% endblock %} 69 | -------------------------------------------------------------------------------- /ui/web/catalog/brand.html: -------------------------------------------------------------------------------- 1 | {% extends 'catalog/page.html' %} 2 | 3 | {% block title %} 4 | {{ brand ? brand.name : 'Brands' }} 5 | {% endblock %} 6 | 7 | {% block catalog_crumb %} 8 | 23 | {% endblock %} 24 | 25 | {% block catalog_content %} 26 | 27 | {% if brands|length %} 28 | 37 | {% endif %} 38 | 39 | {% if brand %} 40 |

    42 | {{ brand.name }} 43 |

    44 | {% if brand.warning %} 45 |
    46 | {{ brand.warning }} 47 |
    48 | {% endif %} 49 | {% if brand.description %} 50 |

    {{ brand.description|markdown_to_html }}

    51 |
    52 |
    53 | {% endif %} 54 | {% endif %} 55 | 56 | {% if products %} 57 | {% include 'catalog/products.twig' %} 58 | {% endif %} 59 | 60 | {% endblock %} 61 | -------------------------------------------------------------------------------- /ui/pos/quickbooks/accounts.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout/page.html' %} 2 | {% import 'macros.twig' as scat %} 3 | 4 | {% block title %} 5 | Quickbooks Account Creation 6 | {% endblock %} 7 | 8 | {% block content %} 9 | 10 | 11 |
    12 | 13 | 14 | Disconnect 15 | 16 | 17 |
    18 | 19 | {% for i, account in accounts %} 20 | 24 | {% else %} 25 |
    26 | No accounts need to be created. 27 |
    28 | {% endfor %} 29 | 30 |
    31 | {% endblock %} 32 | 33 | {% block script %} 34 | 63 | {% endblock %} 64 | -------------------------------------------------------------------------------- /ui/pos/dialog/punch.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout/dialog.html' %} 2 | {% import 'macros.twig' as scat %} 3 | 4 | {% block title %} 5 | Clock Punch 6 | {% endblock %} 7 | 8 | {% block body %} 9 | 36 | {% endblock %} 37 | 38 | {% block submit %} 39 | 42 | {% endblock %} 43 | 44 | {% block script %} 45 | form.onsubmit= (event) => { 46 | event.preventDefault() 47 | 48 | let form= dialog.getElementsByTagName('form')[0] 49 | let formData= new FormData(form) 50 | 51 | return scat.patch('/clock/{{ punch.id }}', formData) 52 | .then((res) => res.json()) 53 | .then((data) => { 54 | dialog.resolution= data; 55 | $(dialog).modal('hide') 56 | }) 57 | } 58 | {% endblock %} 59 | -------------------------------------------------------------------------------- /ui/pos/person/index.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout/page.html' %} 2 | 3 | {% block title %} 4 | People 5 | {% endblock %} 6 | 7 | {% block content %} 8 | {% include 'person/searchform.twig' %} 9 | 10 | {% if q and not people %} 11 |

    12 | No results found. 13 | Nothing was found for those search parameters. 14 | {% if not (q matches '/active:0/') %} 15 | Try again with inactive people. 16 | {% endif %} 17 |

    18 | {% endif %} 19 | 20 | {% if people %} 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | {% for person in people %} 33 | 34 | 37 | 40 | 43 | 46 | 49 | 50 | {% endfor %} 51 | 52 |
    #NameCompanyPhoneEmail
    35 | {{ loop.index }} 36 | 38 | {{ person.name ?: '​' }} 39 | 41 | {{ person.company ?: '​' }} 42 | 44 | {{ person.pretty_phone ?: '​' }} 45 | 47 | {{ person.email ?: '​' }} 48 |
    53 | 56 | {% endif %} 57 | {% endblock %} 58 | -------------------------------------------------------------------------------- /ui/pos/dialog/tracker.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout/dialog.html' %} 2 | {% import 'macros.twig' as scat %} 3 | 4 | {% block title %} 5 | Add Tracker 6 | {% endblock %} 7 | 8 | {% block body %} 9 | 29 | {% endblock %} 30 | 31 | {% block submit %} 32 | 35 | {% endblock %} 36 | 37 | {% block script %} 38 | form.onsubmit= (event) => { 39 | event.preventDefault() 40 | 41 | let form= dialog.getElementsByTagName('form')[0] 42 | let formData= new FormData(form) 43 | fetch("/sale/{{ txn.id }}/shipment{{ shipment.id ? '/' ~ shipment.id }}", { 44 | method: '{{ shipment ? 'PATCH' : 'POST' }}', 45 | headers: { 46 | 'Accept' : 'application/json', 47 | 'Content-type' : 'application/json', 48 | }, 49 | body: JSON.stringify(Object.fromEntries(formData)) 50 | }) 51 | .then((res) => { 52 | if (!res.ok) { 53 | return Promise.reject(new Error(response.statusText)) 54 | } 55 | scat.alert('info', 'Added tracker.') 56 | $(dialog).modal('hide') 57 | }) 58 | } 59 | {% endblock %} 60 | -------------------------------------------------------------------------------- /ui/pos/dialog/dropship.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout/dialog.html' %} 2 | {% import 'macros.twig' as scat %} 3 | 4 | {% block title %} 5 | Create Drop Shipment 6 | {% endblock %} 7 | 8 | {% block body %} 9 | 29 | {% endblock %} 30 | 31 | {% block submit %} 32 | 35 | {% endblock %} 36 | 37 | {% block script %} 38 | form.onsubmit= (event) => { 39 | event.preventDefault() 40 | 41 | let form= dialog.getElementsByTagName('form')[0] 42 | let formData= new FormData(form) 43 | fetch("/sale/{{ txn.id }}/dropship{{ dropship.id ? '/' ~ dropship.id }}", { 44 | method: '{{ dropship ? 'PATCH' : 'POST' }}', 45 | headers: { 46 | 'Accept' : 'application/json', 47 | 'Content-type' : 'application/json', 48 | }, 49 | body: JSON.stringify(Object.fromEntries(formData)) 50 | }) 51 | .then((res) => { 52 | if (!res.ok) { 53 | return Promise.reject(new Error(response.statusText)) 54 | } 55 | return res.json() 56 | }) 57 | .then((data) => { 58 | window.location.href= '/purchase/' + data.id 59 | }) 60 | } 61 | {% endblock %} 62 | -------------------------------------------------------------------------------- /db/migrations/20191210233744_add_brand_department_active_and_times.php: -------------------------------------------------------------------------------- 1 | table('brand', [ 'signed' => false ]); 11 | $table 12 | /* Don't use ->addTimestamps() because we use DATETIME */ 13 | ->addColumn('created_at', 'datetime', [ 14 | 'default' => 'CURRENT_TIMESTAMP', 15 | ]) 16 | ->addColumn('updated_at', 'datetime', [ 17 | 'update' => 'CURRENT_TIMESTAMP', 18 | 'default' => 'CURRENT_TIMESTAMP', 19 | ]) 20 | ->addColumn('active', 'integer', [ 21 | 'signed' => 'false', 22 | 'limit' => MysqlAdapter::INT_TINY, 23 | 'default' => 0, 24 | ]) 25 | ->addIndex(['created_at']) 26 | ->save(); 27 | 28 | $table= $this->table('department', [ 'signed' => false ]); 29 | $table 30 | /* Don't use ->addTimestamps() because we use DATETIME */ 31 | ->addColumn('created_at', 'datetime', [ 32 | 'default' => 'CURRENT_TIMESTAMP', 33 | ]) 34 | ->addColumn('updated_at', 'datetime', [ 35 | 'update' => 'CURRENT_TIMESTAMP', 36 | 'default' => 'CURRENT_TIMESTAMP', 37 | ]) 38 | ->addColumn('active', 'integer', [ 39 | 'signed' => 'false', 40 | 'limit' => MysqlAdapter::INT_TINY, 41 | 'default' => 0, 42 | ]) 43 | ->addIndex(['created_at']) 44 | ->save(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /db/migrations/20201005192908_add_kits.php: -------------------------------------------------------------------------------- 1 | table('kit_item', [ 'signed' => false ]); 11 | $table 12 | ->addColumn('kit_id', 'integer', [ 13 | 'signed' => false, 14 | 'null' => false, 15 | ]) 16 | ->addColumn('item_id', 'integer', [ 17 | 'signed' => false, 18 | 'null' => false, 19 | ]) 20 | ->addColumn('sort', 'integer', [ 21 | 'signed' => false, 22 | 'null' => false, 23 | 'default' => 0, 24 | ]) 25 | ->addColumn('description', 'text', [ 26 | 'limit' => MysqlAdapter::TEXT_MEDIUM, 27 | 'null' => true, 28 | ]) 29 | ->addColumn('created', 'datetime', [ 30 | 'default' => 'CURRENT_TIMESTAMP' 31 | ]) 32 | ->addColumn('modified', 'datetime', [ 33 | 'default' => 'CURRENT_TIMESTAMP', 34 | 'update' => 'CURRENT_TIMESTAMP' 35 | ]) 36 | ->addIndex(['kit_id']) 37 | ->create(); 38 | 39 | $table= $this->table('item', [ 'signed' => false ]); 40 | $table 41 | ->addColumn('is_kit', 'integer', [ 42 | 'limit' => MysqlAdapter::INT_TINY, 43 | 'default' => 0, 44 | 'after' => 'tic', 45 | ]) 46 | ->save(); 47 | 48 | $table= $this->table('txn_line', [ 'signed' => false ]); 49 | $table 50 | ->addColumn('kit_id', 'integer', [ 51 | 'after' => 'item_id', 52 | 'signed' => false, 53 | 'null' => true 54 | ]) 55 | ->save(); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Scat is a web-based Point-of-Sale (POS) system. 2 | 3 | Scat requires PHP 8.1 or later and MySQL 8.0 or later. 4 | 5 | ## INSTALL 6 | 7 | You shouldn't try to run this "in production" unless you know what you're doing, but you can get it running in a test/demo environment using the included docker-compose configuration. 8 | 9 | * clone the repository 10 | * `docker-compose up` 11 | * connect to `http://localhost:5080` (or the server name if it’s not running on your local machine) 12 | * click the “Set up the database” button 13 | * click the "Load some sample data" button 14 | * click the “Return to Scat” button 15 | * start poking around 16 | 17 | ## Dependencies 18 | 19 | See `composer.json` for most of the dependencies, but here are some notable and/or bundled ones. 20 | 21 | Scat uses the jQuery Javascript library. 22 | http://jquery.com/ 23 | 24 | Scat uses the Bootstrap front-end framework: 25 | http://getbootstrap.com/ 26 | 27 | Scat uses Fork Awesome, a fork of the iconic font and CSS toolkit: 28 | http://forkaweso.me/ 29 | 30 | Scat includes bootstrap-datepicker.js: 31 | https://github.com/eternicode/bootstrap-datepicker/ 32 | 33 | Scat includes Mousetrap for handling keyboard shortcuts: 34 | https://craig.is/killing/mice 35 | 36 | Scat includes Chart.js for charting: 37 | http://chartjs.com/ 38 | 39 | Scat includes FPDF for PDF generation: 40 | http://www.fpdf.org/ 41 | 42 | Scat includes BarCode Coder Library for barcode generation: 43 | http://barcode-coder.com/ 44 | 45 | Scat uses PhpSpreadSheet for writing Excel spreadsheets: 46 | https://phpspreadsheet.readthedocs.io/ 47 | 48 | Scat includes X-editable: 49 | https://vitalets.github.io/x-editable/index.html 50 | 51 | Scat uses the Titi minimalist database toolkit: 52 | https://github.com/jimwins/titi 53 | 54 | See the LICENSE file for licensing information. 55 | -------------------------------------------------------------------------------- /lib/Scat/JsonErrorRenderer.php: -------------------------------------------------------------------------------- 1 | $exception->getMessage()]; 37 | 38 | if ($displayErrorDetails) { 39 | $error['exception'] = []; 40 | do { 41 | $error['exception'][] = $this->formatExceptionFragment($exception); 42 | } while ($exception = $exception->getPrevious()); 43 | } 44 | 45 | return (string) json_encode($error, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); 46 | } 47 | 48 | /** 49 | * @param Throwable $exception 50 | * @return array 51 | */ 52 | private function formatExceptionFragment(Throwable $exception): array 53 | { 54 | return [ 55 | 'type' => get_class($exception), 56 | 'code' => $exception->getCode(), 57 | 'message' => $exception->getMessage(), 58 | 'file' => $exception->getFile(), 59 | 'line' => $exception->getLine(), 60 | ]; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /ui/web/paymarks/cc-paypal.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /db/migrations/20200110000112_add_address_table.php: -------------------------------------------------------------------------------- 1 | table('address', [ 'signed' => false ]); 11 | $table 12 | ->addColumn('easypost_id', 'string', [ 'limit' => 50 ]) 13 | ->addColumn('name', 'string', [ 'limit' => 255, 'null' => true ]) 14 | ->addColumn('company', 'string', [ 'limit' => 255, 'null' => true ]) 15 | ->addColumn('street1', 'string', [ 'limit' => 255, 'null' => true ]) 16 | ->addColumn('street2', 'string', [ 'limit' => 255, 'null' => true ]) 17 | ->addColumn('city', 'string', [ 'limit' => 128, 'null' => true ]) 18 | ->addColumn('state', 'string', [ 'limit' => 50, 'null' => true ]) 19 | ->addColumn('zip', 'string', [ 'limit' => 10, 'null' => true ]) 20 | ->addColumn('country', 'string', [ 'limit' => 2, 'null' => true ]) 21 | ->addColumn('phone', 'string', [ 'limit' => 50, 'null' => true ]) 22 | ->addColumn('email', 'string', [ 'limit' => 255, 'null' => true ]) 23 | ->addColumn('residential', 'integer', [ 24 | 'limit' => MysqlAdapter::INT_TINY, 25 | 'default' => 0, 26 | ]) 27 | ->addColumn('created', 'datetime', [ 28 | 'default' => 'CURRENT_TIMESTAMP' 29 | ]) 30 | ->addColumn('modified', 'datetime', [ 31 | 'default' => 'CURRENT_TIMESTAMP', 32 | 'update' => 'CURRENT_TIMESTAMP' 33 | ]) 34 | ->addColumn('active', 'integer', [ 35 | 'limit' => MysqlAdapter::INT_TINY, 36 | 'default' => 1, 37 | ]) 38 | ->create(); 39 | 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /ui/pos/dialog/person-loyalty.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout/dialog.html' %} 2 | {% import 'macros.twig' as scat %} 3 | 4 | {% block title %} 5 | Loyalty Activity for {{ person.friendly_name }} 6 | {% endblock %} 7 | 8 | {% block body %} 9 | 41 | {% endblock %} 42 | 43 | {% block submit %} 44 | 47 | {% endblock %} 48 | 49 | {% block script %} 50 | form.onsubmit= (event) => { 51 | event.preventDefault() 52 | 53 | let form= dialog.getElementsByTagName('form')[0] 54 | let formData= new FormData(form) 55 | scat.call("/person/{{ person.id }}/loyalty", formData) 56 | .then((res) => { 57 | if (res.redirected) { 58 | window.location.href= res.url 59 | } else { 60 | window.location.reload() 61 | } 62 | }) 63 | } 64 | {% endblock %} 65 | -------------------------------------------------------------------------------- /ui/pos/dialog/email-gift-card.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout/dialog.html' %} 2 | {% import 'macros.twig' as scat %} 3 | 4 | {% block title %} 5 | Email Gift Card 6 | {% endblock %} 7 | 8 | {% block body %} 9 | 46 | {% endblock %} 47 | 48 | {% block submit %} 49 |