Compare commits

...

420 Commits

Author SHA1 Message Date
Alexis Lahouze 1496dca1bc Add materialize in webpack configuration. 2017-10-28 22:28:25 +02:00
Alexis Lahouze 4982931457 Add jquery and materialize-css in tsconfig. 2017-10-28 22:28:25 +02:00
Alexis Lahouze 13d79ae567 Concat jquery and materialize in separated bundle. 2017-10-28 22:28:25 +02:00
Alexis Lahouze a20e481f08 Add materialize in webpack provide plugin. 2017-10-28 22:26:02 +02:00
Alexis Lahouze a283651eae Try to use materialize for login popup. 2017-10-28 22:26:02 +02:00
Alexis Lahouze 3216be85ef Remove requirejs. 2017-10-28 22:26:02 +02:00
Alexis Lahouze 5cab9fa5e4 Remove require. 2017-10-28 22:26:02 +02:00
Alexis Lahouze 116b61e509 Improve webpack configuration. 2017-10-28 22:26:02 +02:00
Alexis Lahouze 49482eab9f Remove unused old login.config module. 2017-10-28 22:26:02 +02:00
Alexis Lahouze 7021cff9d9 Fix JQuery provide for materialize. 2017-10-28 22:26:02 +02:00
Alexis Lahouze 926acd18ae Add TS linting. 2017-10-28 22:26:02 +02:00
Alexis Lahouze a650f52fe2 Fix add button style. 2017-10-28 22:24:26 +02:00
Alexis Lahouze fe2d539812 Use ng2-materialize in schedule module. 2017-10-28 22:24:26 +02:00
Alexis Lahouze fbcec48038 Use ng2-materialize in operations module. 2017-10-28 22:24:26 +02:00
Alexis Lahouze 4a1c246e00 Use ng2-materialize in app component. 2017-10-28 22:24:26 +02:00
Alexis Lahouze 8fcbef9b5a Use ng2-materialize in account module. 2017-10-28 22:24:26 +02:00
Alexis Lahouze b5abe44bd9 Fix operation row action buttons. 2017-10-28 22:24:26 +02:00
Alexis Lahouze e592efd25d Fix operation table styles. 2017-10-28 22:24:26 +02:00
Alexis Lahouze 6a4523aceb Remove button groups. 2017-10-28 22:24:26 +02:00
Alexis Lahouze 9b6ae093a4 Fix text and table row styles. 2017-10-28 22:24:26 +02:00
Alexis Lahouze 34ade59d4c Fix button styles. 2017-10-28 22:24:26 +02:00
Alexis Lahouze 7a0b7837da Fix table row style. 2017-10-28 22:24:26 +02:00
Alexis Lahouze fdef99085a Fix table and add button styles. 2017-10-28 22:24:26 +02:00
Alexis Lahouze bf82f2a4ea Update index to remove useless style. 2017-10-28 22:24:26 +02:00
Alexis Lahouze acdbe3ab48 Update account list component template to use Materialize. 2017-10-28 22:24:26 +02:00
Alexis Lahouze 674411a1e2 Update operation list component template to use Materialize. 2017-10-28 22:24:26 +02:00
Alexis Lahouze 5e7b2167c1 Update app component template to use Materialize. 2017-10-28 22:24:26 +02:00
Alexis Lahouze d3295d8a96 Bootstrap Materialize module. 2017-10-28 22:24:26 +02:00
Alexis Lahouze 7e378fd86a Fix SASS loading. 2017-10-28 22:22:28 +02:00
Alexis Lahouze c620a92250 Upgrades. 2017-10-28 21:51:39 +02:00
Alexis Lahouze 427272514b Fix webpack.config.js. 2017-10-09 08:48:55 +02:00
Alexis Lahouze 936c5caa5b Upgrades. 2017-10-09 08:42:24 +02:00
Alexis Lahouze 648b18eb6a Add ajs-loader dependency. 2017-10-09 08:28:23 +02:00
Alexis Lahouze 8f87c43f8a Improve index.ejs. 2017-10-09 08:27:01 +02:00
Alexis Lahouze 3053444ca2 Remove uneeded require. 2017-10-09 08:26:09 +02:00
Alexis Lahouze c53f917374 Improve webpack configuration. 2017-10-09 08:22:47 +02:00
Alexis Lahouze 1a5f886810 Upgrades 2017-10-04 20:45:55 +02:00
Alexis Lahouze 4a2d0e2619 Upgrades 2017-10-04 20:16:32 +02:00
Alexis Lahouze ba50bf815f Upgrades. 2017-08-27 17:29:28 +02:00
Alexis Lahouze 9805e8b2e6 Merge branch 'feature/angular2' into develop 2017-08-27 17:25:47 +02:00
Alexis Lahouze a485baf7d2 Switch styles to SASS. 2017-08-13 15:14:12 +02:00
Alexis Lahouze 332495411e Upgrade to bootstrap v4-beta. 2017-08-13 15:13:26 +02:00
Alexis Lahouze 92a4ca97eb Upgrade dependencies. 2017-08-13 14:22:18 +02:00
Alexis Lahouze bcd5363ff6 Fix password required message. 2017-08-13 14:19:01 +02:00
Alexis Lahouze c3e785122b Use copy instead of new instance. 2017-08-13 14:17:05 +02:00
Alexis Lahouze 13670f0317 Migrate to reactive forms in operation module. 2017-08-13 12:28:10 +02:00
Alexis Lahouze 0600c1b653 Switch to reactive forms in schedule module. 2017-08-11 23:15:07 +02:00
Alexis Lahouze 1f896e1a40 Cleanup modules. 2017-08-11 22:32:09 +02:00
Alexis Lahouze fd0df50d14 Fix schedule edit modal size. 2017-08-11 22:24:09 +02:00
Alexis Lahouze 8973c1fe9a Fix operation edit modal size. 2017-08-11 22:22:23 +02:00
Alexis Lahouze 012e5a16fa Fix account edit dialog size. 2017-08-11 22:21:36 +02:00
Alexis Lahouze 49716421a1 Use reactive forms for login modal. 2017-08-09 00:12:17 +02:00
Alexis Lahouze 5f413f0cad Simplify classes. 2017-08-09 00:11:50 +02:00
Alexis Lahouze 985174eec9 Remove unused dependency. 2017-08-09 00:11:35 +02:00
Alexis Lahouze 0e69216920 Rename LoginForm class. 2017-08-09 00:11:21 +02:00
Alexis Lahouze 5a81547108 Fix modal size. 2017-08-08 23:37:45 +02:00
Alexis Lahouze e23adb4316 Format. 2017-08-08 23:37:21 +02:00
Alexis Lahouze 061cda89d0 Fix submit on enter. 2017-08-08 23:37:09 +02:00
Alexis Lahouze e68db9f54a Improve Account form and edit modal. 2017-08-04 14:52:37 +02:00
Alexis Lahouze 58f1abce21 Implement login mechanism. 2017-08-04 08:32:49 +02:00
Alexis Lahouze 16bbc67850 Use Login module in app. 2017-08-01 23:53:57 +02:00
Alexis Lahouze cdbaf58253 Add Login class. 2017-08-01 23:53:27 +02:00
Alexis Lahouze beac7c6eaa Add Login module. 2017-08-01 23:52:34 +02:00
Alexis Lahouze ef4baeed88 Add auth interceptor bootstrap. 2017-08-01 23:51:52 +02:00
Alexis Lahouze e2a4b0b7ec Upgrade Login service. 2017-08-01 23:50:28 +02:00
Alexis Lahouze 57256e9ba7 Cleanup old dependencies. 2017-08-01 23:47:51 +02:00
Alexis Lahouze 1310807939 Remove unused method. 2017-08-01 23:25:29 +02:00
Alexis Lahouze 3e6cadffbd Indent. 2017-08-01 23:22:44 +02:00
Alexis Lahouze a1d71b92d5 Module order in App module. 2017-08-01 23:14:56 +02:00
Alexis Lahouze 17754f9a88 Cleanup Angular1 modules. 2017-08-01 23:12:51 +02:00
Alexis Lahouze 5726d8bf2e Upgrade routing to Angular2. 2017-08-01 23:09:21 +02:00
Alexis Lahouze 2ae8a9cfad Remove Restangular. 2017-07-31 13:17:54 +02:00
Alexis Lahouze 0b0c771558 Set url function to private. 2017-07-31 13:16:18 +02:00
Alexis Lahouze dc30f38662 Migrate to HttpClient in Schedule module. 2017-07-31 13:15:45 +02:00
Alexis Lahouze d71656412a Remove unused services. 2017-07-31 13:00:44 +02:00
Alexis Lahouze 2d8d39442d Migrate to HttpClient in Account module. 2017-07-31 13:00:17 +02:00
Alexis Lahouze c9e1483206 Migrate to HttpClient in Operation module. 2017-07-31 12:50:59 +02:00
Alexis Lahouze 849a7ae95c Style. 2017-07-30 16:51:41 +02:00
Alexis Lahouze 38208f6420 Add missing module. 2017-07-30 16:51:20 +02:00
Alexis Lahouze 474b7de02d Cleanup schedule module. 2017-07-30 16:49:55 +02:00
Alexis Lahouze 19fdb785ba Remove unused template. 2017-07-30 16:49:42 +02:00
Alexis Lahouze 3559d4acc0 Remove windowClass in modal open. 2017-07-30 16:44:58 +02:00
Alexis Lahouze efb0f70f31 Modal related stuff. 2017-07-30 16:42:02 +02:00
Alexis Lahouze 28460e10ff Remove return statement. 2017-07-30 16:39:11 +02:00
Alexis Lahouze 5e964dd8e8 Simplify save function. 2017-07-30 16:38:28 +02:00
Alexis Lahouze 25fcc34b68 Merge add ans modify functions in Operation List Component. 2017-07-30 16:36:54 +02:00
Alexis Lahouze 73485ac1af Indent. 2017-07-30 16:35:15 +02:00
Alexis Lahouze 08a35f0d2c Style. 2017-07-30 16:33:38 +02:00
Alexis Lahouze 745eee03c5 Remove unused dependency. 2017-07-30 16:32:39 +02:00
Alexis Lahouze d977286637 Remove unused service. 2017-07-30 16:29:15 +02:00
Alexis Lahouze 5b7a4f8aa5 Cleanup dependencies. 2017-07-30 16:27:17 +02:00
Alexis Lahouze 6bc53a0cd2 Remove unused dependency. 2017-07-30 16:25:10 +02:00
Alexis Lahouze 03f069fc74 Fix delete function in services. 2017-07-30 16:22:44 +02:00
Alexis Lahouze 1c18d93d13 Use Operation Edit form to edit operation in Operation Row component. 2017-07-30 16:15:22 +02:00
Alexis Lahouze 6da1861139 Use Operation Edit form to add new operation in Operation List component. 2017-07-30 16:11:47 +02:00
Alexis Lahouze f6bcdcfc2b Fix form. 2017-07-30 16:10:45 +02:00
Alexis Lahouze 7b368df9b9 Cleanup component. 2017-07-30 16:10:28 +02:00
Alexis Lahouze 0782675d19 Fix form template. 2017-07-30 16:04:28 +02:00
Alexis Lahouze 7966301651 Add missing modules. 2017-07-30 16:04:05 +02:00
Alexis Lahouze fb477429cc Add Operation Edit Modal component. 2017-07-30 15:55:24 +02:00
Alexis Lahouze 3f6d60bb50 Add Operation Form component. 2017-07-30 15:51:52 +02:00
Alexis Lahouze 10d8959178 Remove unused factory. 2017-07-30 15:40:20 +02:00
Alexis Lahouze dcd79d085a Remove unused template. 2017-07-30 15:38:38 +02:00
Alexis Lahouze 70eb1febff Fix Operation confirmation. 2017-07-30 15:36:57 +02:00
Alexis Lahouze 896daa70c5 Cleanup. 2017-07-30 15:36:12 +02:00
Alexis Lahouze a5769dad83 Fix currency. 2017-07-30 15:35:46 +02:00
Alexis Lahouze bc4a69b64c Use Operation Delete Modal in List Component. 2017-07-30 15:35:26 +02:00
Alexis Lahouze 162d98add7 Add id in operation list. 2017-07-30 15:34:12 +02:00
Alexis Lahouze b5e4b1cd08 Add Operation Delete Modal component. 2017-07-30 15:22:25 +02:00
Alexis Lahouze 072efe7fc3 Rename Operation controller to Operation List component. 2017-07-30 15:16:44 +02:00
Alexis Lahouze 8d63b30a32 Remove unused template. 2017-07-30 15:14:02 +02:00
Alexis Lahouze 129c9f9ee3 Upgrade Operation controller. 2017-07-30 15:13:36 +02:00
Alexis Lahouze b7c2d94c62 Fix Operation Row component. 2017-07-30 15:12:57 +02:00
Alexis Lahouze 558988a57a Add Operation Row component. 2017-07-30 13:22:08 +02:00
Alexis Lahouze 2fdce22698 Indent. 2017-07-30 13:15:16 +02:00
Alexis Lahouze c3d6fa97cf Add missing fields in operation. 2017-07-30 13:15:00 +02:00
Alexis Lahouze 3eba873eb5 Fix callbacks. 2017-07-30 09:22:14 +02:00
Alexis Lahouze ecf38725e3 Readd missing dependency. 2017-07-29 23:14:00 +02:00
Alexis Lahouze 90ecb2bd57 Upgrades. 2017-07-29 23:13:07 +02:00
Alexis Lahouze f7ea8a4621 Prepare needed injection to upgrade Operation Component to Angular2. 2017-07-29 23:06:10 +02:00
Alexis Lahouze 45af7791ff Remove unused injection. 2017-07-29 23:05:38 +02:00
Alexis Lahouze f484833380 Remove unused injection. 2017-07-29 23:04:09 +02:00
Alexis Lahouze f374815371 Add typing. 2017-07-29 22:58:34 +02:00
Alexis Lahouze bb21dd700e Use Account Id service instead of $stateParam. 2017-07-29 22:53:41 +02:00
Alexis Lahouze 0a5a7e72e3 Add account_id in Operation. 2017-07-29 22:27:25 +02:00
Alexis Lahouze 75dc2afc80 Fix Operation query parameters. 2017-07-29 22:26:57 +02:00
Alexis Lahouze 8348f5bb8f Use Operation Service in Operation controller. 2017-07-29 22:26:38 +02:00
Alexis Lahouze 4c921bfaaa Add minDate and maxDate parameters in Operation query. 2017-07-29 17:59:34 +02:00
Alexis Lahouze 595fe60fc4 Add Operation Service. 2017-07-29 17:51:44 +02:00
Alexis Lahouze 1d35438444 Rename Category Chart component file. 2017-07-29 17:24:25 +02:00
Alexis Lahouze f58afb37bd Cleanup unused dependency. 2017-07-29 17:22:25 +02:00
Alexis Lahouze 20437ba548 Indent and cleanup. 2017-07-29 17:21:45 +02:00
Alexis Lahouze 02f447d63c Upgrade Category chart component to Angular2. 2017-07-29 17:18:36 +02:00
Alexis Lahouze 1bd59cbbf8 Fix date format in category service. 2017-07-29 17:07:43 +02:00
Alexis Lahouze 89fc42c47a Fix date range in category service. 2017-07-29 16:57:05 +02:00
Alexis Lahouze 927459b6c5 Remove unused ngResource. 2017-07-29 16:36:16 +02:00
Alexis Lahouze efcf07565b Use categoryService instead of old Categories factory. 2017-07-29 16:34:24 +02:00
Alexis Lahouze a6a7c1cd77 Add category service in Angular2. 2017-07-29 16:33:24 +02:00
Alexis Lahouze 2812891b23 Inject account in category chart component instead of using accountIdService. 2017-07-29 16:32:07 +02:00
Alexis Lahouze fa147e74fc Use account directly to load daily balances. 2017-07-29 16:28:59 +02:00
Alexis Lahouze 7281d1f11d Use accountIdService to retrieve current Account Id. 2017-07-27 14:31:15 +02:00
Alexis Lahouze eb0d898c19 Fix imports. 2017-07-27 14:30:12 +02:00
Alexis Lahouze 13ffa1dc98 Cleanup. 2017-07-27 14:28:28 +02:00
Alexis Lahouze 27e3307a8e Fix callback. 2017-07-27 14:21:35 +02:00
Alexis Lahouze 2619238996 Add missing component in operation module. 2017-07-27 14:13:13 +02:00
Alexis Lahouze e24d5defb7 Add missinf component in operation module. 2017-07-27 14:12:54 +02:00
Alexis Lahouze 99d0eb6d1c Cleanup. 2017-07-27 14:09:09 +02:00
Alexis Lahouze 6ca4c29e38 Cleanup, indent. 2017-07-27 14:08:11 +02:00
Alexis Lahouze 003b4de822 Upgrade Balance Chart component to Angular2. 2017-07-27 14:03:39 +02:00
Alexis Lahouze 863160881f Initialize Operation module. 2017-07-27 13:57:40 +02:00
Alexis Lahouze ac0aa056cf Fix DailyBalanceService query return type. 2017-07-27 13:55:30 +02:00
Alexis Lahouze c0bc7b82b3 Remove unused Balances factory. 2017-07-27 01:07:27 +02:00
Alexis Lahouze 64c53441cb Use Daily Balance service in Balance Chart component. 2017-07-27 01:02:02 +02:00
Alexis Lahouze d8adfd91c5 Add Daily Balance service. 2017-07-27 01:00:33 +02:00
Alexis Lahouze c0b236ff6c Use accountIdService to retrieve current Account Id. 2017-07-27 00:41:07 +02:00
Alexis Lahouze 2d50ad8a0b Remove uneeded dependency. 2017-07-27 00:35:11 +02:00
Alexis Lahouze e73d105420 Move accountId retrieving into account module. 2017-07-27 00:32:38 +02:00
Alexis Lahouze fab4880389 Add format in date field labels. 2017-07-26 23:12:03 +02:00
Alexis Lahouze b7c44a39da Style. 2017-07-26 23:10:21 +02:00
Alexis Lahouze 3363cf682a Fix Schedule field types. 2017-07-26 23:09:55 +02:00
Alexis Lahouze c546cbf833 Add text-mask on date fields. 2017-07-26 23:09:16 +02:00
Alexis Lahouze 931ef38f29 Fix button sizes and styles. 2017-07-25 23:33:29 +02:00
Alexis Lahouze 0e037e664f Begin migration to bootstrap v4. 2017-07-25 22:12:47 +02:00
Alexis Lahouze b3d274eabf Replace angular-ui-notifications by toastr. 2017-07-25 22:08:18 +02:00
Alexis Lahouze cd7aba50e5 Cleanup unused title definition. 2017-07-25 17:26:26 +02:00
Alexis Lahouze fc14efbda1 Fix inverted create and update calls. 2017-07-25 17:26:09 +02:00
Alexis Lahouze 2f64fa1018 Use Schedule Edit Modal component in Schedule List component. 2017-07-25 17:25:50 +02:00
Alexis Lahouze d1344d57b8 Use Schedule Edit Modal component in Schedule Row component. 2017-07-25 17:21:01 +02:00
Alexis Lahouze 57aa737465 Add Schedule Edit Modal component. 2017-07-25 17:18:04 +02:00
Alexis Lahouze 3c0d3c1a39 Add Schedule Form component. 2017-07-25 17:17:22 +02:00
Alexis Lahouze 3dd347ee8c Remove unused template. 2017-07-25 16:59:48 +02:00
Alexis Lahouze 2cf432ed2c Rename component. 2017-07-25 16:56:57 +02:00
Alexis Lahouze d83a164cc6 Remove dependency. 2017-07-25 16:14:48 +02:00
Alexis Lahouze 53a29062ea Use Schedule Delete Modal Component in scheduleRow Component. 2017-07-25 15:59:06 +02:00
Alexis Lahouze 46f5f72bd4 Add Schedule Delete Modal component. 2017-07-25 15:45:25 +02:00
Alexis Lahouze fd4da93c20 Add missing loglevel dependency. 2017-07-25 12:09:51 +02:00
Alexis Lahouze e8247e30ab Upgrade Schedule component to Angular4. 2017-07-25 12:04:45 +02:00
Alexis Lahouze 2166def0f1 Inject $modal and accountIdService in Angular Component. 2017-07-25 11:48:48 +02:00
Alexis Lahouze 3c148e5297 Remove unused dependency. 2017-07-25 10:42:29 +02:00
Alexis Lahouze 434020f7ad Add Restangular module and ScheduleRowComponent. 2017-07-25 10:15:00 +02:00
Alexis Lahouze 3c3741c33f Use accountIdService to retrieve current Account Id in Schedule Component. 2017-07-25 10:06:41 +02:00
Alexis Lahouze 7ea652e23e Add Shecdule Row Component. 2017-07-25 08:55:56 +02:00
Alexis Lahouze fa9402e25e Add accountIdService to be able to correctly retrieve accountId in UiRouter transition. 2017-07-25 08:53:08 +02:00
Alexis Lahouze ad16b5a391 Upgrades. 2017-07-24 21:22:54 +02:00
Alexis Lahouze 17a363d69d Move up ng1 specific configuration in ng1 app module. 2017-07-24 20:33:30 +02:00
Alexis Lahouze 8fb5f2ff7a Separate hybrid application bootstrap. 2017-07-24 19:55:20 +02:00
Alexis Lahouze 6873b32005 Set name for Angular1 app module. 2017-07-24 19:51:30 +02:00
Alexis Lahouze a7cee2891c Move states in separate files. 2017-07-24 18:48:57 +02:00
Alexis Lahouze 7b7f72bf1e Inject $modal from router. 2017-07-23 07:58:57 +02:00
Alexis Lahouze 4f090a22df Format. 2017-07-23 07:53:26 +02:00
Alexis Lahouze 1f5d4980e5 Cleanup module. 2017-07-23 07:44:31 +02:00
Alexis Lahouze 40bc4bf1e8 Rename controller to component. 2017-07-23 07:44:03 +02:00
Alexis Lahouze c4c10f9ab7 Use component in scheduler route, inject accountId into. 2017-07-23 00:12:44 +02:00
Alexis Lahouze 3326dac51a Fix creation and update. 2017-07-22 14:29:29 +02:00
Alexis Lahouze e927736b45 Remove unused factory. 2017-07-22 11:16:20 +02:00
Alexis Lahouze d52d382653 Replace schedule factory by schedule service. 2017-07-22 11:16:04 +02:00
Alexis Lahouze 42c0fe6c9b Add schedule module. 2017-07-22 11:14:52 +02:00
Alexis Lahouze c257069644 Add schedule service. 2017-07-22 11:11:39 +02:00
Alexis Lahouze c5f3a53347 Add Schedule model. 2017-07-22 10:39:19 +02:00
Alexis Lahouze d96a66ee3e Remove unneeded . 2017-07-22 09:46:06 +02:00
Alexis Lahouze d5e00b8fe3 Remove eslint comments. 2017-07-22 09:18:22 +02:00
Alexis Lahouze 801d2ae380 Migrate to toastr. 2017-07-22 09:17:50 +02:00
Alexis Lahouze 68df4e5ce2 Prepare scheduler module for angular2 upgrade. 2017-07-22 09:16:16 +02:00
Alexis Lahouze aa07ffb125 Remove unused factory. 2017-07-22 08:44:54 +02:00
Alexis Lahouze adec1f102e Rename dependencies. 2017-07-22 08:42:17 +02:00
Alexis Lahouze f9c2e7f4bc Cleanup imports. 2017-07-21 23:11:04 +02:00
Alexis Lahouze fc0c08d58e Fix currency. 2017-07-21 23:07:20 +02:00
Alexis Lahouze 8626c78708 Transform controller function to class. 2017-07-21 23:06:59 +02:00
Alexis Lahouze a398752c0c Fix export and import. 2017-07-21 23:06:10 +02:00
Alexis Lahouze cc5b0b1ee1 Fix condition. 2017-07-21 22:42:16 +02:00
Alexis Lahouze 220426e6f8 Separate account row from account list. 2017-07-21 21:50:19 +02:00
Alexis Lahouze bd484a994e Fix indent. 2017-07-21 00:52:47 +02:00
Alexis Lahouze f780be6b63 Remove unused template. 2017-07-21 00:51:31 +02:00
Alexis Lahouze fc75945a76 Fix indent. 2017-07-21 00:45:04 +02:00
Alexis Lahouze f8fa34f269 Dissociate new account modal from editing account on AccountListComponent. 2017-07-21 00:41:04 +02:00
Alexis Lahouze 79d55bfc44 Upgrade AccountListComponent to Angular. 2017-07-21 00:39:15 +02:00
Alexis Lahouze 88e0599cd7 Remove unneeded script include. 2017-07-21 00:35:45 +02:00
Alexis Lahouze 1c93f528f6 Cleanup licence. 2017-07-21 00:33:55 +02:00
Alexis Lahouze d522aaad45 Fix promise error handling. 2017-07-21 00:33:27 +02:00
Alexis Lahouze d91089238d Change import order. 2017-07-21 00:32:51 +02:00
Alexis Lahouze 8b0d3decd4 Change scope of attribute. 2017-07-21 00:32:39 +02:00
Alexis Lahouze 598ddc6a85 Remove unused dependency. 2017-07-21 00:30:00 +02:00
Alexis Lahouze e28500c557 Use ngx-toastr for notifications in account module. 2017-07-21 00:29:03 +02:00
Alexis Lahouze b95f36f09c Change field parent bind. 2017-07-21 00:06:07 +02:00
Alexis Lahouze 4064848242 Change account form validation for ahtorized overdraft. 2017-07-20 23:34:07 +02:00
Alexis Lahouze 6ff893a08d Rename AccountComponent to AccountListComponent. 2017-07-20 23:00:52 +02:00
Alexis Lahouze 4e9915aab0 Cleanup. 2017-07-20 22:54:57 +02:00
Alexis Lahouze 5100d0fd0c Cleanup. 2017-07-20 22:53:22 +02:00
Alexis Lahouze 60aa6310bb Separate account form from modal. 2017-07-20 22:52:33 +02:00
Alexis Lahouze adfd61fac9 Add missing dependencies for NgbModal. 2017-07-20 22:31:58 +02:00
Alexis Lahouze 4057705e22 Use ng-bootstrap Modal. 2017-07-20 10:32:05 +02:00
Alexis Lahouze 3e6b1ecccc Use new Logger in account module. 2017-07-16 22:25:50 +02:00
Alexis Lahouze de945cd16c Create service for AccountBalances and use it. 2017-07-16 17:03:50 +02:00
Alexis Lahouze 330ed6b926 Transform account state to component. 2017-07-16 14:23:23 +02:00
Alexis Lahouze 83d7e61875 Switch to uirouter. 2017-07-16 13:56:49 +02:00
Alexis Lahouze 9910c0e0d3 Externalize log level in configuration file. 2017-07-16 10:51:54 +02:00
Alexis Lahouze 877f77babd Configure Logger at app level. 2017-07-16 10:49:02 +02:00
Alexis Lahouze 95fa835496 Remove newline. 2017-07-16 10:45:27 +02:00
Alexis Lahouze 6244b817d8 Use ngx-restangular to handle account API. 2017-07-16 10:44:25 +02:00
Alexis Lahouze 8b62380c52 Fix indent. 2017-07-16 10:30:53 +02:00
Alexis Lahouze 270406ff58 Remove unused AccountFactory. 2017-07-15 08:19:30 +02:00
Alexis Lahouze 716dd94943 Fix resource configuration for trailing slashes. 2017-07-15 08:17:22 +02:00
Alexis Lahouze cd2e20c744 Import order. 2017-07-14 21:41:27 +02:00
Alexis Lahouze faa2abfca3 Rename account controller to component. 2017-07-14 21:38:29 +02:00
Alexis Lahouze 839002cc47 Upgrade extract text webpack plugin. 2017-07-14 21:33:37 +02:00
Alexis Lahouze 4861f5ce49 Fix account change handling in balance chart component. 2017-07-14 10:33:20 +02:00
Alexis Lahouze 5960e9ee77 Rename item to account. 2017-07-14 10:25:33 +02:00
Alexis Lahouze c6a406272f Use account service. 2017-07-14 10:24:46 +02:00
Alexis Lahouze 79ecb1630b Account account module in app. 2017-07-14 10:19:39 +02:00
Alexis Lahouze c4baf94d3a Move default value in account constructor. 2017-07-14 10:15:07 +02:00
Alexis Lahouze c6761e1379 Fix URL in account service. 2017-07-14 10:05:48 +02:00
Alexis Lahouze 3fb442ab5d Fix URL in account service. 2017-07-14 10:05:09 +02:00
Alexis Lahouze df4d12cfb8 Catch errors in account service. 2017-07-14 10:04:55 +02:00
Alexis Lahouze 1e94109910 Typescript conversion. 2017-07-13 21:37:47 +02:00
Alexis Lahouze bda2c90e3d Change account controller into class. 2017-07-13 17:14:20 +02:00
Alexis Lahouze 995af2fbd3 Add missing dependencies. 2017-07-13 16:58:31 +02:00
Alexis Lahouze 65a322466a Add account module. 2017-07-13 16:57:02 +02:00
Alexis Lahouze 48caa14cd8 Add account service. 2017-07-13 16:55:02 +02:00
Alexis Lahouze f55efca9fd Add account models. 2017-07-13 16:54:09 +02:00
Alexis Lahouze f3c3ddfebf Bootstrap Angular app. 2017-07-11 18:51:01 +02:00
Alexis Lahouze 3ff0015690 Fix module resolution. 2017-07-11 08:49:39 +02:00
Alexis Lahouze 2ae971a7ad Typescript conversion. 2017-07-10 15:51:58 +02:00
Alexis Lahouze d4eaa1454c Separate Account balances in another factory. 2017-07-10 08:17:11 +02:00
Alexis Lahouze 4abe5092ec Upgrades. 2017-07-10 00:55:25 +02:00
Alexis Lahouze cae86d3014 Separate schedule module into different files. 2017-07-08 12:20:17 +02:00
Alexis Lahouze e5857bc68e Use angular strap modal instead of angular-ui-bootstrap one. 2017-07-08 12:12:20 +02:00
Alexis Lahouze 319c0adc16 Remove angular-ui-bootstrap dependency. 2017-07-08 09:39:18 +02:00
Alexis Lahouze 28afd8f563 Remove unneeded $q. 2017-07-08 09:34:55 +02:00
Alexis Lahouze 30549dd6d8 Use angular strap modal instead of angular-ui-bootstrap one. 2017-07-08 09:33:52 +02:00
Alexis Lahouze 8194978bcc Add vim tagline. 2017-07-08 09:20:10 +02:00
Alexis Lahouze 1ff7c98f93 Move needed libraries to the right places. 2017-07-08 09:19:35 +02:00
Alexis Lahouze 36e25fc1b7 Separate account module into different files. 2017-07-08 09:19:02 +02:00
Alexis Lahouze d4400b788d Use angular strap modal instead of angular-ui-bootstrap one. 2017-07-08 09:14:19 +02:00
Alexis Lahouze 0c85266ab2 Separate login module into different files. 2017-07-08 00:51:59 +02:00
Alexis Lahouze 292486f8fd Add event release on . 2017-07-08 00:18:16 +02:00
Alexis Lahouze 0268280a73 Rename factory to service. 2017-07-08 00:17:37 +02:00
Alexis Lahouze 9a7d3938aa Use anonymous function as interceptor. 2017-07-08 00:17:15 +02:00
Alexis Lahouze d6eb6781c6 Move template imports in controller. 2017-07-07 23:12:48 +02:00
Alexis Lahouze 9386b4a4b9 Import order. 2017-07-07 23:11:27 +02:00
Alexis Lahouze d4faff7a6c Separate operations module into different files. 2017-07-07 22:52:27 +02:00
Alexis Lahouze 06ca00e627 Move templates in routing. Module import order. 2017-07-07 22:36:04 +02:00
Alexis Lahouze fbeb3fd362 Separate app.config in another file. 2017-07-07 22:31:49 +02:00
Alexis Lahouze 2fb98fdeed Force AngularJS to use jQuery. 2017-07-07 01:16:49 +02:00
Alexis Lahouze 6556227f88 Remove usage of . 2017-07-07 01:16:33 +02:00
Alexis Lahouze bebe2aa874 Use modal from Angular-Strap instead of the one in angular-ui-bootstrap. 2017-07-07 01:14:18 +02:00
Alexis Lahouze 0833536d5e Fix dependencies. 2017-07-06 10:39:14 +02:00
Alexis Lahouze a7f37e88a9 Exports module name instead of module itself. 2017-07-06 10:35:15 +02:00
Alexis Lahouze d7de013954 Fix category loading. 2017-07-05 23:19:24 +02:00
Alexis Lahouze facc1c5a0d Add account injection. 2017-07-05 21:58:17 +02:00
Alexis Lahouze 0340f333b6 Upgrade dev dependencies. 2017-07-05 21:57:49 +02:00
Alexis Lahouze 3b925d4b37 Freeze webpack-dev-server version to avoid error on run. 2017-07-05 21:57:35 +02:00
Alexis Lahouze c14d88f421 Fix indent. 2017-07-05 21:48:20 +02:00
Alexis Lahouze c558994c6d Change chart height. 2017-07-05 21:36:45 +02:00
Alexis Lahouze 2a2738ddeb Initialize account from parent. 2017-07-05 21:36:23 +02:00
Alexis Lahouze 7ace852f43 Fix date timezones to avoid operation saved on the day before the one entered. 2017-07-05 09:35:09 +02:00
Alexis Lahouze 4fed3c9320 Remove unneeded ng-click from submit buttons. 2017-07-04 21:57:54 +02:00
Alexis Lahouze 4fdbb40e92 Fix ng-submit for login form. 2017-07-04 21:57:30 +02:00
Alexis Lahouze c06dea22f6 Style. 2017-07-04 21:54:31 +02:00
Alexis Lahouze 0fa2f06a0e Fix account form validation and disable submit button is form is invalid. 2017-07-04 21:54:21 +02:00
Alexis Lahouze fe2355a405 Fix account form submit on enter. 2017-07-04 21:53:32 +02:00
Alexis Lahouze a3e9ba02ac Fix authorized overdraft input field. 2017-07-04 21:50:34 +02:00
Alexis Lahouze e90f4d21e8 Fix login form submit on enter. 2017-07-04 21:20:13 +02:00
Alexis Lahouze 4218b741db Add red line for account authorized overdraft. 2017-07-04 21:13:08 +02:00
Alexis Lahouze 2e6bc006f5 Add regions to separate past and future operations. 2017-07-04 20:37:03 +02:00
Alexis Lahouze 003ad498db Cleanup . 2017-07-04 20:36:18 +02:00
Alexis Lahouze 6f477b6d22 Unload chart data before loading new one. 2017-07-04 20:35:53 +02:00
Alexis Lahouze 0f547a532c Rename row 'x' to 'date', swap expenses and revenues. 2017-07-04 20:35:18 +02:00
Alexis Lahouze ab14ead2dc Cleanup. 2017-07-02 00:13:40 +02:00
Alexis Lahouze 7004b6cf44 Reimplement category chart. 2017-07-02 00:13:04 +02:00
Alexis Lahouze 0cd5dab07c Improve bindings between components. 2017-07-02 00:11:02 +02:00
Alexis Lahouze 08b6643eda Use c3.js instead of chart.js. 2017-07-01 14:50:17 +02:00
Alexis Lahouze ca64dcd4e2 Use angular-chart.js to draw balance chart. 2017-06-18 00:46:27 +02:00
Alexis Lahouze 86c32772c0 Cleanup unused services. 2017-06-18 00:45:25 +02:00
Alexis Lahouze 1bfd7693dd Cleanup unused events. 2017-06-18 00:44:14 +02:00
Alexis Lahouze 66a19034dd Replace load arguments by controller variables, in order to link them to input controls later. 2017-06-18 00:43:28 +02:00
Alexis Lahouze a32b344b2c Remove last references to xeditable. 2017-06-17 00:27:24 +02:00
Alexis Lahouze 6fc89eeb9a Remove unused dependencies. 2017-06-17 00:25:21 +02:00
Alexis Lahouze ddb0b08ef2 Use angular-ui-bootstrap instead of ngBootbox, remove usage of xeditable. 2017-06-17 00:25:01 +02:00
Alexis Lahouze 13320602bf Rename solds to balances. 2017-06-16 23:28:34 +02:00
Alexis Lahouze 396210d924 Add missing dependency. 2017-06-16 23:22:27 +02:00
Alexis Lahouze 50c90b085c Use angular-ui-bootstrap instead of ngBootbox. 2017-06-16 23:22:07 +02:00
Alexis Lahouze f75e65a3de Fiw operation column name. 2017-06-16 22:27:17 +02:00
Alexis Lahouze e433aed773 Improve operation workflow. 2017-06-16 22:26:49 +02:00
Alexis Lahouze 4f3c196179 Use angular-ui-bootstrap for Operation deletion confirmation modal. 2017-06-15 17:21:17 +02:00
Alexis Lahouze 69d0e06b57 Remove unused function. 2017-06-15 08:47:15 +02:00
Alexis Lahouze 9acaa4033e Use angular-ui-bootstrap for Operation modal. 2017-06-15 08:46:53 +02:00
Alexis Lahouze 3537470cff Cleanup. 2017-06-14 22:21:25 +02:00
Alexis Lahouze c6586ad224 Remove unused import. 2017-06-14 22:20:28 +02:00
Alexis Lahouze e364b1e3ef Remove unused code. 2017-06-14 22:14:40 +02:00
Alexis Lahouze 13766be8cb Fix object field name. 2017-06-14 22:13:25 +02:00
Alexis Lahouze 17d41d7f99 Fix operation order. 2017-06-14 22:12:52 +02:00
Alexis Lahouze 7fbb9d7bcd Remove uninstalled formatter. 2017-06-14 22:12:01 +02:00
Alexis Lahouze 060f9a01b6 Remove unused rule. 2017-06-11 09:24:36 +02:00
Alexis Lahouze 20f50d533a Remove eslint formatter. 2017-06-11 09:24:23 +02:00
Alexis Lahouze 23d1189591 Ignore auth on login API call. 2017-06-11 09:18:19 +02:00
Alexis Lahouze e494967888 Remove data in login form. 2017-06-11 09:18:00 +02:00
Alexis Lahouze eba176c000 Add cancelLogin method on LoginService. 2017-06-11 09:17:42 +02:00
Alexis Lahouze 3d5f211824 Style. 2017-06-11 09:17:09 +02:00
Alexis Lahouze 5c4e77d6bb Remove modal config. 2017-06-11 09:17:02 +02:00
Alexis Lahouze 353ca4fef1 Rename module. 2017-06-11 09:16:44 +02:00
Alexis Lahouze feef3d825c Merge config functions. 2017-06-11 08:07:48 +02:00
Alexis Lahouze 86911c106d Improve login dialog. 2017-06-11 01:14:26 +02:00
Alexis Lahouze 3697ff9f21 Use angular-ui-bootstrap for login modal. 2017-06-11 00:48:03 +02:00
Alexis Lahouze c907c56c4a Cleanup. 2017-06-10 23:13:08 +02:00
Alexis Lahouze 3f90c33a3a Move up all sources in src directory. 2017-06-10 23:09:57 +02:00
Alexis Lahouze b5804f5d21 Remove unused style. 2017-06-10 22:55:06 +02:00
Alexis Lahouze cecfa6db4f Remove unused controller. 2017-06-10 22:54:13 +02:00
Alexis Lahouze f4d0988fdd Move login handling in a separate module. 2017-06-10 22:50:48 +02:00
Alexis Lahouze 5627746e98 Disable HTML linter. 2017-06-10 22:07:27 +02:00
Alexis Lahouze 9eed69363f Move templates, improve inclusion. 2017-06-10 22:06:36 +02:00
Alexis Lahouze 12414a9dc7 Move modules in subfolders. 2017-06-10 21:10:52 +02:00
Alexis Lahouze 00dac1dec7 Completely remove charts. 2017-06-10 21:01:39 +02:00
Alexis Lahouze 0965498145 Remove unneeded import. 2017-06-10 21:01:01 +02:00
Alexis Lahouze 65c9262257 Force load of some data. 2017-06-10 20:59:25 +02:00
Alexis Lahouze 23f414b2d5 Fix template URL. 2017-06-10 20:46:41 +02:00
Alexis Lahouze 3e287cb7f4 Fix element finding. 2017-06-10 20:36:36 +02:00
Alexis Lahouze 9fe38b2560 Rename accountant-ui dir to src. 2017-06-10 20:24:51 +02:00
Alexis Lahouze 3c4a67a952 Disable buggy graphs. 2017-06-10 18:19:13 +02:00
Alexis Lahouze 548c4ae23e Fix authentication process. 2017-06-10 18:19:00 +02:00
Alexis Lahouze c3daad9cd7 Fix angular route urls. 2017-06-10 18:08:31 +02:00
Alexis Lahouze d24da8d56b HTML style. 2017-06-10 18:04:46 +02:00
Alexis Lahouze de5e89b155 Add style dependencies. 2017-06-10 18:04:02 +02:00
Alexis Lahouze c8cfed2018 Fix template paths. 2017-06-10 18:03:08 +02:00
Alexis Lahouze a9c45119d6 Fix API URI. 2017-06-10 18:02:34 +02:00
Alexis Lahouze 5c25d5c79f Use webpack. 2017-06-10 18:02:19 +02:00
Alexis Lahouze 1a43135e55 Improve gitignore. 2017-06-10 17:59:53 +02:00
Alexis Lahouze 150aca8269 Add webpack bootstrap. 2017-06-10 15:48:20 +02:00
Alexis Lahouze c895d5cc4d Use ejs template for index file. 2017-06-10 15:35:10 +02:00
Alexis Lahouze 28229ea954 Use webpack. 2017-06-10 15:34:10 +02:00
Alexis Lahouze 91a77b776f Remove grunt and bower. 2017-06-10 14:40:00 +02:00
Alexis Lahouze 8b6c179c84 Move less file. 2016-10-15 22:03:34 +02:00
Alexis Lahouze 35b5a4d2a1 Fix name of less subtask. 2016-10-15 21:51:19 +02:00
Alexis Lahouze cc94ec1ca6 Less linting. 2016-10-15 21:50:02 +02:00
Alexis Lahouze 542bce0ab6 Add less plugin. 2016-10-15 21:40:20 +02:00
Alexis Lahouze 6eeaf76e96 Use less format. 2016-10-15 21:39:25 +02:00
Alexis Lahouze 07ea908c73 Add csslint for frontend linting. 2016-10-15 13:46:08 +02:00
Alexis Lahouze 189d76fc22 Fix typo. 2016-10-15 13:09:17 +02:00
Alexis Lahouze 3d7bafd7e1 Manage modelines. 2016-10-15 13:03:28 +02:00
Alexis Lahouze 47ed5a2b5c Improve grunt tasks dependencies. 2016-10-15 12:50:57 +02:00
Alexis Lahouze 4c0a37495f Fix dist path. 2016-10-15 11:56:02 +02:00
Alexis Lahouze 3435c50ca0 Remove old dependency. 2016-10-15 11:50:38 +02:00
Alexis Lahouze 49135f0873 Upgrade dev dependencies. 2016-10-15 11:48:47 +02:00
Alexis Lahouze 5e2ac3b833 Remove old dependencies. 2016-10-15 11:48:15 +02:00
Alexis Lahouze cfc0db2507 Style. 2016-10-15 11:46:17 +02:00
Alexis Lahouze a2996a5074 Update dependencies. 2016-10-15 11:46:03 +02:00
Alexis Lahouze 8cad18d5ea Review toolchain. 2016-10-15 11:45:39 +02:00
Alexis Lahouze 96a553c130 Remove unused jquery from eslint plugins. 2016-10-15 11:44:17 +02:00
Alexis Lahouze 0b713fad34 Add Angular eslint plugin. 2016-10-14 18:19:13 +02:00
Alexis Lahouze dbef4039bd Add jsonlint dependency. 2016-10-14 18:18:48 +02:00
Alexis Lahouze 727b2fc313 Fix eslint errors again. 2016-10-14 09:20:02 +02:00
Alexis Lahouze 43714c1b1d Remove unused $http in controllers. 2016-10-14 09:02:44 +02:00
Alexis Lahouze cfe4904084 Remove unecessary globals. 2016-10-14 09:01:21 +02:00
Alexis Lahouze 81434d7fde Fix eslint errors again. 2016-10-14 09:00:55 +02:00
Alexis Lahouze 97d23c5bcd Add vim modeline. 2016-10-14 08:52:24 +02:00
Alexis Lahouze e39d2813df Fix controller names in views. 2016-10-14 08:52:10 +02:00
Alexis Lahouze bff2d826dc Fix event handling and emiting. 2016-10-14 08:51:35 +02:00
Alexis Lahouze 8ebe15f22f Move API call in dedicated service. 2016-10-14 08:50:54 +02:00
Alexis Lahouze f12c89a9ee Fix angular style issues. 2016-10-14 08:21:43 +02:00
Alexis Lahouze 4aed242a48 Fix angular style issues. 2016-10-14 08:19:58 +02:00
Alexis Lahouze e91bf14298 Fix angular style issues. 2016-10-14 08:19:23 +02:00
Alexis Lahouze edac1ee6a9 Fix angular style issues. 2016-10-14 07:50:06 +02:00
Alexis Lahouze a872788def Fix angular style issues. 2016-10-12 23:32:54 +02:00
Alexis Lahouze 032dd9bdc6 Use YAML syntax instead of JSON. 2016-10-12 23:31:50 +02:00
Alexis Lahouze 5811541722 Invert conditions. 2016-10-12 19:59:09 +02:00
Alexis Lahouze 606cde59f8 Use eslint and be compliant. (mostly) 2016-10-12 19:57:19 +02:00
Alexis Lahouze d7ead2aa5c Style with jscs. 2016-10-09 20:33:59 +02:00
Alexis Lahouze 9f0258905d Style with jscs. 2016-10-09 20:22:21 +02:00
Alexis Lahouze 83b43dfff5 Add wiredep in jsdev task. 2016-10-09 20:20:21 +02:00
Alexis Lahouze e5721b3573 Style with jscs. 2016-10-09 20:17:11 +02:00
Alexis Lahouze 655d72d4ae Remove too much spaces. 2016-07-15 16:50:05 +02:00
Alexis Lahouze d0fca63dd7 Add missing Account parameter. 2016-07-15 16:46:27 +02:00
Alexis Lahouze a045fd77d7 Improve forms. 2016-07-15 16:40:39 +02:00
Alexis Lahouze f4df257548 Update .gitignore. 2016-06-19 22:18:28 +02:00
Alexis Lahouze f09ba377b5 Fix flask invokaction from grunt. 2016-06-17 10:54:02 +02:00
Alexis Lahouze f8a71af4d8 Remove old SQL stuff. 2016-06-17 10:18:24 +02:00
Alexis Lahouze 7aee85c08f Move remaining stuff in manage.py to application. 2016-06-17 10:17:44 +02:00
Alexis Lahouze 38da04a412 Move migrations in application. 2016-06-17 09:52:08 +02:00
Alexis Lahouze 1a9363b723 Update make template for revision making. 2016-06-17 09:51:23 +02:00
Alexis Lahouze 7d1d9bbcc8 Add missing license. 2016-05-24 08:56:32 +02:00
Alexis Lahouze 1660c7a635 Set operation edition in form dialog. 2016-05-24 08:53:15 +02:00
98 changed files with 3139 additions and 2631 deletions

View File

@ -1,3 +0,0 @@
{
"directory": "accountant-ui/bower_components"
}

147
.gitignore vendored
View File

@ -1,100 +1,19 @@
# Created by https://www.gitignore.io
### Vim ###
[._]*.s[a-w][a-z]
[._]s[a-w][a-z]
*.un~
Session.vim
.netrwhist
*~
.vimrc
.vimtags
### Python ###
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
# C extensions
*.so
# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*,cover
# Translations
*.mo
*.pot
# Django stuff:
*.log
# Sphinx documentation
docs/_build/
# PyBuilder
target/
### grunt ###
# Grunt usually compiles files inside this directory
dist/
# Grunt usually preprocesses files such as coffeescript, compass... inside the .tmp directory
.tmp/
### Bower ###
bower_components
.bower-cache
.bower-registry
.bower-tmp
# Created by https://www.gitignore.io/api/vim,node
### Node ###
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
@ -102,20 +21,64 @@ lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directory
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
node_modules
# Dependency directories
node_modules/
jspm_packages/
# Typescript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# Yarn Lock file
yarn.lock
# dotenv environment variables file
.env
### Local files ###
config.cfg
/flask_session
### Vim ###
# swap
[._]*.s[a-v][a-z]
[._]*.sw[a-p]
[._]s[a-v][a-z]
[._]sw[a-p]
# session
Session.vim
# temporary
.netrwhist
*~
# auto-generated tag files
tags
# End of https://www.gitignore.io/api/vim,node
/build

35
.jscsrc Normal file
View File

@ -0,0 +1,35 @@
{
"preset": "google",
"fileExtensions": [".js", "jscs"],
"requireSemicolons": true,
"requireParenthesesAroundIIFE": true,
"maximumLineLength": 120,
"validateLineBreaks": "LF",
"validateIndentation": 4,
"disallowTrailingComma": true,
"disallowUnusedParams": true,
"disallowSpacesInsideObjectBrackets": null,
"disallowImplicitTypeConversion": ["string"],
"safeContextKeyword": "_this",
"jsDoc": {
"checkAnnotations": "closurecompiler",
"checkParamNames": true,
"requireParamTypes": true,
"checkRedundantParams": true,
"checkReturnTypes": true,
"checkRedundantReturns": true,
"requireReturnTypes": true,
"checkTypes": "capitalizedNativeCase",
"checkRedundantAccess": true,
"requireNewlineAfterDescription": true
},
"excludeFiles": [
"test/data/**",
"patterns/*"
]
}

View File

@ -1,21 +0,0 @@
{
"bitwise": true,
"browser": true,
"curly": true,
"eqeqeq": true,
"esnext": true,
"latedef": true,
"noarg": true,
"node": true,
"strict": true,
"undef": true,
"unused": true,
"quotmark": "single",
"indent": 2,
"jquery": true,
"globals": {
"angular": false,
"moment": false,
"Highcharts": false
}
}

View File

@ -1,75 +0,0 @@
'use strict';
module.exports = function(grunt) {
require('load-grunt-tasks')(grunt);
require('time-grunt')(grunt);
// Options
var options = {
accountant: {
frontend: {
app: require('./bower.json'),
src: 'accountant-ui',
dist: 'accountant-ui_dist'
}
},
config: {
src: 'grunt-config/*.js'
},
pkg: grunt.file.readJSON('package.json'),
banner: '/*! <%= pkg.name %> <%= grunt.template.today("dd-mm-yyyy") %>\n'+
'* Copyright (c) <%= grunt.template.today("yyyy") %> <%= pkg.author %> */\n',
};
var configs = require('load-grunt-configs')(grunt, options);
grunt.initConfig(configs);
grunt.registerTask('dependencies', [
'shell:npm_install',
'shell:bower_install',
'shell:pip_install',
'wiredep:app',
]);
grunt.registerTask('pydev', [
'newer:flake8'
]);
grunt.registerTask('jsdev', [
'newer:jshint',
'newer:jscs'
]);
grunt.registerTask('htmldev', [
'newer:htmllint'
]);
grunt.registerTask('dev', [
'dependencies',
'pydev',
'jsdev',
'htmldev'
]);
grunt.registerTask('serve', [
'dev',
'bgShell:runserver',
'connect:livereload',
'watch'
]);
grunt.registerTask('dist', [
'wiredep',
'clean:dist',
'useminPrepare',
'copy:dist',
'copy:styles',
'cssmin:generated',
'concat:generated',
'ngAnnotate',
'uglify:generated',
'filerev',
'usemin'
]);
};

View File

@ -1,7 +0,0 @@
.italic {
font-style: italic
}
.stroke {
text-decoration: line-through
}

View File

@ -1,96 +0,0 @@
<!--
This file is part of Accountant.
Accountant is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Accountant is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with Accountant. If not, see <http://www.gnu.org/licenses/>.
-->
<!-- vim: set tw=80 ts=2 sw=2 sts=2: -->
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<!-- Title -->
<title>Accountant</title>
<!-- build:css(accountant-ui) css/vendor.css -->
<!-- bower:css -->
<link rel="stylesheet" href="bower_components/bootstrap/dist/css/bootstrap.css" />
<link rel="stylesheet" href="bower_components/bootstrap-additions/dist/bootstrap-additions.css" />
<link rel="stylesheet" href="bower_components/angular-xeditable/dist/css/xeditable.css" />
<link rel="stylesheet" href="bower_components/angular-ui-notification/dist/angular-ui-notification.css" />
<link rel="stylesheet" href="bower_components/font-awesome/css/font-awesome.css" />
<!-- endbower -->
<!-- endbuild -->
<!-- Custom styles -->
<!-- build:css(.tmp) css/main.css -->
<!-- include: "type": "css", "files": "css/*.css" -->
<link href="css/main.css" rel="stylesheet" type="text/css">
<!-- /include -->
<!-- endbuild -->
</head>
<!-- htmllint attr-bans="false" -->
<body style="padding-bottom: 50px; padding-top: 70px" ng-app="accountant">
<!-- htmllint attr-bans="$previous" -->
<!-- Navbar -->
<nav class="navbar navbar-fixed-top navbar-inverse">
<div class="container-fluid">
<!-- Brand -->
<div class="navbar-header">
<a class="navbar-brand" href="#/accounts">&nbsp;Accountant</a>
</div>
</div>
</nav>
<div class="container-fluid" ng-controller="MainController">
<div ng-view></div>
</div>
<!-- build:js(accountant-ui) js/vendor.js -->
<!-- bower:js -->
<script src="bower_components/jquery/dist/jquery.js"></script>
<script src="bower_components/moment/moment.js"></script>
<script src="bower_components/bootstrap/dist/js/bootstrap.js"></script>
<script src="bower_components/angular/angular.js"></script>
<script src="bower_components/angular-resource/angular-resource.js"></script>
<script src="bower_components/angular-route/angular-route.js"></script>
<script src="bower_components/angular-strap/dist/angular-strap.js"></script>
<script src="bower_components/angular-strap/dist/angular-strap.tpl.js"></script>
<script src="bower_components/angular-xeditable/dist/js/xeditable.js"></script>
<script src="bower_components/angular-ui-notification/dist/angular-ui-notification.js"></script>
<script src="bower_components/highcharts-ng/dist/highcharts-ng.js"></script>
<script src="bower_components/highstock-release/highstock.js"></script>
<script src="bower_components/highstock-release/highcharts-more.js"></script>
<script src="bower_components/highstock-release/modules/exporting.js"></script>
<script src="bower_components/angular-http-auth/src/http-auth-interceptor.js"></script>
<script src="bower_components/meanie-angular-storage/release/meanie-angular-storage.js"></script>
<script src="bower_components/bootbox/bootbox.js"></script>
<script src="bower_components/angular-bootstrap/ui-bootstrap-tpls.js"></script>
<script src="bower_components/ngBootbox/dist/ngBootbox.js"></script>
<!-- endbower -->
<!-- endbuild -->
<!-- Custom Javascript libraries -->
<!-- build:js({.tmp,accountant-ui}) js/scripts.js -->
<!-- include: "type": "js", "files": "js/*.js" -->
<script src="js/accounts.js"></script>
<script src="js/app.js"></script>
<script src="js/operations.js"></script>
<script src="js/scheduler.js"></script>
<!-- /include -->
<!-- endbuild -->
</body>
</html>

View File

@ -1,211 +0,0 @@
/*
This file is part of Accountant.
Accountant is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Accountant is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with Accountant. If not, see <http://www.gnu.org/licenses/>.
*/
// vim: set tw=80 ts=2 sw=2 sts=2:
'use strict';
angular.module('accountant.accounts', [
'ngResource',
'ui-notification',
'xeditable',
'ngBootbox'
])
.config(['$resourceProvider', function($resourceProvider) {
// Keep trailing slashes to avoid redirect by flask..
$resourceProvider.defaults.stripTrailingSlashes = false;
}])
.factory('Account', ['$resource', function($resource) {
var Account = $resource(
'/api/account/:id', {
id: '@id'
}
);
Account.prototype.getSolds = function() {
var Solds = $resource('/api/account/:id/solds', {id: this.id});
this.solds = Solds.get();
};
Account.prototype.getBalance = function(begin, end) {
var Balance = $resource(
'/api/account/:id/balance', {
id: this.id,
begin: begin.format('YYYY-MM-DD'),
end: end.format('YYYY-MM-DD')
});
this.balance = Balance.get();
};
return Account;
}])
.controller(
'AccountController', [
'$scope', '$ngBootbox', 'Account', 'Notification',
function($scope, $ngBootbox, Account, Notification) {
/*
* Return the class for an account current value compared to authorized
* overdraft.
*/
$scope.rowClass = function(account) {
if(!account || !account.authorized_overdraft || !account.current) {
return;
}
if(account.current < account.authorized_overdraft) {
return 'danger';
} else if(account.current < 0) {
return 'warning';
}
};
/*
* Return the class for a value compared to account authorized overdraft.
*/
$scope.valueClass = function(account, value) {
if(!account || !value) {
return;
}
if(value < account.authorized_overdraft) {
return 'text-danger';
} else if(value < 0) {
return 'text-warning';
}
};
/*
* Add an empty account.
*/
$scope.add = function() {
var account = new Account({
authorized_overdraft: 0
});
// Insert account at the begining of the array.
$scope.accounts.splice(0, 0, account);
};
/*
* Cancel account edition. Remove it from array if a new one.
*/
$scope.cancelEdit = function(rowform, account, $index) {
if(!account.id) {
// Account not saved, just remove it from array.
$scope.accounts.splice($index, 1);
} else {
rowform.$cancel();
}
};
/*
* Save account.
*/
$scope.save = function(account) {
//var account = $scope.accounts[$index];
//account = angular.merge(account, $data);
return account.$save().then(function(data) {
Notification.success('Account #' + data.id + ' saved.');
// TODO Alexis Lahouze 2016-03-08 Update solds
return data;
});
};
/*
* Delete an account.
*/
$scope.delete = function(account, $index) {
var id = account.id;
$ngBootbox.confirm(
'Voulez-vous supprimer le compte \'' + account.name + '\' ?',
function(result) {
if(result) {
account.$delete().then(function() {
Notification.success('Account #' + id + ' deleted.');
// Remove account from array.
$scope.accounts.splice($index, 1);
});
}
}
);
};
// Load accounts.
$scope.accounts = Account.query();
}])
.directive(
'accountFormDialog', function($ngBootbox) {
return {
restrict: 'A',
scope: {
account: '=ngModel'
},
link: function(scope, element) {
var title = 'Account';
if(scope.account && scope.account.id) {
title = title + ' #' + scope.account.id;
}
scope.form = {};
element.on('click', function() {
//angular.copy(scope.account, scope.form);
// Open dialog with form.
$ngBootbox.customDialog({
scope: scope,
title: title,
templateUrl: 'views/account.form.tmpl.html',
onEscape: true,
buttons: {
save: {
label: 'Save',
className: 'btn-success',
callback: function() {
// Validate form
console.log(scope.form);
// Save account
console.log(scope.account);
return false;
}
},
cancel: {
label: 'Cancel',
className: 'btn-default',
callback: true
}
}
});
});
}
};
});

View File

@ -1,154 +0,0 @@
/*
This file is part of Accountant.
Accountant is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Accountant is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with Accountant. If not, see <http://www.gnu.org/licenses/>.
*/
// vim: set tw=80 ts=2 sw=2 sts=2:
'use strict';
angular.module('accountant', [
'accountant.accounts',
'accountant.operations',
'accountant.scheduler',
'ngRoute',
'ngBootbox',
'http-auth-interceptor',
'Storage.Service'
])
.factory('sessionInjector', ['$storage', function($storage) {
var sessionInjector = {
request : function(config) {
var token = $storage.get('token');
if(token) {
var token_type = $storage.get('token_type');
var authorization = token_type + ' ' + token;
config.headers.Authorization = authorization;
}
return config;
}
};
return sessionInjector;
}])
.config(['$httpProvider', function($httpProvider) {
// Define interceptors.
$httpProvider.interceptors.push('sessionInjector');
}])
.config(['$routeProvider', function($routeProvider) {
// Defining template and controller in function of route.
$routeProvider
.when('/account/:accountId/operations', {
templateUrl: 'views/operations.html',
controller: 'OperationController',
controllerAs: 'operationsCtrl'
})
.when('/account/:accountId/scheduler', {
templateUrl: 'views/scheduler.html',
controller: 'SchedulerController',
controllerAs: 'schedulerCtrl'
})
.when('/accounts', {
templateUrl: 'views/accounts.html',
controller: 'AccountController',
controllerAs: 'accountsCtrl'
})
.otherwise({
redirectTo: '/accounts'
});
}])
.config(['$storageProvider', function($storageProvider) {
// Configure storage
// Set global prefix for stored keys
$storageProvider.setPrefix('accountant');
// Change the default storage engine
// Defaults to 'local'
$storageProvider.setDefaultStorageEngine('session');
// Change the enabled storage engines
// Defaults to ['memory', 'cookie', 'session', 'local']
$storageProvider.setEnabledStorageEngines(['local', 'session']);
}])
.run(function(editableOptions) {
editableOptions.theme = 'bs3'; // bootstrap3 theme. Can be also 'bs2', 'default'
})
.controller('MainController', [
'$scope', '$rootScope', '$http', 'authService', '$storage', '$ngBootbox',
function($scope, $rootScope, $http, authService, $storage, $ngBootbox) {
$scope.dialogShown = false;
$scope.showLoginForm = function() {
// First, if there are registered credentials, use them
if($scope.dialogShown) {
return;
}
$scope.dialogShown = true;
$storage.clear();
$ngBootbox.customDialog({
title: 'Authentification requise',
templateUrl: 'views/login.tmpl.html',
buttons: {
login: {
label: 'Login',
className: 'btn-primary',
callback: function() {
$scope.dialogShown = false;
var email = $('#email').val();
var password = $('#password').val();
$http.post(
'/api/user/login',
{
'email': email,
'password': password
}
).success(function(result) {
// TODO Alexis Lahouze 2015-08-28 Handle callback.
// Call to /api/login to retrieve the token
$storage.set('token_type', result.token_type);
$storage.set('token', result.token);
$storage.set('expiration_date', result.expiration_date);
authService.loginConfirmed();
});
}
},
cancel: {
label: 'Annuler',
className: 'btn-default',
callback: function() {
authService.loginCancelled(null, 'Login cancelled by user action.');
$scope.dialogShown = false;
}
}
}
});
};
$rootScope.$on('event:auth-loginRequired', $scope.showLoginForm);
}])
;

View File

@ -1,466 +0,0 @@
/*
This file is part of Accountant.
Accountant is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Accountant is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with Accountant. If not, see <http://www.gnu.org/licenses/>.
*/
// vim: set tw=80 ts=2 sw=2 sts=2:
'use strict';
angular.module('accountant.operations', [
'accountant.accounts',
'ngRoute',
'ngResource',
'ngBootbox',
'ui-notification',
'mgcrea.ngStrap',
'highcharts-ng',
])
.config(['$resourceProvider', function($resourceProvider) {
// Keep trailing slashes to avoid redirect by flask..
$resourceProvider.defaults.stripTrailingSlashes = false;
}])
.factory('Operation', [ '$resource', function($resource) {
return $resource(
'/api/operation/:id', {
id: '@id'
}
);
}])
.factory('OHLC', [ '$resource', '$routeParams',
function($resource, $routeParams) {
return $resource(
'/api/account/:account_id/ohlc', {
account_id: $routeParams.accountId
}
);
}])
.factory('Category', [ '$resource', '$routeParams',
function($resource, $routeParams) {
return $resource(
'/api/account/:account_id/category', {
account_id: $routeParams.accountId
}
);
}])
.factory('Balance', [ '$resource', '$routeParams',
function($resource, $routeParams) {
return $resource(
'/api/account/:account_id/balance', {
account_id: $routeParams.accountId
}
);
}])
/*
* Controller for category chart.
*/
.controller(
'CategoryChartController', [
'$rootScope', '$scope', '$http', 'Category', 'Balance',
function($rootScope, $scope, $http, Category, Balance) {
var colors = Highcharts.getOptions().colors;
$scope.revenueColor = colors[2];
$scope.expenseColor = colors[3];
// Configure pie chart for categories.
$scope.config = {
options: {
chart: {
type: 'pie',
animation: {
duration: 500
}
},
plotOptions: {
pie: {
startAngle: -90
},
series: {
allowPointSelect: 0
}
},
tooltip: {
valueDecimals: 2,
valueSuffix: '€'
}
},
yAxis: {
title: {
text: 'Categories'
}
},
title: {
text: 'Répartition dépenses/recettes'
},
series: [{
name: 'Value',
data: [],
innerSize: '33%',
size: '60%',
dataLabels: {
formatter: function() {
return this.point.name;
},
distance: -40
}
}, {
name: 'Value',
data: [],
innerSize: '66%',
size: '60%',
dataLabels: {
formatter: function() {
return this.point.name !== null && this.percentage >= 2.5 ? this.point.name : null;
},
}
}]
};
$scope.brightenColor = function(color) {
var brightness = 0.2;
return Highcharts.Color(color).brighten(brightness).get();
};
// Load categories, mainly to populate the pie chart.
$scope.load = function(begin, end) {
$scope.config.loading = true;
Category.query({
begin: begin.format('YYYY-MM-DD'),
end: end.format('YYYY-MM-DD')
}, function(data) {
var expenses = [], revenues = [];
var expenseColor = $scope.brightenColor($scope.expenseColor);
var revenueColor = $scope.brightenColor($scope.revenueColor);
angular.forEach(angular.fromJson(data), function(category) {
expenses.push({
name: category.category,
y: -category.expenses,
color: expenseColor
});
revenues.push({
name: category.category,
y: category.revenues,
color: revenueColor
});
});
// Note: expenses and revenues must be in the same order than in series[0].
$scope.config.series[1].data = revenues.concat(expenses);
$scope.config.loading = false;
});
};
/*
* Get account balance.
*/
$scope.getBalance = function(begin, end) {
Balance.get({
begin: begin.format('YYYY-MM-DD'),
end: end.format('YYYY-MM-DD')
}, function(balance) {
// Update pie chart subtitle with Balance.
$scope.config.subtitle = {
text: 'Balance: ' + balance.balance
};
$scope.config.series[0].data = [{
name: 'Revenues',
y: balance.revenues,
color: $scope.revenueColor
}, {
name: 'Expenses',
y: -balance.expenses,
color: $scope.expenseColor,
}];
});
};
// Reload categories and account status on range selection.
$rootScope.$on('rangeSelectedEvent', function(e, args) {
$scope.load(args.begin, args.end);
$scope.getBalance(args.begin, args.end);
});
}])
/*
* Controller for the sold chart.
*/
.controller(
'SoldChartController', [
'$rootScope', '$scope', '$http', 'OHLC',
function($rootScope, $scope, $http, OHLC) {
// Configure chart for operations.
$scope.config = {
options: {
chart: {
zoomType: 'x'
},
rangeSelector: {
buttons: [{
type: 'month',
count: 1,
text: '1m'
}, {
type: 'month',
count: 3,
text: '3m'
}, {
type: 'month',
count: 6,
text: '6m'
}, {
type: 'year',
count: 1,
text: '1y'
}, {
type: 'all',
text: 'All'
}],
selected: 0,
},
navigator: {
enabled: true
},
tooltip: {
crosshairs: true,
shared: true,
valueDecimals: 2,
valueSuffix: '€'
},
scrollbar: {
liveRedraw: false
}
},
series: [{
type: 'ohlc',
name: 'Sold',
data: [],
dataGrouping : {
units : [[
'week', // unit name
[1] // allowed multiples
], [
'month',
[1, 2, 3, 4, 6]
]]
}
}],
title: {
text: 'Sold evolution'
},
xAxis: {
type: 'datetime',
dateTimeLabelFormats: {
month: '%e. %b',
year: '%Y'
},
minRange: 3600 * 1000 * 24 * 14, // 2 weeks
events: {
afterSetExtremes: function(e) {
$scope.$emit('rangeSelectedEvent', {
begin: moment.utc(e.min), end: moment.utc(e.max)
});
}
},
currentMin: moment.utc().startOf('month'),
currentMax: moment.utc().endOf('month')
},
yAxis: {
plotLines: [{
color: 'orange',
width: 2,
value: 0.0
}, {
color: 'red',
width: 2,
value: 0.0
}]
},
useHighStocks: true
};
$scope.loadSolds = function() {
$scope.config.loading = true;
OHLC.query({}, function(data) {
$scope.config.series[0].data = [];
angular.forEach(data, function(operation) {
$scope.config.series[0].data.push([
moment.utc(operation.operation_date).valueOf(),
operation.open, operation.high, operation.low, operation.close
]);
});
$scope.$emit('rangeSelectedEvent', {
begin: $scope.config.xAxis.currentMin,
end: $scope.config.xAxis.currentMax
});
$scope.config.loading = false;
});
};
// Reload solds when an operation is saved.
$rootScope.$on('operationSavedEvent', function() {
$scope.loadSolds();
});
// Reload solds when an operation is deleted.
$rootScope.$on('operationDeletedEvent', function() {
$scope.loadSolds();
});
// Update authorized overdraft on account loading.
$rootScope.$on('accountLoadedEvent', function(e, account) {
$scope.config.yAxis.plotLines[1].value = account.authorized_overdraft;
});
// Select beginning and end of month.
$scope.loadSolds();
}])
/*
* Controller for the operations.
*/
.controller(
'OperationController', [
'$scope', '$rootScope', '$routeParams', '$ngBootbox', 'Notification', 'Account', 'Operation',
function($scope, $rootScope, $routeParams, $ngBootbox, Notification, Account, Operation) {
// List of operations.
$scope.operations = [];
/*
* Add an empty operation.
*/
$scope.add = function() {
var operation = new Operation({
account_id: $routeParams.accountId
});
$scope.operations.splice(0, 0, operation);
};
/*
* Load operations.
*/
$scope.load = function(begin, end) {
$scope.operations = Operation.query({
account_id: $routeParams.accountId,
begin: begin.format('YYYY-MM-DD'),
end: end.format('YYYY-MM-DD')
});
};
/*
* Cancel edition.
*/
$scope.cancelEdit = function(operation, rowform, $index) {
if(!operation.id) {
$scope.operations.splice($index, 1);
} else {
rowform.$cancel();
}
};
/*
* Toggle pointed indicator for an operation.
*/
$scope.togglePointed = function(operation, rowform) {
operation.pointed = !operation.pointed;
// Save operation if not editing it.
if(!rowform.$visible) {
$scope.save(operation);
}
};
/*
* Toggle cancel indicator for an operation.
*/
$scope.toggleCanceled = function(operation) {
operation.canceled = !operation.canceled;
$scope.save(operation);
};
/*
* Save an operation and emit operationSavedEvent.
*/
$scope.save = function($data, $index) {
// Check if $data is already a resource.
var operation;
if($data.$save) {
operation = $data;
} else {
operation = $scope.operations[$index];
operation = angular.merge(operation, $data);
}
operation.confirmed = true;
return operation.$save().then(function(data) {
Notification.success('Operation #' + data.id + ' saved.');
$scope.$emit('operationSavedEvent', data);
});
};
/*
* Delete an operation and emit operationDeletedEvent.
*/
$scope.delete = function(operation, $index) {
var id = operation.id;
$ngBootbox.confirm(
'Voulez-vous supprimer l\'opération \\\'' + operation.label + '\\\' ?',
function(result) {
if(result) {
operation.$delete().then(function() {
Notification.success('Operation #' + id + ' deleted.');
// Remove operation from array.
$scope.operation.splice($index, 1);
$scope.$emit('operationDeletedEvent', operation);
});
}
}
);
};
$scope.account = Account.get({
id: $routeParams.accountId
});
/*
* Reload operations on rangeSelectedEvent.
*/
$rootScope.$on('rangeSelectedEvent', function(e, args) {
$scope.load(args.begin, args.end);
});
}]);

View File

@ -1,120 +0,0 @@
/*
This file is part of Accountant.
Accountant is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Accountant is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with Accountant. If not, see <http://www.gnu.org/licenses/>.
*/
// vim: set tw=80 ts=2 sw=2 sts=2:
'use strict';
angular.module('accountant.scheduler', [
'ngRoute',
'ngBootbox',
'ui-notification',
'mgcrea.ngStrap'
])
.config(['$resourceProvider', function($resourceProvider) {
// Keep trailing slashes to avoid redirect by flask..
$resourceProvider.defaults.stripTrailingSlashes = false;
}])
.factory('ScheduledOperation', ['$resource', function($resource) {
return $resource(
'/api/scheduled_operation/:id', {
id: '@id'
}
);
}])
.controller(
'SchedulerController', [
'$scope', '$rootScope', '$routeParams', '$ngBootbox', 'Notification', 'ScheduledOperation',
function($scope, $rootScope, $routeParams, $ngBootbox, Notification, ScheduledOperation) {
// Operation store.
$scope.operations = [];
/*
* Add a new operation at the beginning of th array.
*/
$scope.add = function() {
var operation = new ScheduledOperation({
account_id: $routeParams.accountId
});
// Insert new operation at the beginning of the array.
$scope.operations.splice(0, 0, operation);
};
/*
* Load operations.
*/
$scope.load = function() {
$scope.operations = ScheduledOperation.query({
account_id: $routeParams.accountId
});
};
/*
* Save operation.
*/
$scope.save = function($data, $index) {
var operation;
if($data.$save) {
operation = $data;
} else {
operation = $scope.operations[$index];
operation = angular.merge(operation, $data);
}
return operation.$save().then(function(data) {
Notification.success('Operation #' + data.id + ' saved.');
});
};
/*
* Cancel operation edition. Delete if new.
*/
$scope.cancelEdit = function(operation, rowform, $index) {
if(!operation.id) {
$scope.operations.splice($index, 1);
} else {
rowform.$cancel();
}
};
/*
* Delete operation.
*/
$scope.delete = function(operation, $index) {
var id = operation.id;
$ngBootbox.confirm(
'Voulez-vous supprimer l\'operation planifiée \\\'' + operation.label + '\\\' ?',
function(result) {
if(result) {
operation.$delete().then(function() {
Notification.success('Operation #' + id + ' deleted.');
// Remove account from array.
$scope.operations.splice($index, 1);
});
}
}
);
};
// Load operations on controller initialization.
$scope.load();
}]);

View File

@ -1,22 +0,0 @@
<!-- vim: set tw=80 ts=2 sw=2 sts=2: -->
<form class="form-horizontal" role="form" name="form">
<div class="form-group">
<label class="col-sm-4 control-label" for="name">Account name</label>
<div class="col-sm-8">
<input id="name" class="form-control"
name="name" ng-model="account.name"
placeholder="Account name" type="text">
</input>
</div>
</div>
<div class="form-group">
<label class="col-sm-4 control-label" for="authorized-overdraft">Authorized overdraft</label>
<div class="col-sm-8">
<input id="authorized-overdraft" class="form-control" type="number"
name="authorized_overdraft" ng-model="account.authorized_overdraft"
placeholder="Authorized overdraft">
</input>
</div>
</div>
</form>

View File

@ -1,92 +0,0 @@
<!--
This file is part of Accountant.
Accountant is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Accountant is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with Accountant. If not, see <http://www.gnu.org/licenses/>.
-->
<!-- vim: set tw=80 ts=2 sw=2 sts=2: -->
<div class="row">
<table class="table table-striped table-condensed table-hover">
<thead>
<tr>
<th>Nom du compte</th>
<th class="col-md-1">Solde courant</th>
<th class="col-md-1">Solde pointé</th>
<th class="col-md-1">Découvert autorisé</th>
<th class="col-md-1">Actions</th>
</tr>
</thead>
<tbody>
<tr>
<td colspan="5">
<button class="btn btn-success btn-success"
ng-click="add()">Ajouter</button>
</td>
</tr>
<tr id="{{ account.id }}"
class="form-inline" ng-class="rowClass(account)"
ng-repeat="account in accounts | orderBy:'name'" ng-init="account.getSolds()">
<td>
<a href="#/account/{{ account.id }}/operations">{{ account.name }}</a>
</td>
<td>
<span ng-class="valueClass(account, account.solds.current)">
{{ account.solds.current | currency : "€" }}
</span>
</td>
<td>
<span ng-class="valueClass(account, account.solds.pointed)">
{{ account.solds.pointed | currency : "€" }}
</span>
</td>
<td>
{{ account.authorized_overdraft | currency : "€" }}
</td>
<td>
<div class="btn-group btn-group-xs">
<!-- Edit account. -->
<button type="button" class="btn btn-success"
account-form-dialog ng-model="account">
<span class="fa fa-pencil-square-o"></span>
</button>
<!-- Cancel account edition. -->
<button type="button" class="btn btn-default"
ng-click="cancelEdit(rowform, account, $index)">
<span class="fa fa-times"></span>
</button>
<!-- Delete account, with confirm. -->
<button type="button" class="btn btn-default"
ng-click="delete(account, $index)">
<span class="fa fa-trash-o"></span>
</button>
<!-- Open account scheduler. -->
<a class="btn btn-default"
ng-if="account.id"
href="#/account/{{ account.id }}/scheduler">
<span class="fa fa-clock-o"></span>
</a>
</div>
</td>
</tr>
</tbody>
</table>
</div>

View File

@ -1,18 +0,0 @@
<!-- vim: set tw=80 ts=2 sw=2 sts=2: -->
<form class="form-horizontal">
<div class="form-group">
<label for="email" class="col-sm-4 control-label">Adresse email</label>
<div class="col-sm-8">
<input type="text" class="form-control" id="email" ng-model="email"
placeholder="Nom d'utilisateur">
</div>
</div>
<div class="form-group">
<label for="password" class="col-sm-4 control-label">Mot de passe</label>
<div class="col-sm-8">
<input type="password" class="form-control" id="password" ng-model="password" placeholder="Mot de passe">
</div>
</div>
</form>

View File

@ -1,152 +0,0 @@
<!--
This file is part of Accountant.
Accountant is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Accountant is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with Accountant. If not, see <http://www.gnu.org/licenses/>.
-->
<!-- vim: set tw=80 ts=2 sw=2 sts=2: -->
<div>
<!-- Chart row -->
<div class="row">
<!-- Sold evolution chart placeholder -->
<div class="col-md-8" ng-controller="SoldChartController">
<highchart id="sold-chart" config="config"></highchart>
</div>
<!-- Category piechart -->
<div class="col-md-4" ng-controller="CategoryChartController">
<highchart id="categories-chart" config="config"></highchart>
</div>
</div>
<div class="row">
<table class="table table-striped table-condensed table-hover">
<thead>
<tr>
<th class="col-md-1">Date d'op.</th>
<th>Libell&eacute; de l'op&eacute;ration</th>
<th class="col-md-1">Montant</th>
<th class="col-md-1">Solde</th>
<th class="col-md-2">Cat&eacute;gorie</th>
<th class="col-md-2">Actions</th>
</tr>
</thead>
<tbody>
<tr>
<td colspan="6">
<button class="btn btn-success" ng-click="add()">
Ajouter
</button>
</td>
</tr>
<tr id="{{ operation.id }}" class="form-inline"
ng-class="{stroke: operation.canceled, italic: !operation.confirmed, warning: operation.sold < 0, danger: operation.sold < account.authorized_overdraft}"
ng-repeat="operation in operations | orderBy:['-operation_date', '-value', 'label']">
<td>
<span editable-text="operation.operation_date"
e-data-date-format="yyyy-MM-dd" e-bs-datepicker
e-timezone="UTC"
e-class="input-sm" e-style="width: 100%"
e-name="operation_date" e-form="rowform" e-required>
{{ operation.operation_date | date:"yyyy-MM-dd" }}
</span>
</td>
<td>
<span editable-text="operation.label"
e-placeholder="Libellé de l'opération"
e-class="input-sm" e-style="width: 100%"
e-name="label" e-form="rowform" e-required>
{{ operation.label }}
</span>
</td>
<td>
<span editable-number="operation.value"
e-class="input-sm" e-style="width: 100%"
e-name="value" e-form="rowform" e-required>
{{ operation.value | currency:"€" }}
</span>
</td>
<td ng-class="{'text-warning': operation.sold < 0, 'text-danger': operation.sold < account.authorized_overdraft}">
{{ operation.sold | currency:"€" }}
</td>
<td>
<span editable-text="operation.category"
e-placeholder="Catégorie"
e-class="input-sm" e-style="width: 100%"
e-name="category" e-form="rowform" e-required>
{{ operation.category }}
</span>
</td>
<td>
<form editable-form name="rowform"
onbeforesave="save($data, $index)"
shown="!operation.id">
<div class="btn-group btn-group-xs">
<!-- Save current operation, for editing and non-confirmed non-canceled operation. -->
<button type="submit" class="btn btn-success"
ng-if="!operation.canceled && (!operation.confirmed || rowform.$visible)"
title="Save">
<span class="fa fa-floppy-o"></span>
</button>
<!-- Edit operation, for non-canceled and non-editing operation -->
<button type="button" class="btn btn-default"
ng-if="!operation.canceled && !rowform.$visible"
ng-click="rowform.$show()" title="edit">
<span class="fa fa-pencil-square-o"></span>
</button>
<!-- Cancel edition, for editing operation. -->
<button type="button" class="btn btn-default"
ng-if="rowform.$visible"
ng-click="cancelEdit(operation, rowform)">
<span class="fa fa-times"></span>
</button>
<!-- Toggle pointed operation, for non-canceled operations. -->
<button type="button" class="btn btn-default"
ng-if="!operation.canceled"
ng-click="togglePointed(operation, rowform)"
ng-class="{active: operation.pointed}" title="point">
<span ng-class="{'fa fa-check-square-o': operation.pointed, 'fa fa-square-o': !operation.pointed}"></span>
</button>
<!-- Toggle canceled operation, for non-editing operations. -->
<button type="button" class="btn btn-default"
ng-click="toggleCanceled(operation)"
ng-if="operation.scheduled_operation_id && !rowform.$visible"
ng-class="{active: operation.canceled}" title="cancel">
<span class="fa fa-remove"></span>
</button>
<!-- Delete operation, with confirm. -->
<button type="button" class="btn btn-default"
ng-if="operation.id && !operation.scheduled_operation_id"
ng-click="delete(operation, $index)">
<span class="fa fa-trash-o"></span>
</button>
</div>
</form>
</td>
</tr>
</tbody>
</table>
</div>
</div>

View File

@ -1,142 +0,0 @@
<!--
This file is part of Accountant.
Accountant is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Accountant is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with Accountant. If not, see <http://www.gnu.org/licenses/>.
-->
<!-- vim: set tw=80 ts=2 sw=2 sts=2: -->
<div class="row">
<table class="table table-striped table-condensed table-hover">
<thead>
<tr>
<th class="col-md-1">Date de d&eacute;but</th>
<th class="col-md-1">Date de fin</th>
<th class="col-md-1">Jour</th>
<th class="col-md-1">Fr&eacute;q.</th>
<th>Libell&eacute; de l'op&eacute;ration</th>
<th class="col-md-1">Montant</th>
<th class="col-md-2">Cat&eacute;gorie</th>
<th class="col-md-1">Actions</th>
</tr>
</thead>
<tbody>
<tr>
<td colspan="8">
<button class="btn btn-success" ng-click="add()">
Ajouter
</button>
</td>
</tr>
<tr id="{{ operation.id }}" class="form-inline"
ng-repeat="operation in operations">
<td class="col-md-1">
<span editable-text="operation.start_date"
e-style="width: 100%"
e-bs-datepicker e-data-date-format="yyyy-MM-dd"
e-name="start_date" e-form="rowform" e-required>
{{ operation.start_date | date: "yyyy-MM-dd" }}
</span>
</td>
<td>
<span editable-text="operation.stop_date"
e-style="width: 100%"
e-bs-datepicker e-data-date-format="yyyy-MM-dd"
e-name="stop_date" e-form="rowform" e-required>
{{ operation.stop_date | date: "yyyy-MM-dd" }}
</span>
</td>
<td>
<span editable-number="operation.day"
e-style="width: 100%"
e-name="day" e-form="rowform" e-required>
{{ operation.day }}
</span>
</td>
<td>
<span editable-number="operation.frequency"
e-style="width: 100%"
e-name="frequency" e-form="rowform" e-required>
{{ operation.frequency }}
</span>
</td>
<td>
<span editable-text="operation.label"
e-style="width: 100%"
e-placeholder="Libellé de l'opération"
e-name="label" e-form="rowform" e-required>
{{ operation.label }}
</span>
</td>
<td>
<span editable-number="operation.value"
e-style="width: 100%"
e-name="value" e-form="rowform" e-required>
{{ operation.value | currency : "€" }}
</span>
</td>
<td>
<span editable-text="operation.category"
e-style="width: 100%"
e-name="category" e-form="rowform">
{{ operation.category }}
</span>
</td>
<td>
<form editable-form name="rowform"
onbeforesave="save($data, $index)"
shown="!operation.id">
<div class="btn-group btn-group-xs">
<!-- Save current operation -->
<button type="submit" class="btn btn-success"
ng-if="rowform.$visible" title="Save">
<span class="fa fa-floppy-o"></span>
</button>
<!-- Edit operation. -->
<button type="button" class="btn btn-default"
ng-if="!rowform.$visible"
ng-click="rowform.$show()" title="edit">
<span class="fa fa-pencil-square-o"></span>
</button>
<!-- Cancel edit. -->
<button type="button" class="btn btn-default"
ng-if="rowform.$visible"
ng-click="cancelEdit(operation, rowform, $index)"
title="Cancel">
<span class="fa fa-times"></span>
</button>
<!-- Remove operation. -->
<button type="button" class="btn btn-default"
ng-if="operation.id"
ng-click="delete(operation, $index)"
title="remove">
<span class="fa fa-trash"></span>
</button>
</div>
</form>
</td>
</tr>
</tbody>
</table>
</div>

View File

@ -1,54 +0,0 @@
{
"name": "accountant",
"version": "0.1.0",
"authors": [
"Alexis Lahouze <xals@lahouze.org>"
],
"license": "AGPL",
"main": [
"accountant-ui/index.html",
"accountant-ui/js/app.js"
],
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"test",
"tests"
],
"dependencies": {
"jquery": "~2.2",
"moment": "~2.12",
"bootstrap": "~3.3.6",
"bootstrap-additions": "~0.3.1",
"angular": "~1.5",
"angular-resource": "~1.5",
"angular-route": "~1.5",
"angular-strap": "~2.3.6",
"angular-xeditable": "~0.1",
"angular-ui-notification": "~0.2",
"highcharts-ng": "~0.0.11",
"highstock-release": "~4.2",
"angular-http-auth": "~1.3",
"meanie-angular-storage": "~1.1",
"font-awesome": ">=4.5.0",
"bootbox": "~4.4.0",
"angular-bootstrap": "~1.3",
"ngBootbox": "^0.1.3"
},
"overrides": {
"bootstrap": {
"main": [
"less/bootstrap.less",
"dist/css/bootstrap.css",
"dist/js/bootstrap.js"
]
},
"font-awesome": {
"main": [
"./css/font-awesome.css",
"./fonts/*"
]
}
}
}

View File

@ -1,7 +0,0 @@
module.exports = {
runserver: {
cmd: 'python -m manage runserver -d -r',
fail: true,
bg: true
}
};

View File

@ -1,7 +0,0 @@
'use strict';
module.exports = {
dist: [
'<%= accountant.frontend.dist %>'
]
};

View File

@ -1,40 +0,0 @@
'use strict';
module.exports = {
options: {
port: 5001,
hostname: 'localhost',
base: '<%= accountant.frontend.src %>',
apiUrl: 'http://localhost:5000/api/',
swaggerUiUrl: 'http://localhost:5000/swaggerui/',
livereload: 1337,
},
proxies: [{
context: '/api',
host: '127.0.0.1',
port: 5000,
https: false
}, {
contect: '/swaggerui',
host: '127.0.0.1',
port: 5000,
https: false
}],
livereload: {
options: {
//open: true,
middleware: function(connect, options, middlewares) {
var connectLogger = require('connect-logger');
var connectProxy = require('connect-proxy-layer');
var apiProxy = connectProxy(options.apiUrl);
var swaggerUiProxy = connectProxy(options.swaggerUiUrl);
return [
connectLogger(),
connect().use('/api', apiProxy),
connect().use('/swaggerUi', swaggerUiProxy),
].concat(middlewares);
}
}
}
};

View File

@ -1,22 +0,0 @@
'use strict';
module.exports = {
dist: {
files: [{
expand: true,
dot: true,
cwd: '<%= accountant.frontend.src %>',
dest: '<%= accountant.frontend.dist %>',
src :[
'*.html',
'views/*.html',
]
}]
},
styles: {
expand: true,
cwd: '<%= accountant.frontend.src %>/css',
dest: '.tmp/css',
src: '{,*/}*.css'
}
};

View File

@ -1,17 +0,0 @@
'use strict';
module.exports = {
options: {
encoding: 'utf-8',
algorithm: 'md5',
length: 8
},
dist: {
src: [
'<%= accountant.frontend.dist %>/css/*.css',
'<%= accountant.frontend.dist %>/js/*.js',
'!<%= accountant.frontend.dist %>/css/*.map.css',
'!<%= accountant.frontend.dist %>/js/*.map.js'
]
},
};

View File

@ -1,7 +0,0 @@
'use strict';
module.exports = {
src: [
'accountant/**/*.py'
]
};

View File

@ -1,16 +0,0 @@
module.exports = {
frontend: {
options: {
'attr-name-style': 'dash',
'attr-req-value': false,
'id-class-ignore-regex': '{{.*?}}',
'id-class-style': 'dash',
'indent-style': 'spaces',
'indent-width': 2
},
src: [
'<%= accountant.frontend.src %>/*.html',
'<%= accountant.frontend.src %>/views/*.html'
]
}
};

View File

@ -1,13 +0,0 @@
'use strict';
module.exports = {
options: {
basePath: '<%= accountant.frontend.src %>',
baseUrl: ''
},
index: {
files: {
'<%= accountant.frontend.src %>/index.html': '<%= accountant.frontend.src %>/index.html'
}
}
};

View File

@ -1,13 +0,0 @@
module.exports = {
options: {
config: '.jscsrc',
verbose: true
},
frontend_js: [
'<%= accountant.frontend.src %>/js/*.js'
],
toolchain: [
'Gruntfile.js',
'grunt-config/*.js'
]
};

View File

@ -1,13 +0,0 @@
module.exports = {
options: {
jshintrc: '.jshintrc',
reporter: require('jshint-stylish')
},
frontend_js: [
'<%= accountant.frontend.src %>/js/*.js'
],
toolchain: [
'Gruntfile.js',
'grunt-config/*.js'
]
};

View File

@ -1,12 +0,0 @@
'use strict';
module.exports = {
dist: {
files: [{
expand: true,
cwd: '.tmp/concat/js',
src: '*.js',
dest: '.tmp/concat/js'
}]
}
};

View File

@ -1,11 +0,0 @@
module.exports = {
npm_install: {
command: 'npm install'
},
bower_install: {
command: 'bower install'
},
pip_install: {
command: 'pip install --upgrade --requirement requirements.txt'
}
};

View File

@ -1,16 +0,0 @@
'use strict';
module.exports = {
html: ['<%= accountant.frontend.dist %>/{,*/}*.html'],
css: ['<%= accountant.frontend.dist %>/css/{,*/}*.css'],
js: ['<%= accountant.frontend.dist %>/js/{,*/}*.js'],
options: {
assetsDir: [
'<%= accountant.frontend.dist %>',
'<%= accountant.frontend.dist %>/css',
],
patterns: {
js: [[/(images\/[^''""]*\.(png|jpg|jpeg|gif|webp|svg))/g, 'Replacing references to images']]
}
}
};

View File

@ -1,17 +0,0 @@
'use strict';
module.exports = {
html: '<%= accountant.frontend.src %>/index.html ',
options: {
dest: '<%= accountant.frontend.dist %>',
flow: {
html: {
steps: {
js: ['concat', 'uglify'],
css: ['cssmin']
},
post: {}
}
}
}
};

View File

@ -1,43 +0,0 @@
'use strict';
module.exports = {
bower: {
files: 'bower.json',
tasks: ['wiredep']
},
js: {
files: [
'<%= accountant.frontend.src %>/js/*.js %>',
'grunt-config/*.js'
],
tasks: ['jsdev']
},
py: {
files: 'accountant/**/*.py',
tasks: ['pydev', 'bgShell:runserver']
},
html: {
files: [
'<%= accountant.frontend.src %>/*.html',
'<%= accountant.frontend.src %>/views/*.html'
],
tasks: ['htmldev']
},
gruntfile: {
files: ['Gruntfile.js', 'grunt-config/*.js']
},
livereload: {
options: {
livereload: '<%= connect.options.livereload %>'
},
files: [
'<%= accountant.frontend.src %>/{,*/}*.html',
'<%= accountant.frontend.src %>/js/*.js',
'<%= accountant.frontend.src %>/css/*.css'
]
},
requirements: {
files: ['requirements.txt'],
tasks: ['shell:pip_install']
}
};

View File

@ -1,6 +0,0 @@
module.exports = {
app: {
src: ['<%= accountant.frontend.src %>/index.html'],
ignorePath: /\.\.\//
}
};

View File

@ -1,47 +0,0 @@
#!/usr/bin/env python
from flask.ext.script import Manager
from flask.ext.migrate import Migrate, MigrateCommand, stamp
from accountant import app, db
from accountant.api.models.users import User
manager = Manager(app)
migrate = Migrate(app, db)
manager.add_command('db', MigrateCommand)
@manager.command
def initdb():
""" Create the database ans stamp it. """
tables = db.engine.table_names()
if len(tables) > 1 and 'alembic_version' not in tables:
exit("Database already initialized.")
db.metadata.create_all(bind=db.engine)
stamp()
print("Database created.")
user_manager = Manager(usage="Manage users.")
manager.add_command('user', user_manager)
@user_manager.command
def add(email, password):
""" Add a new user. """
user = User()
user.email = email
user.password = User.hash_password(password)
db.session.add(user)
print("User '%s' successfully added." % email)
if __name__ == "__main__":
manager.run()

View File

@ -1 +0,0 @@
Generic single-database configuration.

View File

@ -1,45 +0,0 @@
# A generic, single database configuration.
[alembic]
# template used to generate migration files
# file_template = %%(rev)s_%%(slug)s
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S

View File

@ -1,73 +0,0 @@
from __future__ import with_statement
from alembic import context
from sqlalchemy import engine_from_config, pool
from logging.config import fileConfig
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
fileConfig(config.config_file_name)
# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
from flask import current_app
config.set_main_option('sqlalchemy.url', current_app.config.get('SQLALCHEMY_DATABASE_URI'))
target_metadata = current_app.extensions['migrate'].metadata
# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.
def run_migrations_offline():
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
url = config.get_main_option("sqlalchemy.url")
context.configure(url=url)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online():
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
engine = engine_from_config(
config.get_section(config.config_ini_section),
prefix='sqlalchemy.',
poolclass=pool.NullPool)
connection = engine.connect()
context.configure(
connection=connection,
target_metadata=target_metadata
)
try:
with context.begin_transaction():
context.run_migrations()
finally:
connection.close()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()

View File

@ -1,22 +0,0 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision}
Create Date: ${create_date}
"""
# revision identifiers, used by Alembic.
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
def upgrade():
${upgrades if upgrades else "pass"}
def downgrade():
${downgrades if downgrades else "pass"}

View File

@ -1,34 +0,0 @@
"""Add user support.
Revision ID: 1232daf66ac
Revises: 144929e0f5f
Create Date: 2015-08-31 10:24:40.578432
"""
# revision identifiers, used by Alembic.
revision = '1232daf66ac'
down_revision = '144929e0f5f'
from alembic import op
import sqlalchemy as sa
def upgrade():
### commands auto generated by Alembic - please adjust! ###
op.create_table('user',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('email', sa.String(length=200), nullable=False),
sa.Column('password', sa.String(length=100), nullable=True),
sa.Column('active', sa.Boolean(), server_default=sa.text('true'), nullable=False),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_user_email'), 'user', ['email'], unique=True)
### end Alembic commands ###
def downgrade():
### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f('ix_user_email'), table_name='user')
op.drop_table('user')
### end Alembic commands ###

View File

@ -1,142 +0,0 @@
"""Improve operation scheduling.
Revision ID: 144929e0f5f
Revises: None
Create Date: 2015-07-17 15:04:01.002581
"""
# revision identifiers, used by Alembic.
revision = '144929e0f5f'
down_revision = None
from alembic import op
import sqlalchemy as sa
from accountant.api.models.scheduled_operations import ScheduledOperation
def upgrade():
op.get_bind().execute("DROP VIEW operation")
op.rename_table('entry', 'operation')
# Add column "canceled" in table "entry"
op.add_column(
'operation',
sa.Column(
'canceled',
sa.Boolean(),
nullable=False,
default=False,
server_default=sa.false()
)
)
# Add column "confirmed" in table "entry"
op.add_column(
'operation',
sa.Column(
'confirmed',
sa.Boolean(),
nullable=False,
default=True,
server_default=sa.true()
)
)
# Drop unused table canceled_operation.
op.drop_table('canceled_operation')
op.get_bind().execute(
"alter sequence entry_id_seq rename to operation_id_seq"
)
connection = op.get_bind()
Session = sa.orm.sessionmaker()
session = Session(bind=connection)
# Get all scheduled operations
scheduled_operations = ScheduledOperation.query(session).all()
for scheduled_operation in scheduled_operations:
scheduled_operation.reschedule(session)
def downgrade():
op.create_table(
"canceled_operation",
sa.Column("id", sa.Integer, primary_key=True),
sa.Column(
"scheduled_operation_id", sa.Integer(),
sa.ForeignKey("scheduled_operation.id")),
sa.Column("operation_date", sa.Date, nullable=False)
)
op.drop_column('operation', 'canceled')
op.drop_column('operation', 'confirmed')
op.get_bind().execute(
"alter sequence operation_id_seq rename to entry_id_seq"
)
op.rename_table('operation', 'entry')
op.get_bind().execute(
"""
CREATE VIEW operation AS
SELECT entry.id,
entry.operation_date,
entry.label,
entry.value,
entry.account_id,
entry.category,
entry.pointed,
entry.scheduled_operation_id,
false AS canceled
FROM entry
UNION
SELECT NULL::bigint AS id,
scheduled_operation.operation_date,
scheduled_operation.label,
scheduled_operation.value,
scheduled_operation.account_id,
scheduled_operation.category,
false AS pointed,
scheduled_operation.id AS scheduled_operation_id,
false AS canceled
FROM (
SELECT scheduled_operation_1.id,
scheduled_operation_1.start_date,
scheduled_operation_1.stop_date,
scheduled_operation_1.day,
scheduled_operation_1.frequency,
scheduled_operation_1.label,
scheduled_operation_1.value,
scheduled_operation_1.account_id,
scheduled_operation_1.category,
generate_series(scheduled_operation_1.start_date::timestamp without time zone, scheduled_operation_1.stop_date::timestamp without time zone, scheduled_operation_1.frequency::double precision * '1 mon'::interval) AS operation_date
FROM scheduled_operation scheduled_operation_1) scheduled_operation
LEFT JOIN (
SELECT entry.scheduled_operation_id,
date_trunc('MONTH'::text, entry.operation_date::timestamp with time zone) AS operation_date
FROM entry
UNION
SELECT canceled_operation.scheduled_operation_id,
date_trunc('MONTH'::text, canceled_operation.operation_date::timestamp with time zone) AS operation_date
FROM canceled_operation
) saved_operation ON saved_operation.scheduled_operation_id = scheduled_operation.id AND saved_operation.operation_date = date_trunc('MONTH'::text, scheduled_operation.operation_date)
WHERE saved_operation.scheduled_operation_id IS NULL
UNION
SELECT NULL::bigint AS id,
canceled_operation.operation_date,
scheduled_operation.label,
scheduled_operation.value,
scheduled_operation.account_id,
scheduled_operation.category,
false AS pointed,
scheduled_operation.id AS scheduled_operation_id,
true AS canceled
FROM scheduled_operation
JOIN canceled_operation ON canceled_operation.scheduled_operation_id = scheduled_operation.id;
"""
)

View File

@ -4,31 +4,77 @@
"repository": "https://git.lahouze.org/xals/accountant",
"license": "AGPL-1.0",
"devDependencies": {
"connect-logger": "0.0.1",
"connect-proxy-layer": "^0.1.2",
"grunt": "~1.0",
"grunt-bg-shell": "^2.3.1",
"grunt-contrib-clean": "~1.0",
"grunt-contrib-concat": "~1.0",
"grunt-contrib-connect": "~1.0",
"grunt-contrib-cssmin": "~1.0",
"grunt-contrib-jshint": "~1.0",
"grunt-contrib-uglify": "~1.0",
"grunt-contrib-watch": "~1.0",
"grunt-copy": "^0.1.0",
"grunt-filerev": "^2.3.1",
"grunt-flake8": "^0.1.3",
"grunt-htmllint": "^0.2.7",
"grunt-include-source": "^0.7.1",
"grunt-jscs": "^2.7.0",
"grunt-newer": "^1.1.1",
"grunt-ng-annotate": "~2.0",
"grunt-shell": "^1.1.2",
"grunt-usemin": "^3.1.1",
"grunt-wiredep": "~3.0",
"jshint-stylish": "^2.1.0",
"load-grunt-configs": "^0.4.3",
"load-grunt-tasks": "^3.2.0",
"time-grunt": "^1.3.0"
"angular2-template-loader": "^0.6.2",
"awesome-typescript-loader": "^3.2.3",
"babel-core": "^6.26.0",
"babel-eslint": "^8.0.1",
"babel-loader": "^7.1.2",
"codelyzer": "^3.1.2",
"css-loader": "^0.28.5",
"ejs-loader": "^0.3.0",
"eslint": "^4.10.0",
"eslint-config-angular": "^0.5.0",
"eslint-config-webpack": "^1.2.5",
"eslint-loader": "^1.9.0",
"eslint-plugin-angular": "^3.1.0",
"eslint-plugin-html": "^3.2.0",
"eslint-plugin-jquery": "^1.2.1",
"eslint-plugin-promise": "^3.6.0",
"eslint-plugin-security": "^1.4.0",
"eslint-plugin-this": "^0.2.2",
"extract-text-webpack-plugin": "^3.0.2",
"file-loader": "^1.1.5",
"html-loader": "^0.5.1",
"html-webpack-plugin": "^2.30.1",
"htmllint-loader": "^2.1.4",
"imports-loader": "^0.7.1",
"less": "^3.0.0-alpha.3",
"less-loader": "^4.0.5",
"loglevel": "^1.5.1",
"ngtemplate-loader": "^2.0.1",
"node-sass": "^4.5.3",
"resolve-url-loader": "^2.1.0",
"sass-loader": "^6.0.6",
"style-loader": "^0.19.0",
"ts-loader": "^3.1.0",
"tslint": "^5.7.0",
"tslint-config-prettier": "^1.4.0",
"tslint-loader": "^3.5.3",
"typescript": "^2.4.2",
"url-loader": "^0.6.2",
"webpack": "^3.8.1",
"webpack-concat-plugin": "^1.4.1",
"webpack-dev-server": "^2.9.3"
},
"dependencies": {
"@angular/animations": "^4.4.6",
"@angular/common": "^4.4.6",
"@angular/compiler": "^4.4.6",
"@angular/core": "^4.4.6",
"@angular/forms": "^4.4.6",
"@angular/http": "^4.4.6",
"@angular/platform-browser": "^4.4.6",
"@angular/platform-browser-dynamic": "^4.4.6",
"@angular/router": "^4.4.6",
"@ng-bootstrap/ng-bootstrap": "^1.0.0-beta.2",
"@nsalaun/ng-logger": "^2.0.2",
"@types/c3": "^0.4.45",
"@types/node": "^8.0.47",
"angular2-text-mask": "^8.0.3",
"base64util": "^1.0.2",
"bootstrap": "4.0.0-beta",
"c3": "^0.4.17",
"font-awesome": "^4.7.0",
"jquery": "^3.2.1",
"moment": "^2.19.1",
"ng2-materialize": "^1.5.1",
"ngx-toastr": "^6.5.0",
"reflect-metadata": "^0.1.10",
"rxjs": "^5.5.2",
"zone.js": "^0.8.17"
},
"scripts": {
"build": "webpack --config webpack.config.js",
"dev": "webpack-dev-server --debug --devtool eval --config webpack.config.js --progress --colors --hot --content-base build"
}
}

View File

@ -1,178 +0,0 @@
/*
This file is part of Accountant.
Accountant is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Accountant is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with Accountant. If not, see <http://www.gnu.org/licenses/>.
*/
--
-- PostgreSQL database dump
--
SET statement_timeout = 0;
SET client_encoding = 'UTF8';
SET standard_conforming_strings = on;
SET check_function_bodies = false;
SET client_min_messages = warning;
--
-- Name: plpgsql; Type: EXTENSION; Schema: -; Owner: -
--
CREATE EXTENSION IF NOT EXISTS plpgsql WITH SCHEMA pg_catalog;
--
-- Name: EXTENSION plpgsql; Type: COMMENT; Schema: -; Owner: -
--
COMMENT ON EXTENSION plpgsql IS 'PL/pgSQL procedural language';
SET search_path = public, pg_catalog;
SET default_tablespace = '';
SET default_with_oids = false;
--
-- Name: account; Type: TABLE; Schema: public; Owner: -; Tablespace:
--
CREATE TABLE account (
id integer NOT NULL,
name character varying(200) NOT NULL,
authorized_overdraft integer DEFAULT 0 NOT NULL
);
--
-- Name: account_id_seq; Type: SEQUENCE; Schema: public; Owner: -
--
CREATE SEQUENCE account_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
--
-- Name: account_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
--
ALTER SEQUENCE account_id_seq OWNED BY account.id;
--
-- Name: entry; Type: TABLE; Schema: public; Owner: -; Tablespace:
--
CREATE TABLE entry (
id bigint NOT NULL,
operation_date date,
label character varying(500) NOT NULL,
comment character varying(500),
value numeric(15,2) NOT NULL,
account_id integer NOT NULL,
category character varying(100),
pointed boolean DEFAULT false NOT NULL
);
--
-- Name: entry_id_seq; Type: SEQUENCE; Schema: public; Owner: -
--
CREATE SEQUENCE entry_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
--
-- Name: entry_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
--
ALTER SEQUENCE entry_id_seq OWNED BY entry.id;
--
-- Name: id; Type: DEFAULT; Schema: public; Owner: -
--
ALTER TABLE ONLY account ALTER COLUMN id SET DEFAULT nextval('account_id_seq'::regclass);
--
-- Name: id; Type: DEFAULT; Schema: public; Owner: -
--
ALTER TABLE ONLY entry ALTER COLUMN id SET DEFAULT nextval('entry_id_seq'::regclass);
--
-- Name: account_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace:
--
ALTER TABLE ONLY account
ADD CONSTRAINT account_pkey PRIMARY KEY (id);
--
-- Name: entry_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace:
--
ALTER TABLE ONLY entry
ADD CONSTRAINT entry_pkey PRIMARY KEY (id);
--
-- Name: entry_account_id_idx; Type: INDEX; Schema: public; Owner: -; Tablespace:
--
CREATE INDEX entry_account_id_idx ON entry USING btree (account_id);
--
-- Name: entry_operation_date_idx; Type: INDEX; Schema: public; Owner: -; Tablespace:
--
CREATE INDEX entry_operation_date_idx ON entry USING btree (operation_date);
--
-- Name: entry_account_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY entry
ADD CONSTRAINT entry_account_id_fkey FOREIGN KEY (account_id) REFERENCES account(id);
--
-- Name: public; Type: ACL; Schema: -; Owner: -
--
REVOKE ALL ON SCHEMA public FROM PUBLIC;
REVOKE ALL ON SCHEMA public FROM postgres;
GRANT ALL ON SCHEMA public TO postgres;
GRANT ALL ON SCHEMA public TO PUBLIC;
--
-- PostgreSQL database dump complete
--

View File

@ -1,18 +0,0 @@
/*
This file is part of Accountant.
Accountant is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Accountant is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with Accountant. If not, see <http://www.gnu.org/licenses/>.
*/
ALTER TABLE account ADD COLUMN authorized_overdraft INTEGER NOT NULL DEFAULT 0;

View File

@ -1,26 +0,0 @@
/*
This file is part of Accountant.
Accountant is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Accountant is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with Accountant. If not, see <http://www.gnu.org/licenses/>.
*/
ALTER TABLE entry ADD COLUMN pointed BOOLEAN NOT NULL DEFAULT false;
UPDATE entry SET pointed = operation_date IS NOT NULL;
UPDATE entry SET operation_date = value_date;
ALTER TABLE entry DROP COLUMN value_date;
CREATE INDEX entry_operation_date_idx ON entry USING btree (operation_date);

View File

@ -1,16 +0,0 @@
/*
This file is part of Accountant.
Accountant is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Accountant is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with Accountant. If not, see <http://www.gnu.org/licenses/>.
*/

View File

@ -1,16 +0,0 @@
create table scheduled_operation(
id serial primary key,
start_date date not null,
stop_date date not null,
day integer not null check (day > 0 and day <= 31),
frequency integer not null check (frequency > 0),
label varchar(500) not null,
value numeric(15,2) not null,
account_id integer not null references account(id),
category varchar(100)
);
create index scheduled_operation_account_id_idx on scheduled_operation(account_id);
alter table entry add column scheduled_operation_id integer references scheduled_operation(id);

View File

@ -1,2 +0,0 @@
alter table entry alter column operation_date set not null;
alter table entry drop column comment;

View File

@ -0,0 +1,55 @@
// vim: set tw=80 ts=2 sw=2 sts=2 :
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ReactiveFormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http';
import { RouterModule } from '@angular/router';
import { NgLoggerModule, Level } from '@nsalaun/ng-logger';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { ToastrModule } from 'ngx-toastr';
import { MaterializeModule } from 'ng2-materialize';
import { AccountService } from './account.service';
import { AccountBalancesService } from './accountBalances.service';
import { AccountListComponent } from './accountList.component';
import { AccountDeleteModalComponent } from './accountDeleteModal.component';
import { AccountEditModalComponent } from './accountEditModal.component';
import { AccountFormComponent } from './accountForm.component';
import { AccountRowComponent } from './accountRow.component';
import { DailyBalanceService } from './dailyBalance.service';
import { AccountListState } from './account.states'
@NgModule({
imports: [
HttpClientModule,
CommonModule,
ReactiveFormsModule,
RouterModule.forChild([
AccountListState
]),
NgLoggerModule,
ToastrModule,
NgbModule,
MaterializeModule,
],
providers: [
AccountService,
AccountBalancesService,
DailyBalanceService,
],
declarations: [
AccountListComponent,
AccountDeleteModalComponent,
AccountEditModalComponent,
AccountFormComponent,
AccountRowComponent
],
entryComponents: [
AccountListComponent,
AccountDeleteModalComponent,
AccountEditModalComponent,
]
})
export class AccountModule {}

View File

@ -0,0 +1,42 @@
// vim: set tw=80 ts=2 sw=2 sts=2 :
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs/Rx';
import { Account } from './account';
@Injectable()
export class AccountService {
constructor(
private http: HttpClient
) {}
private url(id?: Number): string {
if(id) {
return `/api/account/${id}`;
}
return `/api/account`;
}
query(): Observable<Account[]> {
return this.http.get<Account[]>(this.url());
}
get(id: number): Observable<Account> {
return this.http.get<Account>(this.url(id));
}
create(account: Account): Observable<Account> {
return this.http.post<Account>(this.url(), account);
}
update(account: Account): Observable<Account> {
return this.http.post<Account>(this.url(account.id), account);
}
delete(account: Account): Observable<Account> {
return this.http.delete<Account>(this.url(account.id));
}
}

View File

@ -0,0 +1,8 @@
// vim: set tw=80 ts=2 sw=2 sts=2 :
import { AccountListComponent } from './accountList.component';
export const AccountListState = {
path: 'accounts',
component: AccountListComponent
}

15
src/accounts/account.ts Normal file
View File

@ -0,0 +1,15 @@
// vim: set tw=80 ts=2 sw=2 sts=2 :
import { AccountBalances } from './accountBalances';
export class Account {
id: number;
name: string;
authorized_overdraft: number;
balances: AccountBalances;
constructor() {
this.authorized_overdraft = 0;
}
}

View File

@ -0,0 +1,18 @@
// vim: set tw=80 ts=2 sw=2 sts=2 :
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Rx';
import { HttpClient, HttpParams } from "@angular/common/http";
import { AccountBalances } from './accountBalances';
@Injectable()
export class AccountBalancesService {
constructor(
private http: HttpClient
) {}
get(id: number): Observable<AccountBalances> {
return this.http.get<AccountBalances>(`/api/account/${id}/balances`);
}
}

View File

@ -0,0 +1,6 @@
// vim: set tw=80 ts=2 sw=2 sts=2 :
export class AccountBalances {
current: number;
pointed: number;
future: number;
}

View File

@ -0,0 +1,53 @@
// vim: set tw=80 ts=2 sw=2 sts=2:
import { Component, Input } from '@angular/core';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { Account } from './account';
@Component({
selector: 'account-delete-modal',
template: `
<div class="modal-header">
<h3 class="modal-title" id="modal-title">{{ title() }}</h3>
</div>
<div class="modal-body" id="modal-body">
<p>
Do you really want to delete account #{{ account.id }} with name:<br/>
{{ account.name }}
</p>
</div>
<div class="modal-footer">
<button class="btn btn-danger" (click)="submit()">
Yes
</button>
<button class="btn btn-default" (click)="cancel()">
No
</button>
</div>
`
})
export class AccountDeleteModalComponent {
@Input() account: Account
constructor(public activeModal: NgbActiveModal) {}
title(): string {
if(this.account.id) {
return "Account #" + this.account.id;
} else {
return "New account";
}
}
submit(): void {
this.activeModal.close(this.account);
}
cancel(): void {
this.activeModal.dismiss("closed");
}
}

View File

@ -0,0 +1,59 @@
// vim: set tw=80 ts=2 sw=2 sts=2:
import { Component, Input, ViewChild } from '@angular/core';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { Account } from './account';
import { AccountFormComponent } from './accountForm.component';
@Component({
selector: 'account-edit-modal',
template: `
<div class="modal-header">
<h3 class="modal-title" id="modal-title">{{ title() }}</h3>
</div>
<div class="modal-body" id="modal-body">
<account-form [account]="account" (submit)="submit()" #accountForm="accountForm"></account-form>
</div>
<div class="modal-footer">
<button class="btn btn-primary" [disabled]="!accountForm.form.valid" (click)="submit()">
Save
</button>
<button class="btn btn-default" (click)="cancel()">
Cancel
</button>
</div>
`
})
export class AccountEditModalComponent {
@Input() account: Account;
@ViewChild('accountForm') accountForm: AccountFormComponent;
constructor(private activeModal: NgbActiveModal) {}
title(): string {
if(this.account.id) {
return "Account #" + this.account.id;
} else {
return "New account";
}
}
submit(): void {
let formModel = this.accountForm.form.value;
let account = Object.assign({}, this.account);
account.id = this.account.id;
account.name = formModel.name;
account.authorized_overdraft = formModel.authorizedOverdraft;
this.activeModal.close(account);
}
cancel(): void {
this.activeModal.dismiss("closed");
}
}

View File

@ -0,0 +1,91 @@
// vim: set tw=80 ts=2 sw=2 sts=2 :
import { Component, OnInit, OnChanges, Input, Output, EventEmitter } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Account } from './account';
@Component({
selector: 'account-form',
exportAs: 'accountForm',
template: `
<form novalidate
(keyup.enter)="submit()" [formGroup]="form">
<div class="form-group row">
<label class="col-sm-4 control-label" for="name">
Account name
</label>
<div class="col-sm-8"
[class.has-danger]="name.errors">
<input class="form-control"
id="name" formControlName="name"
placeholder="Account name">
<div class="help-block text-danger" *ngIf="name.errors">
<p *ngIf="name.errors.required">The account name is required.</p>
</div>
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 control-label" for="authorized-overdraft">
Authorized overdraft
</label>
<div class="col-sm-8"
[class.has-danger]="authorizedOverdraft.errors">
<div class="input-group">
<input class="form-control"
id="authorized-overdraft" formControlName="authorizedOverdraft"
placeholder="Authorized overdraft">
<div class="input-group-addon">.00</div>
</div>
<div class="help-block text-danger" *ngIf="authorizedOverdraft.errors">
<p *ngIf="authorizedOverdraft.errors.required">
The authorized overdraft is required.
</p>
<p *ngIf="authorizedOverdraft.errors.max">
The authorized overdraft must be less than or equal to 0.
</p>
</div>
</div>
</div>
</form>
`
})
export class AccountFormComponent implements OnInit {
public form: FormGroup;
@Input() account: Account;
@Output('submit') submitEventEmitter: EventEmitter<void> = new EventEmitter<void>();
constructor(private formBuilder: FormBuilder) {}
ngOnInit() {
this.form = this.formBuilder.group({
name: ['', Validators.required],
authorizedOverdraft: ['', [Validators.required, Validators.max(0)]],
});
this.form.patchValue({
name: this.account.name,
authorizedOverdraft: this.account.authorized_overdraft
});
}
submit() {
if(this.form.valid) {
this.submitEventEmitter.emit();
}
}
get name() {
return this.form.get('name');
}
get authorizedOverdraft() {
return this.form.get('authorizedOverdraft');
}
}

View File

@ -0,0 +1,104 @@
// vim: set tw=80 ts=2 sw=2 sts=2 :
import { Component, Inject, OnInit } from '@angular/core';
import { Observable } from 'rxjs/Rx';
import { Logger } from '@nsalaun/ng-logger';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { ToastrService } from 'ngx-toastr';
import { Account } from './account';
import { AccountBalances } from './accountBalances';
import { AccountService } from './account.service';
import { AccountEditModalComponent } from './accountEditModal.component';
@Component({
selector: 'account-list',
template: `
<div class="row">
<div class="col s12">
<table class="bordered highlight responsive-table">
<thead>
<tr>
<th>Nom du compte</th>
<th>Solde courant</th>
<th>Solde pointé</th>
<th>Découvert autorisé</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr>
<td colspan="5">
<button mz-button class="green" (click)="add()">
Ajouter
</button>
</td>
</tr>
<tr *ngFor="let account of accounts"
[account-row]="account" (needsReload)="load()">
</tr>
</tbody>
</table>
</div>
</div>
`,
})
export class AccountListComponent implements OnInit {
accounts: Account[];
constructor(
private accountService: AccountService,
private toastrService: ToastrService,
private logger: Logger,
private ngbModal: NgbModal
) {
}
ngOnInit() {
// Load accounts.
this.load();
}
load() {
this.logger.log("Load accounts.");
this.accountService.query().subscribe(accounts => {
this.accounts = accounts;
});
};
/*
* Add an empty account.
*/
add() {
const modal = this.ngbModal.open(AccountEditModalComponent, {
size: 'lg'
});
modal.componentInstance.account = new Account();
modal.result.then((account: Account) => {
this.logger.log("Modal closed => save account", account);
this.save(account);
}, (reason) => function(reason) {
});
};
/*
* Save account.
*/
save(account) {
this.accountService.create(account).subscribe(account => {
this.toastrService.success('Account #' + account.id + ' saved.');
this.load();
}, result => {
this.logger.error('Error while saving account', account, result);
this.toastrService.error(
'Error while saving account: ' + result.message
);
});
};
};

View File

@ -0,0 +1,153 @@
// vim: set tw=80 ts=2 sw=2 sts=2 :
import { CurrencyPipe } from '@angular/common';
import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core';
import { Logger } from '@nsalaun/ng-logger';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { ToastrService } from 'ngx-toastr';
import { Account } from './account';
import { AccountBalances } from './accountBalances';
import { AccountBalancesService } from './accountBalances.service';
import { AccountService } from './account.service';
import { AccountDeleteModalComponent } from './accountDeleteModal.component';
import { AccountEditModalComponent } from './accountEditModal.component';
@Component({
selector: 'tr[account-row]',
host: {
"[id]": "account.id",
"class": "lighten-5",
"[class.orange]": "account.authorized_overdraft < 0 && accountBalances?.current < 0",
"[class.red]": "accountBalances?.current < account.authorized_overdraft",
},
template: `
<td>
<a [routerLink]="['/account', account.id, 'operations']">{{ account.name }}</a>
</td>
<td>
<span class="text-lighten-2"
[class.orange-text]="account.authorized_overdraft < 0 && accountBalances?.current < 0"
[class.red-text]="accountBalances?.current < account.authorized_overdraft">
{{ accountBalances?.current | currency:"EUR":true }}
</span>
</td>
<td>
<span class="text-lighten-2"
[class.orange-text]="account.authorized_overdraft < 0 && accountBalances?.pointed < 0"
[class.red-text]="accountBalances?.pointed < account.authorized_overdraft">
{{ accountBalances?.pointed | currency:"EUR":true }}
</span>
</td>
<td>{{ account.authorized_overdraft | currency:"EUR":true }}</td>
<td>
<!-- Edit account. -->
<button mz-button [float]="true" class="green"
(click)="modify()">
<span class="fa fa-pencil-square-o"></span>
</button>
<!-- Delete account, with confirm. -->
<button mz-button [float]="true" class="red"
(click)="confirmDelete()">
<span class="fa fa-trash-o"></span>
</button>
<!-- Open account scheduler. -->
<button mz-button [float]="true"
[hidden]="!account.id"
[routerLink]="['/account', account.id, 'scheduler']">
<span class="fa fa-clock-o"></span>
</button>
</td>
`
})
export class AccountRowComponent implements OnInit {
@Input('account-row') account: Account;
@Output() needsReload: EventEmitter<void> = new EventEmitter<void>();
private accountBalances: AccountBalances;
constructor(
private accountService: AccountService,
private accountBalancesService: AccountBalancesService,
private toastrService: ToastrService,
private logger: Logger,
private ngbModal: NgbModal
) {
this.logger.log("AccountRowComponent constructor");
}
ngOnInit() {
this.logger.log(this.account);
this.accountBalancesService
.get(this.account.id)
.subscribe((accountBalances: AccountBalances) => {
this.accountBalances = accountBalances;
})
}
confirmDelete() {
const modal = this.ngbModal.open(AccountDeleteModalComponent);
modal.componentInstance.account = this.account;
modal.result.then((account: Account) => {
this.delete(account);
}, (reason) => function(reason) {
});
};
/*
* Delete an account.
*/
delete(account: Account) {
var id = account.id;
this.accountService.delete(account).subscribe(account => {
this.toastrService.success('account #' + id + ' deleted.');
this.needsReload.emit();
}, function(result) {
this.toastrService.error(
'An error occurred while trying to delete account #' +
id + ':<br />' + result
);
});
};
/*
* Open the popup to modify the account, save it on confirm.
*/
modify() {
const modal = this.ngbModal.open(AccountEditModalComponent, {
size: 'lg'
});
modal.componentInstance.account = this.account;
modal.result.then((account: Account) => {
this.logger.log("Modal closed => save account", account);
this.save(account);
}, (reason) => function(reason) {
});
};
save(account: Account) {
this.accountService.update(account).subscribe((account: Account) => {
this.toastrService.success('Account #' + account.id + ' saved.');
this.needsReload.emit();
}, result => {
this.logger.error('Error while saving account', account, result);
this.toastrService.error(
'Error while saving account: ' + result.message
);
});
};
}

View File

@ -0,0 +1,19 @@
// vim: set tw=80 ts=2 sw=2 sts=2:
import { Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { Observable } from 'rxjs/Rx';
import { DailyBalance } from './dailyBalance';
@Injectable()
export class DailyBalanceService {
constructor(
private http: HttpClient
) {}
query(id: number): Observable<DailyBalance[]> {
return this.http.get<DailyBalance[]>(`/api/account/${id}/daily_balances`);
}
}

View File

@ -0,0 +1,8 @@
// vim: set tw=80 ts=2 sw=2 sts=2 :
export class DailyBalance {
operation_date: string;
balance: number;
expenses: number;
revenues: number;
}

18
src/app.component.ts Normal file
View File

@ -0,0 +1,18 @@
// vim: set tw=80 ts=2 sw=2 sts=2 :
import { Component } from '@angular/core';
@Component({
selector: 'accountant',
template: `
<!-- Navbar -->
<div class="navbar-fixed">
<mz-navbar>
<a class="brand-logo" routerLink="/accounts">&nbsp;Accountant</a>
</mz-navbar>
</div>
<router-outlet></router-outlet>
`
})
export class AppComponent { }

5
src/app.config.ts Normal file
View File

@ -0,0 +1,5 @@
// vim: set tw=80 ts=2 sw=2 sts=2 :
import { Level } from '@nsalaun/ng-logger';
export const ApiBaseURL = "http://localhost:8080/api";
export const LogLevel = Level.LOG;

55
src/app.module.ts Normal file
View File

@ -0,0 +1,55 @@
// vim: set tw=80 ts=2 sw=2 sts=2:
import 'zone.js';
import 'reflect-metadata';
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { RouterModule } from '@angular/router';
import { NgLoggerModule } from '@nsalaun/ng-logger';
import { ToastrModule } from 'ngx-toastr';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { MaterializeModule } from 'ng2-materialize'
import { LoginModule } from './login/login.module';
import { AccountModule } from './accounts/account.module';
import { ScheduleModule } from './scheduler/schedule.module';
import { OperationModule } from './operations/operation.module';
import { AppComponent } from './app.component';
import { ApiBaseURL, LogLevel } from './app.config';
@NgModule({
imports: [
BrowserModule,
BrowserAnimationsModule,
RouterModule.forRoot([
{
path: '',
redirectTo: '/accounts',
pathMatch: 'full'
}
], {
enableTracing: true,
useHash: true
}),
LoginModule,
NgLoggerModule.forRoot(LogLevel),
ToastrModule.forRoot(),
NgbModule.forRoot(),
MaterializeModule.forRoot(),
AccountModule,
ScheduleModule,
OperationModule,
],
declarations: [
AppComponent
],
bootstrap: [ AppComponent ]
})
export class AppModule {
constructor() {}
}

16
src/index.ejs Normal file
View File

@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<base href="/">
<!-- Title -->
<title><% htmlWebpackPlugin.options.title %></title>
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<accountant></accountant>
</body>
</html>

View File

@ -0,0 +1,92 @@
// vim: set tw=80 ts=2 sw=2 sts=2 :
import { Injectable, Injector } from '@angular/core';
import {
HttpEvent, HttpInterceptor, HttpHandler, HttpRequest, HttpErrorResponse
} from '@angular/common/http';
import { Observable} from 'rxjs/Rx';
import 'rxjs/add/operator/catch';
import { Logger } from '@nsalaun/ng-logger';
import { LoginService } from './login.service';
import { Token } from './token';
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
private observable: Observable<Token>;
constructor(
private logger: Logger,
private injector: Injector,
) {}
injectAuthorizationHeader(request: HttpRequest<any>, accessToken: string) {
this.logger.log('Injecting Authorization header');
return request;
}
intercept(
request: HttpRequest<any>,
next: HttpHandler,
pass?: number
): Observable<HttpEvent<any>> {
if(!pass) {
pass = 1;
}
let loginService = this.injector.get(LoginService);
if(request.url == loginService.url) {
this.logger.log("Login URL, do not handle.");
return next.handle(request);
}
this.logger.log(`Intercepted request, pass #${pass}`, request, next);
let accessToken = loginService.accessToken;
if(accessToken){
request = request.clone({
headers: request.headers.set('Authorization', `Bearer ${accessToken}`)
});
}
this.logger.log('Request', request);
let observable: Observable<any> = next.handle(request);
return observable.catch(
(error, caught): Observable<any> => {
this.logger.error("Error", error, caught);
if(!(error instanceof HttpErrorResponse) || error.status != 401) {
return Observable.throw(error);
}
this.logger.log('Unauthorized', error);
if(pass === 3) {
return Observable.throw(error);
}
if(!this.observable) {
this.logger.log("No current login observable.")
this.observable = loginService.login();
}
return this.observable.flatMap((token: Token): Observable<HttpEvent<any>> => {
this.logger.log("Logged in, access_token:", token.access_token);
this.observable = null;
return this.intercept(request, next, ++pass);
}).catch((error) => {
this.observable = null;
return Observable.throw(error);
});
}
);
}
}

41
src/login/login.module.ts Normal file
View File

@ -0,0 +1,41 @@
// vim: set tw=80 ts=2 sw=2 sts=2 :
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ReactiveFormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http';
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { MaterializeModule } from 'ng2-materialize';
import { NgLoggerModule } from '@nsalaun/ng-logger';
import { AuthInterceptor } from './authInterceptor';
import { LoginService } from './login.service';
import { LoginFormComponent } from './loginForm.component';
import { LoginModalComponent } from './loginModal.component';
@NgModule({
imports: [
HttpClientModule,
CommonModule,
ReactiveFormsModule,
MaterializeModule,
NgLoggerModule,
],
providers: [
LoginService,
{
provide: HTTP_INTERCEPTORS,
useClass: AuthInterceptor,
multi: true
}
],
declarations: [
LoginModalComponent,
LoginFormComponent,
],
entryComponents: [
LoginModalComponent,
]
})
export class LoginModule {};

View File

@ -0,0 +1,68 @@
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Observable} from 'rxjs/Rx';
import * as base64 from 'base64util';
import { Logger } from '@nsalaun/ng-logger';
import { MzModalService } from 'ng2-materialize';
import { Token } from './token';
import { LoginModalComponent } from './loginModal.component';
import { Login } from './login';
@Injectable()
export class LoginService {
constructor(
private httpClient: HttpClient,
private logger: Logger,
private mzModalService: MzModalService,
) {}
public readonly url: string = '/api/user/login';
login(): Observable<Token> {
let modal = this.mzModalService.open(LoginModalComponent);
sessionStorage.clear();
//let observable: Observable<any> = Observable.fromPromise(modal.result);
//return observable.flatMap((login: Login) =>
// this.doLogin(login)
//).map((token: Token): Token => {
// this.accessToken = token.access_token;
// return token;
//});
return null;
}
logout() {
sessionStorage.clear();
}
doLogin(login: Login): Observable<any> {
var authdata = base64.encode(
`${login.email}:${login.password}`
);
let headers = new HttpHeaders()
headers = headers.set('Authorization', `Basic ${authdata}`);
this.logger.log("Headers", headers);
return this.httpClient.post(this.url, {}, {
headers: headers
});
}
get accessToken(): string {
return sessionStorage.getItem('access_token');
}
set accessToken(token: string) {
sessionStorage.setItem('access_token', token);
}
};

38
src/login/login.tmpl.html Normal file
View File

@ -0,0 +1,38 @@
<!-- vim: set tw=80 ts=2 sw=2 sts=2: -->
<div class="modal top am-fade" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h3 class="modal-title" id="modal-title">Authentification requise</h3>
</div>
<form class="form-horizontal" ng-submit="$login()">
<div class="modal-body" id="modal-body">
<div class="form-group">
<label for="email" class="col-sm-4 control-label">Adresse email</label>
<div class="col-sm-8">
<input type="text" class="form-control" id="email" ng-model="email"
placeholder="Nom d'utilisateur">
</div>
</div>
<div class="form-group">
<label for="password" class="col-sm-4 control-label">Mot de passe</label>
<div class="col-sm-8">
<input type="password" class="form-control" id="password"
ng-model="password" placeholder="Mot de passe">
</div>
</div>
</div>
<div class="modal-footer">
<input class="btn btn-primary" type="submit" value="OK"/>
<button class="btn btn-default" type="button" ng-click="$hide()">
Annuler
</button>
</div>
</form>
</div>
</div>
</div>

6
src/login/login.ts Normal file
View File

@ -0,0 +1,6 @@
// vim: set tw=80 ts=2 sw=2 sts=2:
export class Login {
email: string;
password: string;
}

View File

@ -0,0 +1,70 @@
// vim: set tw=80 ts=2 sw=2 sts=2 :
import { Component, OnInit, OnChanges, Input, Output, EventEmitter } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Login } from './login';
@Component({
selector: 'login-form',
exportAs: 'loginForm',
template: `
<form novalidate (keyup.enter)="submit()" [formGroup]="form">
<div class="form-group row">
<label for="email" class="col-sm-4 control-label">Adresse email</label>
<div class="col-sm-8"
[class.has-danger]="email.errors">
<input type="text" class="form-control" id="email"
formControlName="email" placeholder="Nom d'utilisateur">
<div class="help-block text-danger" *ngIf="email.errors">
<p *ngIf="email.errors.required">The email is required.</p>
</div>
</div>
</div>
<div class="form-group row">
<label for="password" class="col-sm-4 control-label">Mot de passe</label>
<div class="col-sm-8"
[class.has-danger]="password.errors">
<input type="password" class="form-control" id="password"
formControlName="password" placeholder="Mot de passe">
<div class="help-block text-danger" *ngIf="password.errors">
<p *ngIf="password.errors.required">The password is required.</p>
</div>
</div>
</div>
</form>
`
})
export class LoginFormComponent {
public form: FormGroup;
@Input('login-form') private login: Login
@Output('submit') submitEventEmitter: EventEmitter<void> = new EventEmitter<void>();
constructor(private formBuilder : FormBuilder) {}
ngOnInit() {
this.form = this.formBuilder.group({
email: ['', Validators.required],
password: ['', Validators.required]
})
}
submit() {
if(this.form.valid) {
this.submitEventEmitter.emit();
}
}
get email() {
return this.form.get('email');
}
get password() {
return this.form.get('password');
}
}

View File

@ -0,0 +1,49 @@
// vim: set tw=80 ts=2 sw=2 sts=2:
import { Component, Input, ViewChild } from '@angular/core';
import { MzBaseModal } from 'ng2-materialize';
import { Login } from './login';
import { LoginFormComponent } from './loginForm.component';
@Component({
selector: 'login-modal',
template: `
<mz-modal>
<mz-modal-header>
Authentification requise
</mz-modal-header>
<mz-modal-content>
<login-form (submit)="submit()" #loginForm="loginForm"></login-form>
</mz-modal-content>
<mz-modal-footer>
<button class="btn btn-primary" [disabled]="!loginForm.form.valid" (click)="submit()">
Login
</button>
<button class="btn btn-default" (click)="cancel()">
Cancel
</button>
</mz-modal-footer>
</mz-modal>
`
})
export class LoginModalComponent extends MzBaseModal {
@ViewChild('loginForm') loginForm: LoginFormComponent;
//submit(): void {
// let formModel = this.loginForm.form.value;
// let login: Login = new Login();
// login.email = formModel.email;
// login.password = formModel.password;
// this.activeModal.close(login);
//}
//cancel(): void {
// this.activeModal.dismiss("closed");
//}
}

6
src/login/token.ts Normal file
View File

@ -0,0 +1,6 @@
// vim: set tw=80 ts=2 sw=2 sts=2 :
export class Token {
access_token: string;
refresh_token: string;
}

25
src/main.scss Normal file
View File

@ -0,0 +1,25 @@
$fa-font-path: '~font-awesome/fonts';
@import '~font-awesome/scss/font-awesome';
@import '~materialize-css/sass/materialize.scss';
@import '~c3/c3';
@import '~ngx-toastr/toastr';
.italic {
font-style: italic;
}
.stroke {
text-decoration: line-through;
}
.c3-ygrid-line.zeroline line {
stroke: orange;
}
.c3-ygrid-line.overdraft line {
stroke: #FF0000;
}

7
src/main.ts Normal file
View File

@ -0,0 +1,7 @@
// vim: set tw=80 ts=2 sw=2 sts=2 :
import { AppModule } from './app.module';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
platformBrowserDynamic().bootstrapModule(AppModule);

View File

@ -0,0 +1,177 @@
// vim: set tw=80 ts=2 sw=2 sts=2:
import * as moment from 'moment';
import * as c3 from 'c3';
import {
Component, ElementRef,
Inject, Input, Output, EventEmitter,
OnInit, OnChanges
} from '@angular/core';
import { Account } from '../accounts/account';
import { DailyBalanceService } from '../accounts/dailyBalance.service';
class DateRange {
minDate: Date;
maxDate: Date;
}
@Component({
selector: 'balance-chart',
template: '<div></div>'
})
export class BalanceChartComponent implements OnInit, OnChanges {
@Input() account: Account;
@Output() onUpdate: EventEmitter<DateRange> = new EventEmitter<DateRange>();
private chart: c3.ChartAPI;
private balances: number[];
constructor(
private elementRef: ElementRef,
private dailyBalanceService: DailyBalanceService,
) {
}
loadData(account: Account) {
this.dailyBalanceService.query(
account.id
).subscribe((results) => {
var headers: any[][] = [['date', 'balances', 'expenses', 'revenues']];
var rows = results.map(function(result) {
return [
result.operation_date,
result.balance,
result.expenses,
result.revenues
];
});
this.chart.unload();
this.chart.load({
rows: headers.concat(rows)
});
var x: any;
x = this.chart.x();
var balances = x.balances;
this.onUpdate.emit({
minDate: balances[0],
maxDate: balances[balances.length - 1]
});
});
};
ngOnInit() {
var tomorrow = moment().endOf('day').valueOf();
this.chart = c3.generate({
bindto: this.elementRef.nativeElement.children[0],
size: {
height: 450,
},
data: {
x: 'date',
rows: [],
axes: {
expenses: 'y2',
revenues: 'y2'
},
type: 'bar',
types: {
balances: 'area'
},
groups: [
['expenses', 'revenues']
],
// Disable for the moment because there is an issue when
// using subchart line is not refreshed after subset
// selection.
//regions: {
// balances: [{
// start: tomorrow,
// style: 'dashed'
// }]
//}
},
regions: [{
start: tomorrow,
}],
axis: {
x: {
type: 'timeseries',
tick: {
format: '%Y-%m-%d',
rotate: 50,
}
},
y: {
label: {
text: 'Amount',
position: 'outer-middle'
}
},
y2: {
show: true,
label: {
text: 'Amount',
position: 'outer-middle'
}
}
},
grid: {
x: {
show: true,
},
y: {
show: true,
}
},
tooltip: {
format: {
value: function(value, ratio, id, index) {
return value + '€';
}
}
},
subchart: {
show: true,
onbrush: (domain) => {
this.onUpdate.emit({minDate: domain[0], maxDate: domain[1]});
}
}
});
};
setLines(account: Account) {
if(this.chart) {
this.chart.ygrids([
{ value: 0, axis: 'y2' },
{ value: 0, axis: 'y', class: 'zeroline'},
]);
if(account) {
this.chart.ygrids.add({
value: account.authorized_overdraft,
axis: 'y',
class: 'overdraft'
});
}
}
};
ngOnChanges(changes) {
if('account' in changes && changes.account.currentValue) {
this.loadData(changes.account.currentValue);
this.setLines(changes.account.currentValue);
} else {
this.setLines(this.account);
}
};
}

View File

@ -0,0 +1,39 @@
// vim: set tw=80 ts=2 sw=2 sts=2:
import * as moment from 'moment';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Rx';
import { HttpClient, HttpParams } from "@angular/common/http";
import { Category } from './category';
@Injectable()
export class CategoryService {
constructor(
private http: HttpClient
) {}
formatDate(date: Date|string) {
if(date instanceof Date) {
return moment(date).format('YYYY-MM-DD');
}
return date;
}
query(id: number, minDate: Date = null, maxDate: Date = null): Observable<Category[]> {
let params: HttpParams = new HttpParams();
if(minDate) {
params = params.set('begin', this.formatDate(minDate));
}
if(maxDate) {
params = params.set('end', this.formatDate(maxDate));
}
return this.http.get<Category[]>(`/api/account/${id}/category`, { params: params});
}
}

View File

@ -0,0 +1,7 @@
// vim: set tw=80 ts=2 sw=2 sts=2 :
export class Category {
category: string;
expenses: number;
revenues: number;
}

View File

@ -0,0 +1,107 @@
// vim: set tw=80 ts=2 sw=2 sts=2 :
import * as c3 from 'c3';
import {
Component, ElementRef,
Inject, Input, Output,
OnInit, OnChanges
} from '@angular/core';
import { Account } from '../accounts/account';
import { CategoryService } from './category.service';
@Component({
selector: 'category-chart',
template: '<div></div>'
})
export class CategoryChartComponent implements OnInit, OnChanges {
@Input() minDate: Date;
@Input() maxDate: Date;
@Input() account: Account;
chart: c3.ChartAPI;
constructor(
private elementRef: ElementRef,
private categoryService: CategoryService,
) {}
loadData(account: Account) {
this.categoryService.query(
account.id,
this.minDate,
this.maxDate
).subscribe((results) => {
var expenses=[],
revenues=[],
colors={},
names={};
var revenuesColor = 'green',
expensesColor = 'orange';
for(let result of results) {
if(result.revenues > 0) {
var revenuesName = 'revenues-' + result.category;
revenues.push([revenuesName, result.revenues]);
names[revenuesName] = result.category;
colors[revenuesName] = revenuesColor;
}
if(result.expenses < 0) {
var expensesName = 'expenses-' + result.category;
expenses.splice(0, 0, [expensesName, -result.expenses]);
names[expensesName] = result.category;
colors[expensesName] = expensesColor;
}
};
this.chart.unload();
this.chart.load({
columns: revenues.concat(expenses),
names: names,
colors: colors
});
});
};
ngOnInit() {
this.chart = c3.generate({
bindto: this.elementRef.nativeElement.children[0],
data: {
columns: [],
type: 'donut',
order: null,
},
tooltip: {
format: {
value: function(value, ratio, id, index) {
return value + '€';
}
}
},
donut: {
label: {
format: function(value) {
return value + '€';
}
}
},
legend: {
show: false
}
});
};
ngOnChanges(changes) {
if('account' in changes && changes.account.currentValue) {
this.loadData(changes.account.currentValue);
} else if (this.account) {
this.loadData(this.account);
}
};
}

View File

@ -0,0 +1,59 @@
// vim: set tw=80 ts=2 sw=2 sts=2 :
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ReactiveFormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http';
import { RouterModule } from '@angular/router';
import { NgLoggerModule, Level } from '@nsalaun/ng-logger';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { ToastrModule } from 'ngx-toastr';
import { TextMaskModule } from 'angular2-text-mask';
import { MaterializeModule } from 'ng2-materialize';
import { BalanceChartComponent } from './balanceChart.component';
import { CategoryChartComponent } from './categoryChart.component';
import { OperationRowComponent } from './operationRow.component';
import { CategoryService } from './category.service';
import { OperationService } from './operation.service';
import { OperationListComponent } from './operationList.component';
import { OperationDeleteModalComponent } from './operationDeleteModal.component';
import { OperationFormComponent } from './operationForm.component';
import { OperationEditModalComponent } from './operationEditModal.component'
import { OperationListState } from './operation.states'
@NgModule({
imports: [
HttpClientModule,
CommonModule,
ReactiveFormsModule,
RouterModule.forChild([
OperationListState
]),
NgLoggerModule,
ToastrModule,
NgbModule,
TextMaskModule,
MaterializeModule,
],
providers: [
CategoryService,
OperationService,
],
declarations: [
BalanceChartComponent,
CategoryChartComponent,
OperationRowComponent,
OperationListComponent,
OperationDeleteModalComponent,
OperationFormComponent,
OperationEditModalComponent,
],
entryComponents: [
OperationDeleteModalComponent,
OperationEditModalComponent,
OperationListComponent,
]
})
export class OperationModule {}

View File

@ -0,0 +1,71 @@
// vim: set tw=80 ts=2 sw=2 sts=2 :
import * as moment from 'moment';
import { Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { Observable } from 'rxjs/Rx';
import { Operation } from './operation';
@Injectable()
export class OperationService {
constructor(
private http: HttpClient,
) {}
formatDate(date: Date|string) {
if(date instanceof Date) {
return moment(date).format('YYYY-MM-DD');
}
return date;
}
private url(id?: Number): string {
if(id) {
return `/api/operation/${id}`;
}
return `/api/operation`;
}
query(
accountId: number,
minDate: Date|string = null,
maxDate: Date|string = null
): Observable<Operation[]> {
let params = new HttpParams();
params = params.set('account_id', `${accountId}`);
if(minDate) {
params = params.set('begin', this.formatDate(minDate));
}
if(maxDate) {
params = params.set('end', this.formatDate(maxDate));
}
return this.http.get<Operation[]>(this.url(), {
params: params
});
}
get(id: number): Observable<Operation> {
return this.http.get<Operation>(this.url(id));
}
create(operation: Operation): Observable<Operation> {
return this.http.post<Operation>(this.url(), operation);
}
update(operation: Operation): Observable<Operation> {
return this.http.post<Operation>(this.url(operation.id), operation);
}
delete(operation: Operation): Observable<Operation> {
return this.http.delete<Operation>(this.url(operation.id));
}
}

View File

@ -0,0 +1,8 @@
// vim: set tw=80 ts=2 sw=2 sts=2 :
import { OperationListComponent } from './operationList.component';
export const OperationListState = {
path: 'account/:accountId/operations',
component: OperationListComponent
}

View File

@ -0,0 +1,15 @@
// vim: set tw=80 ts=2 sw=2 sts=2:
export class Operation {
id: number;
operation_date: string;
label: string;
value: number;
category: string;
scheduled_operation_id: number;
account_id: number;
balance: number;
confirmed: boolean;
pointed: boolean;
cancelled: boolean
}

View File

@ -0,0 +1,45 @@
// vim: set tw=80 ts=2 sw=2 sts=2:
import { Component, Input } from '@angular/core';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { Operation } from './operation';
@Component({
selector: 'operation-delete-modal',
template: `
<div class="modal-header">
<h3 class="modal-title" id="modal-title">Delete Operation #{{ operation.id }}</h3>
</div>
<div class="modal-body" id="modal-body">
<p>
Do you really want to delete operation #{{ operation.id }} with label:<br/>
{{ operation.label }}
</p>
</div>
<div class="modal-footer">
<button class="btn btn-danger" (click)="submit()">
Yes
</button>
<button class="btn btn-default" (click)="cancel()">
No
</button>
</div>
`
})
export class OperationDeleteModalComponent {
@Input() operation: Operation
constructor(private activeModal: NgbActiveModal) {}
submit(): void {
this.activeModal.close(this.operation);
}
cancel(): void {
this.activeModal.dismiss("closed");
}
}

View File

@ -0,0 +1,63 @@
// vim: set tw=80 ts=2 sw=2 sts=2:
import { Component, Input, ViewChild } from '@angular/core';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { Operation } from './operation';
import { OperationFormComponent } from './operationForm.component';
@Component({
selector: 'operation-edit-modal',
template: `
<div class="modal-header">
<h3 class="modal-title" id="modal-title">{{ title() }}</h3>
</div>
<div class="modal-body" id="modal-body">
<operation-form [operation]="operation" (submit)="submit()" #operationForm="operationForm"></operation-form>
</div>
<div class="modal-footer">
<button class="btn btn-primary" [disabled]="!operationForm.form.valid" (click)="submit()">
Save
</button>
<button class="btn btn-default" (click)="cancel()">
Cancel
</button>
</div>
`
})
export class OperationEditModalComponent {
@Input() operation: Operation;
@ViewChild('operationForm') operationForm: OperationFormComponent;
valid: boolean = false;
constructor(private activeModal: NgbActiveModal) {}
title(): string {
if(this.operation.id) {
return "Operation #" + this.operation.id;
} else {
return "New operation";
}
}
submit(): void {
let formModel = this.operationForm.form.value;
let operation = Object.assign({}, this.operation);
operation.id = this.operation.id;
operation.operation_date = formModel.operationDate;
operation.label = formModel.label;
operation.value = formModel.value;
operation.category = formModel.category;
this.activeModal.close(operation);
}
cancel(): void {
this.activeModal.dismiss("closed");
}
}

View File

@ -0,0 +1,122 @@
// vim: set tw=80 ts=2 sw=2 sts=2 :
import { Component, OnInit, OnChanges, Input, Output, EventEmitter } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Operation } from './operation';
@Component({
selector: 'operation-form',
exportAs: 'operationForm',
template: `
<form novalidate (keyup.enter)="submit()" [formGroup]="form">
<div class="form-group row">
<label class="col-sm-4 control-label" for="operation-date">Date</label>
<div class="col-sm-8"
[class.has-danger]="operationDate.errors">
<input class="form-control"
id="operation-date" formControlName="operationDate"
[textMask]="{mask: dateMask}"
placeholder="Operation date">
<div class="help-block text-danger" *ngIf="operationDate.errors">
<p *ngIf="operationDate.errors.required">The operation date is required.</p>
</div>
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 control-label" for="label">Label</label>
<div class="col-sm-8"
[class.has-danger]="label.errors">
<input class="form-control"
id="label" formControlName="label"
placeholder="Label">
<div class="help-block text-danger" *ngIf="label.errors">
<p *ngIf="label.errors.required">The operation label is required.</p>
</div>
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 control-label" for="value">Montant</label>
<div class="col-sm-8"
[class.has-errors]="value.errors">
<input class="form-control"
id="value" formControlName="value"
type="number" placeholder="Value">
<div class="help-block text-danger" *ngIf="value.errors">
<p *ngIf="value.errors.required">The operation value is required.</p>
</div>
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 control-label" for="category">Catégorie</label>
<div class="col-sm-8"
[class.has-errors]="category.errors">
<input class="form-control"
id="category" formControlName="category"
placeholder="Category">
<div class="help-block text-danger" *ngIf="category.errors">
<p *ngIf="category.errors.required">The operation category is required.</p>
</div>
</div>
</div>
</form>
`
})
export class OperationFormComponent implements OnInit {
public form: FormGroup;
@Input() operation: Operation;
@Output() submitEventEmitter: EventEmitter<void> = new EventEmitter<void>();
//dateMask = [/\d{4}/, '-', /0[1-9]|1[0-2]/, '-', /[0-2]\d|3[0-1]/];
dateMask = ['2', '0', /\d/, /\d/, '-', /[0-1]/, /\d/, '-', /[0-3]/, /\d/];
constructor(private formBuilder: FormBuilder) {}
ngOnInit() {
this.form = this.formBuilder.group({
operationDate: ['', Validators.required],
label: ['', Validators.required],
value: ['', Validators.required],
category: ['', Validators.required],
});
this.form.patchValue({
operationDate: this.operation.operation_date,
label: this.operation.label,
value: this.operation.value,
category: this.operation.category,
});
}
submit() {
if(this.form.valid) {
this.submitEventEmitter.emit();
}
}
get operationDate() {
return this.form.get('operationDate');
}
get label() {
return this.form.get('label');
}
get value() {
return this.form.get('value');
}
get category() {
return this.form.get('category');
}
}

View File

@ -0,0 +1,152 @@
// vim: set tw=80 ts=2 sw=2 sts=2 :
import { Component, Inject, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Observable } from 'rxjs/Rx';
import { Logger } from '@nsalaun/ng-logger';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { ToastrService } from 'ngx-toastr';
import { Account } from '../accounts/account';
import { AccountService } from '../accounts/account.service';
import { Operation } from './operation';
import { OperationService } from './operation.service';
import { OperationEditModalComponent } from './operationEditModal.component';
@Component({
selector: 'operation-list',
template: `
<div>
<div class="row">
<div class="col s9">
<balance-chart (onUpdate)="onUpdate($event)"
[account]="account"></balance-chart>
</div>
<div class="col s3">
<category-chart
[minDate]="minDate"
[maxDate]="maxDate"
[account]="account"></category-chart>
</div>
</div>
<div class="row">
<div class="col s12">
<table class="bordered highlight responsive-table">
<thead>
<tr>
<th>#</th>
<th>Date d'op.</th>
<th>Libell&eacute; de l'op&eacute;ration</th>
<th>Montant</th>
<th>Solde</th>
<th>Cat&eacute;gorie</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr>
<td colspan="7">
<button mz-button class="green" (click)="add()">
Ajouter
</button>
</td>
</tr>
<tr [operation-row]="operation"
[account]="account"
(needsReload)="load(minDate, maxDate)"
*ngFor="let operation of operations">
</tr>
</tbody>
</table>
</div>
</div>
</div>
`
})
export class OperationListComponent implements OnInit {
private account: Account;
private minDate: Date;
private maxDate: Date;
private operations: Operation[];
constructor(
private toastrService: ToastrService,
private operationService: OperationService,
private accountService: AccountService,
private logger: Logger,
private ngbModal: NgbModal,
private route: ActivatedRoute
) {}
ngOnInit() {
this.accountService.get(
+this.route.snapshot.paramMap.get('accountId')
).subscribe(account => {
this.account = account
});
}
/*
* Add an empty operation.
*/
add() {
var operation = new Operation();
operation.account_id = this.account.id;
// FIXME Alexis Lahouze 2017-06-15 i18n
const modal = this.ngbModal.open(OperationEditModalComponent, {
size: 'lg'
});
modal.componentInstance.operation = operation;
modal.result.then((operation: Operation) => {
this.save(operation);
}, (reason) => {
});
};
/*
* Load operations.
*/
load(minDate, maxDate) {
this.minDate = minDate;
this.maxDate = maxDate;
return this.operationService.query(
this.account.id,
minDate,
maxDate
).subscribe((operations: Operation[]) => {
this.operations = operations.reverse();
});
};
/*
* Save an operation and return a promise.
*/
save(operation) {
operation.confirmed = true;
return this.operationService.create(operation).subscribe(
(operation) => {
this.toastrService.success('Operation #' + operation.id + ' saved.');
this.load(this.minDate, this.maxDate);
}, (result) => {
this.toastrService.error(
'Error while saving operation: ' + result.message
);
}
);
};
onUpdate(dateRange) {
this.load(dateRange.minDate, dateRange.maxDate);
};
};

View File

@ -0,0 +1,153 @@
// vim: set tw=80 ts=2 sw=2 sts=2 :
import { CurrencyPipe } from '@angular/common';
import { Component, Inject, Input, Output, EventEmitter } from '@angular/core';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { ToastrService } from 'ngx-toastr';
import { Account } from '../accounts/account';
import { Operation } from './operation';
import { OperationService } from './operation.service';
import { OperationDeleteModalComponent } from './operationDeleteModal.component';
import { OperationEditModalComponent } from './operationEditModal.component';
@Component({
selector: 'tr[operation-row]',
host: {
"[id]": "operation.id",
"[class.stroke]": "operation.canceled",
"[class.italic]": "!operation.confirmed",
"class": "lighten-5",
"[class.orange]": "account.authorized_overdraft < 0 && operation.balance < 0",
"[class.red]": "operation.balance < account.authorized_overdraft"
},
template: `
<td>{{ operation.id }}</td>
<td>{{ operation.operation_date | date:"yyyy-MM-dd" }}</td>
<td>{{ operation.label }}</td>
<td>{{ operation.value | currency:'EUR':true }}</td>
<td class="test-lighten-2"
[class.orange-text]="account.authorized_overdraft < 0 && operation.balance < 0"
[class.red-text]="operation.balance < account.authorized_overdraft">
{{ operation.balance | currency:'EUR':true }}
</td>
<td>{{ operation.category }}</td>
<td>
<!-- Edit operation, for non-canceled operation. -->
<button mz-button [float]="true" class="green"
*ngIf="!operation.canceled"
(click)="modify(operation)" title="edit">
<span class="fa fa-pencil-square-o"></span>
</button>
<!-- Toggle pointed operation, for non-canceled operations. -->
<button mz-button [float]="true" class="blue"
*ngIf="!operation.canceled"
(click)="togglePointed(operation)"
[class.active]="operation.pointed" title="point">
<span class="fa" [class.fa-check-square-o]="operation.pointed"
[class.fa-square-o]="!operation.pointed"></span>
</button>
<!-- Toggle canceled operation. -->
<button mz-button [float]="true" class="orange"
(click)="toggleCanceled(operation)"
*ngIf="operation.scheduled_operation_id"
[class.active]="operation.canceled" title="cancel">
<span class="fa fa-remove"></span>
</button>
<!-- Delete operation, with confirm. -->
<button mz-button [float]="true" class="red"
(click)="confirmDelete(operation)"
*ngIf="operation.id && !operation.scheduled_operation_id">
<span class="fa fa-trash-o"></span>
</button>
</td>
`
})
export class OperationRowComponent {
@Input('operation-row') operation: Operation;
@Input() account: Account;
@Output() needsReload: EventEmitter<void> = new EventEmitter<void>();
constructor(
private operationService: OperationService,
private toastrService: ToastrService,
private ngbModal: NgbModal,
) {}
togglePointed(operation, rowform) {
operation.pointed = !operation.pointed;
this.save(operation);
};
toggleCanceled(operation) {
operation.canceled = !operation.canceled;
this.save(operation);
};
save(operation) {
operation.confirmed = true;
return this.operationService.update(operation).subscribe((operation) => {
this.toastrService.success('Operation #' + operation.id + ' saved.');
this.needsReload.emit();
}, (result) => {
this.toastrService.error(
'Error while saving operation: ' + result.message
);
});
}
confirmDelete(operation) {
const modal = this.ngbModal.open(OperationDeleteModalComponent);
modal.componentInstance.operation = this.operation;
var id = operation.id;
modal.result.then((operation: Operation) => {
this.delete(operation);
}, (reason) => {
})
};
delete(operation) {
var id = operation.id;
return this.operationService.delete(operation).subscribe(() => {
this.toastrService.success('Operation #' + id + ' deleted.');
this.needsReload.emit();
}, (result) => {
this.toastrService.error(
'An error occurred while trying to delete operation #' +
id + ':<br />' + result
);
});
};
modify(operation) {
// FIXME Alexis Lahouze 2017-06-15 i18n
const modal = this.ngbModal.open(OperationEditModalComponent, {
size: 'lg'
});
modal.componentInstance.operation = operation;
modal.result.then((operation: Operation) => {
this.save(operation);
}, (reason) => {
});
};
}

View File

@ -0,0 +1,53 @@
// vim: set tw=80 ts=2 sw=2 sts=2 :
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ReactiveFormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http';
import { RouterModule } from '@angular/router';
import { NgLoggerModule, Level } from '@nsalaun/ng-logger';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { ToastrModule } from 'ngx-toastr';
import { TextMaskModule } from 'angular2-text-mask';
import { MaterializeModule } from 'ng2-materialize';
import { ScheduleService } from './schedule.service';
import { ScheduleDeleteModalComponent } from './scheduleDeleteModal.component';
import { ScheduleEditModalComponent } from './scheduleEditModal.component';
import { ScheduleFormComponent } from './scheduleForm.component';
import { ScheduleRowComponent } from './scheduleRow.component';
import { ScheduleListComponent } from './scheduleList.component';
import { ScheduleListState } from './schedule.states';
@NgModule({
imports: [
HttpClientModule,
CommonModule,
ReactiveFormsModule,
RouterModule.forChild([
ScheduleListState
]),
NgLoggerModule,
ToastrModule,
NgbModule,
TextMaskModule,
MaterializeModule,
],
providers: [
ScheduleService,
],
declarations: [
ScheduleDeleteModalComponent,
ScheduleEditModalComponent,
ScheduleFormComponent,
ScheduleListComponent,
ScheduleRowComponent
],
entryComponents: [
ScheduleDeleteModalComponent,
ScheduleEditModalComponent,
ScheduleListComponent,
]
})
export class ScheduleModule {}

View File

@ -0,0 +1,47 @@
// vim: set tw=80 ts=2 sw=2 sts=2 :
import { Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { Observable } from 'rxjs/Rx';
import { Schedule } from './schedule';
@Injectable()
export class ScheduleService {
constructor(
private http: HttpClient
) {}
private url(id?: number): string {
if(id) {
return `/api/scheduled_operation/${id}`;
}
return `/api/scheduled_operation`;
}
query(accountId: number): Observable<Schedule[]> {
let params = new HttpParams().set('account_id', `${accountId}`);
return this.http.get<Schedule[]>(this.url(), { params: params });
}
get(accountId: number, id: number): Observable<Schedule> {
let params = new HttpParams().set('account_id', `${accountId}`);
return this.http.get<Schedule>(this.url(id), { params: params });
}
create(schedule: Schedule): Observable<Schedule> {
return this.http.post<Schedule>(this.url(), schedule);
}
update(schedule: Schedule): Observable<Schedule> {
return this.http.post<Schedule>(this.url(schedule.id), schedule);
}
delete(schedule: Schedule): Observable<Schedule> {
return this.http.delete<Schedule>(this.url(schedule.id));
}
}

View File

@ -0,0 +1,8 @@
// vim: set tw=80 ts=2 sw=2 sts=2 :
import { ScheduleListComponent } from './scheduleList.component';
export const ScheduleListState = {
path: 'account/:accountId/scheduler',
component: ScheduleListComponent,
}

13
src/scheduler/schedule.ts Normal file
View File

@ -0,0 +1,13 @@
// vim: set tw=80 ts=2 sw=2 sts=2:
export class Schedule {
id: number;
start_date: string;
stop_date: string;
day: number;
frequency: number;
label: string;
value: number;
category: string;
account_id: number
}

View File

@ -0,0 +1,49 @@
// vim: set tw=80 ts=2 sw=2 sts=2:
import { Component, Input } from '@angular/core';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { Schedule } from './schedule';
@Component({
selector: 'schedule-delete-modal',
template: `
<div class="modal-header">
<h3 class="modal-title" id="modal-title">{{ title() }}</h3>
</div>
<div class="modal-body" id="modal-body">
<p>
Do you really want to delete schedule #{{ schedule.id }} with label:<br/>
{{ schedule.label }}
</p>
</div>
<div class="modal-footer">
<button class="btn btn-danger" (click)="submit()">
Yes
</button>
<button class="btn btn-default" (click)="cancel()">
No
</button>
</div>
`
})
export class ScheduleDeleteModalComponent {
@Input() schedule: Schedule
constructor(public activeModal: NgbActiveModal) {}
title(): string {
return "Delete schedule #" + this.schedule.id;
}
submit(): void {
this.activeModal.close(this.schedule);
}
cancel(): void {
this.activeModal.dismiss("closed");
}
}

View File

@ -0,0 +1,66 @@
// vim: set tw=80 ts=2 sw=2 sts=2:
import { Component, Input, ViewChild } from '@angular/core';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { Schedule } from './schedule';
import { ScheduleFormComponent } from './scheduleForm.component';
@Component({
selector: 'schedule-edit-modal',
template: `
<div class="modal-header">
<h3 class="modal-title" id="modal-title">{{ title() }}</h3>
</div>
<div class="modal-body" id="modal-body">
<schedule-form [schedule]="schedule" (submit)="submit()" #scheduleForm="scheduleForm"></schedule-form>
</div>
<div class="modal-footer">
<button class="btn btn-primary" [disabled]="!scheduleForm.form.valid" (click)="submit()">
Save
</button>
<button class="btn btn-default" (click)="cancel()">
Cancel
</button>
</div>
`
})
export class ScheduleEditModalComponent {
@Input() schedule: Schedule;
@ViewChild('scheduleForm') scheduleForm: ScheduleFormComponent;
valid: boolean = false;
constructor(private activeModal: NgbActiveModal) {}
title(): string {
if(this.schedule.id) {
return "Schedule #" + this.schedule.id;
} else {
return "New schedule";
}
}
submit(): void {
let formModel = this.scheduleForm.form.value;
let schedule = Object.assign({}, this.schedule);
schedule.id = this.schedule.id;
schedule.start_date = formModel.startDate;
schedule.stop_date = formModel.stopDate;
schedule.day = formModel.day;
schedule.frequency = formModel.frequency;
schedule.label = formModel.label;
schedule.value = formModel.value;
schedule.category = formModel.category;
this.activeModal.close(schedule);
}
cancel(): void {
this.activeModal.dismiss("closed");
}
}

View File

@ -0,0 +1,189 @@
// vim: set tw=80 ts=2 sw=2 sts=2 :
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Schedule } from './schedule';
@Component({
selector: 'schedule-form',
exportAs: 'scheduleForm',
template: `
<form novalidate (keyup.enter)="submit()" [formGroup]="form">
<div class="form-group row">
<label class="col-sm-4 control-label" for="start-date">Date de début (AAAA-MM-JJ)</label>
<div class="col-sm-8"
[class.has-danger]="startDate.errors">
<input class="form-control"
id="start-date" formControlName="startDate"
[textMask]="{mask: dateMask}"
placeholder="Schedule start date">
<div class="help-block text-danger" *ngIf="startDate.errors">
<p *ngIf="startDate.errors.required">The start date is required.</p>
</div>
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 control-label" for="stop-date">Date de fin (AAAA-MM-JJ)</label>
<div class="col-sm-8"
[class.has-danger]="stopDate.errors">
<input class="form-control"
id="stop-date" formControlName="stopDate"
[textMask]="{mask: dateMask}"
placeholder="Schedule stop date">
<div class="help-block text-danger" *ngIf="stopDate.errors">
<p *ngIf="stopDate.errors.required">The stop date is required.</p>
</div>
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 control-label" for="day">Jour</label>
<div class="col-sm-8"
[class.has-danger]="day.errors">
<input class="form-control"
id="day" formControlName="day"
type="number" placeholder="Day">
<div class="help-block text-danger" *ngIf="day.errors">
<p *ngIf="day.errors.required">The day is required.</p>
<p *ngIf="day.errors.min">The day must be greater than 0.</p>
<p *ngIf="day.errors.max">The day must be less than or equal to 31.</p>
</div>
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 control-label" for="frequency">Fréquence</label>
<div class="col-sm-8"
[class.has-danger]="frequency.errors">
<input class="form-control"
id="frequency" formControlName="frequency"
type="number" placeholder="Frequency">
<div class="help-block text-danger" *ngIf="frequency.errors">
<p *ngIf="frequency.errors.required">The frequency is required.</p>
<p *ngIf="frequency.errors.min">The frequency must be positive.</p>
</div>
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 control-label" for="label">Label</label>
<div class="col-sm-8"
[class.has-danger]="label.errors">
<input class="form-control"
id="label" formControlName="label"
placeholder="Label">
<div class="help-block text-danger" *ngIf="label.errors">
<p *ngIf="label.errors.required">The label is required.</p>
</div>
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 control-label" for="value">Montant</label>
<div class="col-sm-8"
[class.has-danger]="value.errors">
<input class="form-control"
id="value" formControlName="value"
type="number" placeholder="Value">
<div class="help-block text-danger" *ngIf="value.errors">
<p *ngIf="value.errors.required">The value is required.</p>
</div>
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 control-label" for="category">Catégorie</label>
<div class="col-sm-8"
[class.has-danger]="category.errors">
<input class="form-control"
id="category" formControlName="category"
placeholder="Category">
<div class="help-block text-danger" *ngIf="category.errors">
<p *ngIf="category.errors.required">The category is required.</p>
</div>
</div>
</div>
</form>
`
})
export class ScheduleFormComponent implements OnInit {
public form: FormGroup;
@Input() schedule: Schedule;
@Output('submit') submitEventEmitter: EventEmitter<void> = new EventEmitter<void>();
//dateMask = [/\d{4}/, '-', /0[1-9]|1[0-2]/, '-', /[0-2]\d|3[0-1]/];
dateMask = ['2', '0', /\d/, /\d/, '-', /[0-1]/, /\d/, '-', /[0-3]/, /\d/];
constructor(private formBuilder: FormBuilder) {}
ngOnInit() {
this.form = this.formBuilder.group({
startDate: ['', Validators.required],
stopDate: ['', Validators.required],
day: ['', [Validators.required, Validators.min(1), Validators.max(31)]],
frequency: ['', [Validators.required, Validators.min(0)]],
label: ['', Validators.required],
value: ['', Validators.required],
category: ['', Validators.required],
});
this.form.patchValue({
startDate: this.schedule.start_date,
stopDate: this.schedule.stop_date,
day: this.schedule.day,
frequency: this.schedule.frequency,
label: this.schedule.label,
value: this.schedule.value,
category: this.schedule.category,
});
}
submit() {
if(this.form.valid) {
this.submitEventEmitter.emit();
}
}
get startDate() {
return this.form.get('startDate');
}
get stopDate() {
return this.form.get('stopDate');
}
get day() {
return this.form.get('day');
}
get frequency() {
return this.form.get('frequency');
}
get label() {
return this.form.get('label');
}
get value() {
return this.form.get('value');
}
get category() {
return this.form.get('category');
}
}

View File

@ -0,0 +1,115 @@
// vim: set tw=80 ts=2 sw=2 sts=2 :
import { Component, Inject, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Observable } from 'rxjs/Rx';
import { Logger } from '@nsalaun/ng-logger';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { ToastrService } from 'ngx-toastr';
import { ScheduleEditModalComponent } from './scheduleEditModal.component';
import { ScheduleService } from './schedule.service';
import { Schedule } from './schedule';
@Component({
selector: 'schedule-list',
template: `
<div class="row">
<table class="bordered highlight responsive-table">
<thead>
<tr>
<th>Date de d&eacute;but</th>
<th>Date de fin</th>
<th>Jour</th>
<th>Fr&eacute;q.</th>
<th>Libell&eacute; de l'op&eacute;ration</th>
<th>Montant</th>
<th>Cat&eacute;gorie</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr>
<td colspan="8">
<button mz-button class="green" (click)="add()">
Ajouter
</button>
</td>
</tr>
<tr *ngFor="let schedule of schedules"
[schedule-row]="schedule" (needsReload)="load()">
</tr>
</tbody>
</table>
</div>
`
})
export class ScheduleListComponent implements OnInit {
accountId: number;
schedules = [];
constructor(
private toastrService: ToastrService,
private scheduleService: ScheduleService,
private logger: Logger,
private ngbModal: NgbModal,
private route: ActivatedRoute,
) {}
ngOnInit() {
this.logger.log("ngOnInit");
this.accountId = +this.route.snapshot.paramMap.get('accountId')
// Load operations on controller initialization.
this.load();
}
/*
* Add a new operation at the beginning of th array.
*/
add() {
var schedule = new Schedule();
schedule.account_id = this.accountId;
const modal = this.ngbModal.open(ScheduleEditModalComponent, {
size: 'lg'
});
modal.componentInstance.schedule = schedule;
modal.result.then((schedule: Schedule) => {
this.save(schedule);
}, (reason) => function(reason) {
});
};
load() {
this.logger.log("Loading schedules for accountId", this.accountId);
if(!this.accountId) {
return;
}
this.scheduleService.query(this.accountId)
.subscribe((schedules: Schedule[]) => {
this.logger.log("Schedules loaded.", schedules);
this.schedules = schedules;
}, (reason) => {
this.logger.log("Got error", reason);
}
);
};
save(schedule: Schedule) {
return this.scheduleService.create(schedule).subscribe((schedule: Schedule) => {
this.toastrService.success('Schedule #' + schedule.id + ' saved.');
this.load();
}, (result) => {
this.toastrService.error(
'Error while saving schedule: ' + result.message
);
});
};
};

View File

@ -0,0 +1,113 @@
// vim: set tw=80 ts=2 sw=2 sts=2 :
import { CurrencyPipe } from '@angular/common';
import { Component, Inject, Input, Output, EventEmitter } from '@angular/core';
import { Logger } from '@nsalaun/ng-logger';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { ToastrService } from 'ngx-toastr';
import { ScheduleDeleteModalComponent } from './scheduleDeleteModal.component';
import { ScheduleEditModalComponent } from './scheduleEditModal.component';
import { ScheduleService } from './schedule.service';
import { Schedule } from './schedule';
@Component({
selector: 'tr[schedule-row]',
host: {
"[id]": "schedule.id",
},
template: `
<td>{{ schedule.start_date | date: "yyyy-MM-dd" }}</td>
<td>{{ schedule.stop_date | date: "yyyy-MM-dd" }}</td>
<td>{{ schedule.day }}</td>
<td>{{ schedule.frequency }}</td>
<td>{{ schedule.label }}</td>
<td>{{ schedule.value | currency:"EUR":true }}</td>
<td>{{ schedule.category }}</td>
<td>
<!-- Edit operation. -->
<button mz-button [float]="true" class="green"
(click)="modify()" title="edit">
<span class="fa fa-pencil-square-o"></span>
</button>
<!-- Remove operation. -->
<button mz-button [float]="true" class="red"
[hidden]="!schedule.id"
(click)="confirmDelete()"
title="remove">
<span class="fa fa-trash"></span>
</button>
</td>
`
})
export class ScheduleRowComponent {
@Input('schedule-row') schedule: Schedule;
@Output() needsReload: EventEmitter<void> = new EventEmitter<void>();
constructor(
private scheduleService: ScheduleService,
private logger: Logger,
private toastrService: ToastrService,
private ngbModal: NgbModal
) {}
save(schedule: Schedule) {
return this.scheduleService.update(schedule).subscribe((schedule: Schedule) => {
this.toastrService.success('Schedule #' + schedule.id + ' saved.');
this.needsReload.emit();
}, result => {
this.toastrService.error(
'Error while saving schedule: ' + result.message
);
});
}
confirmDelete() {
const modal = this.ngbModal.open(ScheduleDeleteModalComponent);
modal.componentInstance.schedule = this.schedule;
modal.result.then((schedule: Schedule) => {
this.delete(schedule);
}, (reason) => function(reason) {
});
}
delete(schedule: Schedule) {
var id = schedule.id;
return this.scheduleService.delete(schedule).subscribe(() => {
this.toastrService.success('Schedule #' + id + ' deleted.');
this.needsReload.emit();
}, result => {
this.toastrService.error(
'An error occurred while trying to delete schedule #' + id + ':<br />'
+ result.message
);
});
}
modify() {
const modal = this.ngbModal.open(ScheduleEditModalComponent, {
size: 'lg'
});
modal.componentInstance.schedule = this.schedule;
modal.result.then((schedule: Schedule) => {
this.save(schedule);
}, (reason) => function(reason) {
});
}
}

17
tsconfig.json Normal file
View File

@ -0,0 +1,17 @@
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"moduleResolution": "node",
"sourceMap": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"lib": [ "es2015", "dom" ],
"noImplicitAny": false,
"suppressImplicitAnyIndexErrors": true,
"types": [
"jquery",
"materialize-css"
]
}
}

162
webpack.config.js Normal file
View File

@ -0,0 +1,162 @@
/* jshint esversion: 6 */
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack');
module.exports = {
context: path.resolve(__dirname, 'src'),
entry: {
main: [
'./main.ts'
],
styles: [
'./main.scss'
],
vendor: [
'jquery',
'materialize-css'
]
},
devtool: 'source-map',
resolve: {
extensions: ['.js', '.ts', '.html'],
},
module: {
rules: [{
enforce: 'pre',
test: /webpack\.config\.js$/,
include: path.resolve(__dirname),
loader: 'eslint-loader',
options: {
useEslintrc: false,
emitWarning: true,
emitError: true,
failOnWarning: true,
failOnError: true,
baseConfig: 'webpack',
rules: {
indent: ['error', 4]
},
},
}, {
// Javascript
enforce: 'pre',
test: /\.js$/,
//include: path.resolve(__dirname, 'src'),
loader: 'eslint-loader',
options: {
useEslintrc: false,
emitWarning: false,
emitError: true,
failOnWarning: false,
failOnError: true,
baseConfig: 'angular',
rules: {
indent: ['error', 4]
},
plugins: [
'angular',
'html',
'security',
'this',
'jquery',
'promise'
]
},
}, {
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader'
}, {
// Typescript linting
enforce: 'pre',
test: /\.ts$/,
loader: 'tslint-loader',
options: {
configuration: {
extends: [
"tslint:latest",
"codelyzer"
],
rules: {
//quotemark: [true, 'single']
}
},
configFile: 'tslint-custom.json',
emitErrors: true,
failOnHint: true,
typeCheck: true,
tsConfigFile: 'tsconfig.json',
formatter: 'verbose',
formattersDirectory: 'node_modules/tslint/lib/formatters/',
}
}, {
test: /\.ts$/,
exclude: /node_modules/,
loaders: ['awesome-typescript-loader', 'angular2-template-loader?keepUrl=true']
}, {
test: /\.html$/,
loader: 'raw-loader'
}, {
test: /\.css$/,
loaders: [
'style-loader',
'css-loader',
]
}, {
test: /\.scss$/,
loaders: [
'style-loader',
'css-loader',
'sass-loader',
'resolve-url-loader',
'sass-loader?sourceMap'
]
}, {
test: /\.(png|woff|woff2|eot|ttf|svg)$/,
loader: 'url-loader?limit=100000'
}]
},
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: "vendor"
}),
new webpack.ProvidePlugin({
$: "jquery",
jQuery: "jquery",
"window.jQuery": "jquery",
"window.Materialize": "materialize-css",
"Materialize": "materialize-css"
}),
new webpack.ContextReplacementPlugin(
/angular(\\|\/)core(\\|\/)@angular/,
path.resolve(__dirname, './')
),
new HtmlWebpackPlugin({
title: 'Accountant',
template: 'index.ejs',
hash: false,
inject: true,
compile: true,
minify: false,
chunks: 'all'
})
],
output: {
path: path.resolve(__dirname, 'build'),
filename: '[name].bundle.js',
chunkFilename: '[name].chunk.js'
//publicPath: 'js'
},
devServer: {
proxy: {
'/api': {
target: 'http://localhost:5000',
secure: false
}
},
hot: true,
noInfo: false,
quiet: false,
}
};