261 Commits

Author SHA1 Message Date
a41811356a Remove require. 2017-10-28 22:18:29 +02:00
73a43b6e2a Migrate schedule edit form to Material. 2017-10-28 22:16:55 +02:00
2287ca8ad3 Fix and cleanup account edit dialog. 2017-10-28 22:16:55 +02:00
877315d6e1 Migrate schedule edit modal to Material. 2017-10-28 22:16:55 +02:00
d5160a55fb Remove unused shedule row component. 2017-10-28 22:16:55 +02:00
73de67db73 Migrate schedule delete dialog to Material. 2017-10-28 22:16:55 +02:00
f632722916 Migrate schedule list to material. 2017-10-28 22:16:55 +02:00
048a2a7d08 Add Schedule DataSource. 2017-10-28 22:16:55 +02:00
6a93eac767 Migrate to material. 2017-10-28 22:16:55 +02:00
2f5538fc08 Upgrades. 2017-10-28 22:16:55 +02:00
4e21327d49 Fix design. 2017-10-28 22:13:54 +02:00
82670ce86b Finish migration of account list to material. 2017-10-28 22:13:54 +02:00
3c6ebea602 Upgrades. 2017-10-28 22:13:54 +02:00
3088ccc748 Disable theme customization. 2017-10-28 22:12:06 +02:00
2a07506005 Finish migration of account edit form to material. 2017-10-28 22:12:06 +02:00
b6d710ec67 Begin to migrato to angular/material. 2017-10-28 22:12:06 +02:00
ca9bccaf92 Try to improve global page template. 2017-10-28 22:12:06 +02:00
7067366ac1 Improve style. 2017-10-28 22:12:06 +02:00
c63c9c0b24 Add roboto font face. 2017-10-28 22:12:06 +02:00
30a1a69451 Add material theme in style. 2017-10-28 22:09:48 +02:00
a84a1f1c28 Migrate login module to material. 2017-10-28 22:09:48 +02:00
2236f317c6 Migrate app module and component to material. 2017-10-28 22:09:48 +02:00
93563aceae Install flex. 2017-10-28 22:09:48 +02:00
6800b1ee2d Improve webpack configuration. 2017-10-28 22:04:12 +02:00
04433bed3e Add TS linting. 2017-10-28 22:04:12 +02:00
3d5b3ae14b Fix SASS loading. 2017-10-28 22:02:26 +02:00
c620a92250 Upgrades. 2017-10-28 21:51:39 +02:00
427272514b Fix webpack.config.js. 2017-10-09 08:48:55 +02:00
936c5caa5b Upgrades. 2017-10-09 08:42:24 +02:00
648b18eb6a Add ajs-loader dependency. 2017-10-09 08:28:23 +02:00
8f87c43f8a Improve index.ejs. 2017-10-09 08:27:01 +02:00
3053444ca2 Remove uneeded require. 2017-10-09 08:26:09 +02:00
c53f917374 Improve webpack configuration. 2017-10-09 08:22:47 +02:00
1a5f886810 Upgrades 2017-10-04 20:45:55 +02:00
4a2d0e2619 Upgrades 2017-10-04 20:16:32 +02:00
ba50bf815f Upgrades. 2017-08-27 17:29:28 +02:00
9805e8b2e6 Merge branch 'feature/angular2' into develop 2017-08-27 17:25:47 +02:00
a485baf7d2 Switch styles to SASS. 2017-08-13 15:14:12 +02:00
332495411e Upgrade to bootstrap v4-beta. 2017-08-13 15:13:26 +02:00
92a4ca97eb Upgrade dependencies. 2017-08-13 14:22:18 +02:00
bcd5363ff6 Fix password required message. 2017-08-13 14:19:01 +02:00
c3e785122b Use copy instead of new instance. 2017-08-13 14:17:05 +02:00
13670f0317 Migrate to reactive forms in operation module. 2017-08-13 12:28:10 +02:00
0600c1b653 Switch to reactive forms in schedule module. 2017-08-11 23:15:07 +02:00
1f896e1a40 Cleanup modules. 2017-08-11 22:32:09 +02:00
fd0df50d14 Fix schedule edit modal size. 2017-08-11 22:24:09 +02:00
8973c1fe9a Fix operation edit modal size. 2017-08-11 22:22:23 +02:00
012e5a16fa Fix account edit dialog size. 2017-08-11 22:21:36 +02:00
49716421a1 Use reactive forms for login modal. 2017-08-09 00:12:17 +02:00
5f413f0cad Simplify classes. 2017-08-09 00:11:50 +02:00
985174eec9 Remove unused dependency. 2017-08-09 00:11:35 +02:00
0e69216920 Rename LoginForm class. 2017-08-09 00:11:21 +02:00
5a81547108 Fix modal size. 2017-08-08 23:37:45 +02:00
e23adb4316 Format. 2017-08-08 23:37:21 +02:00
061cda89d0 Fix submit on enter. 2017-08-08 23:37:09 +02:00
e68db9f54a Improve Account form and edit modal. 2017-08-04 14:52:37 +02:00
58f1abce21 Implement login mechanism. 2017-08-04 08:32:49 +02:00
16bbc67850 Use Login module in app. 2017-08-01 23:53:57 +02:00
cdbaf58253 Add Login class. 2017-08-01 23:53:27 +02:00
beac7c6eaa Add Login module. 2017-08-01 23:52:34 +02:00
ef4baeed88 Add auth interceptor bootstrap. 2017-08-01 23:51:52 +02:00
e2a4b0b7ec Upgrade Login service. 2017-08-01 23:50:28 +02:00
57256e9ba7 Cleanup old dependencies. 2017-08-01 23:47:51 +02:00
1310807939 Remove unused method. 2017-08-01 23:25:29 +02:00
3e6cadffbd Indent. 2017-08-01 23:22:44 +02:00
a1d71b92d5 Module order in App module. 2017-08-01 23:14:56 +02:00
17754f9a88 Cleanup Angular1 modules. 2017-08-01 23:12:51 +02:00
5726d8bf2e Upgrade routing to Angular2. 2017-08-01 23:09:21 +02:00
2ae8a9cfad Remove Restangular. 2017-07-31 13:17:54 +02:00
0b0c771558 Set url function to private. 2017-07-31 13:16:18 +02:00
dc30f38662 Migrate to HttpClient in Schedule module. 2017-07-31 13:15:45 +02:00
d71656412a Remove unused services. 2017-07-31 13:00:44 +02:00
2d8d39442d Migrate to HttpClient in Account module. 2017-07-31 13:00:17 +02:00
c9e1483206 Migrate to HttpClient in Operation module. 2017-07-31 12:50:59 +02:00
849a7ae95c Style. 2017-07-30 16:51:41 +02:00
38208f6420 Add missing module. 2017-07-30 16:51:20 +02:00
474b7de02d Cleanup schedule module. 2017-07-30 16:49:55 +02:00
19fdb785ba Remove unused template. 2017-07-30 16:49:42 +02:00
3559d4acc0 Remove windowClass in modal open. 2017-07-30 16:44:58 +02:00
efb0f70f31 Modal related stuff. 2017-07-30 16:42:02 +02:00
28460e10ff Remove return statement. 2017-07-30 16:39:11 +02:00
5e964dd8e8 Simplify save function. 2017-07-30 16:38:28 +02:00
25fcc34b68 Merge add ans modify functions in Operation List Component. 2017-07-30 16:36:54 +02:00
73485ac1af Indent. 2017-07-30 16:35:15 +02:00
08a35f0d2c Style. 2017-07-30 16:33:38 +02:00
745eee03c5 Remove unused dependency. 2017-07-30 16:32:39 +02:00
d977286637 Remove unused service. 2017-07-30 16:29:15 +02:00
5b7a4f8aa5 Cleanup dependencies. 2017-07-30 16:27:17 +02:00
6bc53a0cd2 Remove unused dependency. 2017-07-30 16:25:10 +02:00
03f069fc74 Fix delete function in services. 2017-07-30 16:22:44 +02:00
1c18d93d13 Use Operation Edit form to edit operation in Operation Row component. 2017-07-30 16:15:22 +02:00
6da1861139 Use Operation Edit form to add new operation in Operation List component. 2017-07-30 16:11:47 +02:00
f6bcdcfc2b Fix form. 2017-07-30 16:10:45 +02:00
7b368df9b9 Cleanup component. 2017-07-30 16:10:28 +02:00
0782675d19 Fix form template. 2017-07-30 16:04:28 +02:00
7966301651 Add missing modules. 2017-07-30 16:04:05 +02:00
fb477429cc Add Operation Edit Modal component. 2017-07-30 15:55:24 +02:00
3f6d60bb50 Add Operation Form component. 2017-07-30 15:51:52 +02:00
10d8959178 Remove unused factory. 2017-07-30 15:40:20 +02:00
dcd79d085a Remove unused template. 2017-07-30 15:38:38 +02:00
70eb1febff Fix Operation confirmation. 2017-07-30 15:36:57 +02:00
896daa70c5 Cleanup. 2017-07-30 15:36:12 +02:00
a5769dad83 Fix currency. 2017-07-30 15:35:46 +02:00
bc4a69b64c Use Operation Delete Modal in List Component. 2017-07-30 15:35:26 +02:00
162d98add7 Add id in operation list. 2017-07-30 15:34:12 +02:00
b5e4b1cd08 Add Operation Delete Modal component. 2017-07-30 15:22:25 +02:00
072efe7fc3 Rename Operation controller to Operation List component. 2017-07-30 15:16:44 +02:00
8d63b30a32 Remove unused template. 2017-07-30 15:14:02 +02:00
129c9f9ee3 Upgrade Operation controller. 2017-07-30 15:13:36 +02:00
b7c2d94c62 Fix Operation Row component. 2017-07-30 15:12:57 +02:00
558988a57a Add Operation Row component. 2017-07-30 13:22:08 +02:00
2fdce22698 Indent. 2017-07-30 13:15:16 +02:00
c3d6fa97cf Add missing fields in operation. 2017-07-30 13:15:00 +02:00
3eba873eb5 Fix callbacks. 2017-07-30 09:22:14 +02:00
ecf38725e3 Readd missing dependency. 2017-07-29 23:14:00 +02:00
90ecb2bd57 Upgrades. 2017-07-29 23:13:07 +02:00
f7ea8a4621 Prepare needed injection to upgrade Operation Component to Angular2. 2017-07-29 23:06:10 +02:00
45af7791ff Remove unused injection. 2017-07-29 23:05:38 +02:00
f484833380 Remove unused injection. 2017-07-29 23:04:09 +02:00
f374815371 Add typing. 2017-07-29 22:58:34 +02:00
bb21dd700e Use Account Id service instead of $stateParam. 2017-07-29 22:53:41 +02:00
0a5a7e72e3 Add account_id in Operation. 2017-07-29 22:27:25 +02:00
75dc2afc80 Fix Operation query parameters. 2017-07-29 22:26:57 +02:00
8348f5bb8f Use Operation Service in Operation controller. 2017-07-29 22:26:38 +02:00
4c921bfaaa Add minDate and maxDate parameters in Operation query. 2017-07-29 17:59:34 +02:00
595fe60fc4 Add Operation Service. 2017-07-29 17:51:44 +02:00
1d35438444 Rename Category Chart component file. 2017-07-29 17:24:25 +02:00
f58afb37bd Cleanup unused dependency. 2017-07-29 17:22:25 +02:00
20437ba548 Indent and cleanup. 2017-07-29 17:21:45 +02:00
02f447d63c Upgrade Category chart component to Angular2. 2017-07-29 17:18:36 +02:00
1bd59cbbf8 Fix date format in category service. 2017-07-29 17:07:43 +02:00
89fc42c47a Fix date range in category service. 2017-07-29 16:57:05 +02:00
927459b6c5 Remove unused ngResource. 2017-07-29 16:36:16 +02:00
efcf07565b Use categoryService instead of old Categories factory. 2017-07-29 16:34:24 +02:00
a6a7c1cd77 Add category service in Angular2. 2017-07-29 16:33:24 +02:00
2812891b23 Inject account in category chart component instead of using accountIdService. 2017-07-29 16:32:07 +02:00
fa147e74fc Use account directly to load daily balances. 2017-07-29 16:28:59 +02:00
7281d1f11d Use accountIdService to retrieve current Account Id. 2017-07-27 14:31:15 +02:00
eb0d898c19 Fix imports. 2017-07-27 14:30:12 +02:00
13ffa1dc98 Cleanup. 2017-07-27 14:28:28 +02:00
27e3307a8e Fix callback. 2017-07-27 14:21:35 +02:00
2619238996 Add missing component in operation module. 2017-07-27 14:13:13 +02:00
e24d5defb7 Add missinf component in operation module. 2017-07-27 14:12:54 +02:00
99d0eb6d1c Cleanup. 2017-07-27 14:09:09 +02:00
6ca4c29e38 Cleanup, indent. 2017-07-27 14:08:11 +02:00
003b4de822 Upgrade Balance Chart component to Angular2. 2017-07-27 14:03:39 +02:00
863160881f Initialize Operation module. 2017-07-27 13:57:40 +02:00
ac0aa056cf Fix DailyBalanceService query return type. 2017-07-27 13:55:30 +02:00
c0bc7b82b3 Remove unused Balances factory. 2017-07-27 01:07:27 +02:00
64c53441cb Use Daily Balance service in Balance Chart component. 2017-07-27 01:02:02 +02:00
d8adfd91c5 Add Daily Balance service. 2017-07-27 01:00:33 +02:00
c0b236ff6c Use accountIdService to retrieve current Account Id. 2017-07-27 00:41:07 +02:00
2d50ad8a0b Remove uneeded dependency. 2017-07-27 00:35:11 +02:00
e73d105420 Move accountId retrieving into account module. 2017-07-27 00:32:38 +02:00
fab4880389 Add format in date field labels. 2017-07-26 23:12:03 +02:00
b7c44a39da Style. 2017-07-26 23:10:21 +02:00
3363cf682a Fix Schedule field types. 2017-07-26 23:09:55 +02:00
c546cbf833 Add text-mask on date fields. 2017-07-26 23:09:16 +02:00
931ef38f29 Fix button sizes and styles. 2017-07-25 23:33:29 +02:00
0e037e664f Begin migration to bootstrap v4. 2017-07-25 22:12:47 +02:00
b3d274eabf Replace angular-ui-notifications by toastr. 2017-07-25 22:08:18 +02:00
cd7aba50e5 Cleanup unused title definition. 2017-07-25 17:26:26 +02:00
fc14efbda1 Fix inverted create and update calls. 2017-07-25 17:26:09 +02:00
2f64fa1018 Use Schedule Edit Modal component in Schedule List component. 2017-07-25 17:25:50 +02:00
d1344d57b8 Use Schedule Edit Modal component in Schedule Row component. 2017-07-25 17:21:01 +02:00
57aa737465 Add Schedule Edit Modal component. 2017-07-25 17:18:04 +02:00
3c0d3c1a39 Add Schedule Form component. 2017-07-25 17:17:22 +02:00
3dd347ee8c Remove unused template. 2017-07-25 16:59:48 +02:00
2cf432ed2c Rename component. 2017-07-25 16:56:57 +02:00
d83a164cc6 Remove dependency. 2017-07-25 16:14:48 +02:00
53a29062ea Use Schedule Delete Modal Component in scheduleRow Component. 2017-07-25 15:59:06 +02:00
46f5f72bd4 Add Schedule Delete Modal component. 2017-07-25 15:45:25 +02:00
fd4da93c20 Add missing loglevel dependency. 2017-07-25 12:09:51 +02:00
e8247e30ab Upgrade Schedule component to Angular4. 2017-07-25 12:04:45 +02:00
2166def0f1 Inject $modal and accountIdService in Angular Component. 2017-07-25 11:48:48 +02:00
3c148e5297 Remove unused dependency. 2017-07-25 10:42:29 +02:00
434020f7ad Add Restangular module and ScheduleRowComponent. 2017-07-25 10:15:00 +02:00
3c3741c33f Use accountIdService to retrieve current Account Id in Schedule Component. 2017-07-25 10:06:41 +02:00
7ea652e23e Add Shecdule Row Component. 2017-07-25 08:55:56 +02:00
fa9402e25e Add accountIdService to be able to correctly retrieve accountId in UiRouter transition. 2017-07-25 08:53:08 +02:00
ad16b5a391 Upgrades. 2017-07-24 21:22:54 +02:00
17a363d69d Move up ng1 specific configuration in ng1 app module. 2017-07-24 20:33:30 +02:00
8fb5f2ff7a Separate hybrid application bootstrap. 2017-07-24 19:55:20 +02:00
6873b32005 Set name for Angular1 app module. 2017-07-24 19:51:30 +02:00
a7cee2891c Move states in separate files. 2017-07-24 18:48:57 +02:00
7b7f72bf1e Inject $modal from router. 2017-07-23 07:58:57 +02:00
4f090a22df Format. 2017-07-23 07:53:26 +02:00
1f5d4980e5 Cleanup module. 2017-07-23 07:44:31 +02:00
40bc4bf1e8 Rename controller to component. 2017-07-23 07:44:03 +02:00
c4c10f9ab7 Use component in scheduler route, inject accountId into. 2017-07-23 00:12:44 +02:00
3326dac51a Fix creation and update. 2017-07-22 14:29:29 +02:00
e927736b45 Remove unused factory. 2017-07-22 11:16:20 +02:00
d52d382653 Replace schedule factory by schedule service. 2017-07-22 11:16:04 +02:00
42c0fe6c9b Add schedule module. 2017-07-22 11:14:52 +02:00
c257069644 Add schedule service. 2017-07-22 11:11:39 +02:00
c5f3a53347 Add Schedule model. 2017-07-22 10:39:19 +02:00
d96a66ee3e Remove unneeded . 2017-07-22 09:46:06 +02:00
d5e00b8fe3 Remove eslint comments. 2017-07-22 09:18:22 +02:00
801d2ae380 Migrate to toastr. 2017-07-22 09:17:50 +02:00
68df4e5ce2 Prepare scheduler module for angular2 upgrade. 2017-07-22 09:16:16 +02:00
aa07ffb125 Remove unused factory. 2017-07-22 08:44:54 +02:00
adec1f102e Rename dependencies. 2017-07-22 08:42:17 +02:00
f9c2e7f4bc Cleanup imports. 2017-07-21 23:11:04 +02:00
fc0c08d58e Fix currency. 2017-07-21 23:07:20 +02:00
8626c78708 Transform controller function to class. 2017-07-21 23:06:59 +02:00
a398752c0c Fix export and import. 2017-07-21 23:06:10 +02:00
cc5b0b1ee1 Fix condition. 2017-07-21 22:42:16 +02:00
220426e6f8 Separate account row from account list. 2017-07-21 21:50:19 +02:00
bd484a994e Fix indent. 2017-07-21 00:52:47 +02:00
f780be6b63 Remove unused template. 2017-07-21 00:51:31 +02:00
fc75945a76 Fix indent. 2017-07-21 00:45:04 +02:00
f8fa34f269 Dissociate new account modal from editing account on AccountListComponent. 2017-07-21 00:41:04 +02:00
79d55bfc44 Upgrade AccountListComponent to Angular. 2017-07-21 00:39:15 +02:00
88e0599cd7 Remove unneeded script include. 2017-07-21 00:35:45 +02:00
1c93f528f6 Cleanup licence. 2017-07-21 00:33:55 +02:00
d522aaad45 Fix promise error handling. 2017-07-21 00:33:27 +02:00
d91089238d Change import order. 2017-07-21 00:32:51 +02:00
8b0d3decd4 Change scope of attribute. 2017-07-21 00:32:39 +02:00
598ddc6a85 Remove unused dependency. 2017-07-21 00:30:00 +02:00
e28500c557 Use ngx-toastr for notifications in account module. 2017-07-21 00:29:03 +02:00
b95f36f09c Change field parent bind. 2017-07-21 00:06:07 +02:00
4064848242 Change account form validation for ahtorized overdraft. 2017-07-20 23:34:07 +02:00
6ff893a08d Rename AccountComponent to AccountListComponent. 2017-07-20 23:00:52 +02:00
4e9915aab0 Cleanup. 2017-07-20 22:54:57 +02:00
5100d0fd0c Cleanup. 2017-07-20 22:53:22 +02:00
60aa6310bb Separate account form from modal. 2017-07-20 22:52:33 +02:00
adfd61fac9 Add missing dependencies for NgbModal. 2017-07-20 22:31:58 +02:00
4057705e22 Use ng-bootstrap Modal. 2017-07-20 10:32:05 +02:00
3e6b1ecccc Use new Logger in account module. 2017-07-16 22:25:50 +02:00
de945cd16c Create service for AccountBalances and use it. 2017-07-16 17:03:50 +02:00
330ed6b926 Transform account state to component. 2017-07-16 14:23:23 +02:00
83d7e61875 Switch to uirouter. 2017-07-16 13:56:49 +02:00
9910c0e0d3 Externalize log level in configuration file. 2017-07-16 10:51:54 +02:00
877f77babd Configure Logger at app level. 2017-07-16 10:49:02 +02:00
95fa835496 Remove newline. 2017-07-16 10:45:27 +02:00
6244b817d8 Use ngx-restangular to handle account API. 2017-07-16 10:44:25 +02:00
8b62380c52 Fix indent. 2017-07-16 10:30:53 +02:00
270406ff58 Remove unused AccountFactory. 2017-07-15 08:19:30 +02:00
716dd94943 Fix resource configuration for trailing slashes. 2017-07-15 08:17:22 +02:00
cd2e20c744 Import order. 2017-07-14 21:41:27 +02:00
faa2abfca3 Rename account controller to component. 2017-07-14 21:38:29 +02:00
839002cc47 Upgrade extract text webpack plugin. 2017-07-14 21:33:37 +02:00
4861f5ce49 Fix account change handling in balance chart component. 2017-07-14 10:33:20 +02:00
5960e9ee77 Rename item to account. 2017-07-14 10:25:33 +02:00
c6a406272f Use account service. 2017-07-14 10:24:46 +02:00
79ecb1630b Account account module in app. 2017-07-14 10:19:39 +02:00
c4baf94d3a Move default value in account constructor. 2017-07-14 10:15:07 +02:00
c6761e1379 Fix URL in account service. 2017-07-14 10:05:48 +02:00
3fb442ab5d Fix URL in account service. 2017-07-14 10:05:09 +02:00
df4d12cfb8 Catch errors in account service. 2017-07-14 10:04:55 +02:00
1e94109910 Typescript conversion. 2017-07-13 21:37:47 +02:00
bda2c90e3d Change account controller into class. 2017-07-13 17:14:20 +02:00
995af2fbd3 Add missing dependencies. 2017-07-13 16:58:31 +02:00
65a322466a Add account module. 2017-07-13 16:57:02 +02:00
48caa14cd8 Add account service. 2017-07-13 16:55:02 +02:00
f55efca9fd Add account models. 2017-07-13 16:54:09 +02:00
f3c3ddfebf Bootstrap Angular app. 2017-07-11 18:51:01 +02:00
3ff0015690 Fix module resolution. 2017-07-11 08:49:39 +02:00
2ae971a7ad Typescript conversion. 2017-07-10 15:51:58 +02:00
d4eaa1454c Separate Account balances in another factory. 2017-07-10 08:17:11 +02:00
4abe5092ec Upgrades. 2017-07-10 00:55:25 +02:00
83 changed files with 2871 additions and 1986 deletions

View File

@ -4,60 +4,79 @@
"repository": "https://git.lahouze.org/xals/accountant",
"license": "AGPL-1.0",
"devDependencies": {
"@types/angular": "^1.6.25",
"@types/angular-resource": "^1.5.9",
"@types/angular-strap": "^2.2.32",
"@types/angular-ui-notification": "^0.0.4",
"angular-tslint-rules": "^1.0.3",
"babel-core": "^6.25.0",
"babel-eslint": "^7.2.3",
"babel-loader": "^7.0.0",
"bootstrap-webpack": "^0.0.6",
"css-loader": "^0.28.4",
"eslint": "^4.1.1",
"eslint-config-angular": "^0.5",
"eslint-config-webpack": "^1.2.3",
"eslint-loader": "^1.7.1",
"eslint-plugin-angular": "^3.0.0",
"eslint-plugin-html": "^3.0.0",
"eslint-plugin-jquery": "^1.2",
"eslint-plugin-promise": "^3.5",
"eslint-plugin-security": "^1.3",
"eslint-plugin-this": "^0.2",
"extract-text-webpack-plugin": "^2.1.2",
"file-loader": "^0.11.2",
"html-loader": "^0.4.5",
"html-webpack-plugin": "^2.28.0",
"htmllint-loader": "^1.3.8",
"@types/requirejs": "^2.1.31",
"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.2.0",
"css-loader": "^0.28.7",
"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.1",
"eslint-plugin-html": "^3.2.2",
"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": "^2.7.2",
"less-loader": "^4.0.4",
"ngtemplate-loader": "^2.0.0",
"style-loader": "^0.18.2",
"ts-loader": "^2.2.2",
"tslint": "^5.5.0",
"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.5.0",
"tslint-loader": "^3.5.3",
"url-loader": "^0.5.8",
"webpack": "^2.6.1",
"webpack-dev-server": "2.4.5"
"typescript": "^2.5.2",
"url-loader": "^0.6.2",
"webpack": "^3.8.1",
"webpack-dev-server": "^2.9.3"
},
"dependencies": {
"angular": "^1.6",
"angular-http-auth": "^1.5",
"angular-messages": "^1.6",
"angular-resource": "^1.6",
"angular-route": "^1.6",
"angular-strap": "^2.3.12",
"angular-ui-notification": "^0.3",
"@angular/animations": "^4.4.6",
"@angular/cdk": "^2.0.0-beta.10",
"@angular/common": "^4.4.6",
"@angular/compiler": "^4.4.6",
"@angular/core": "^4.4.6",
"@angular/flex-layout": "^2.0.0-beta.9",
"@angular/forms": "^4.4.6",
"@angular/http": "^4.4.6",
"@angular/material": "^2.0.0-beta.10",
"@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/jquery": "^3.2.12",
"@types/node": "^8.0.47",
"angular2-text-mask": "^8.0.4",
"base64util": "^1.0.2",
"bootbox": "^4.4.0",
"bootstrap": "^3.3.7",
"bootstrap-additions": "^0.3.1",
"c3": "^0.4.13",
"bootstrap": "^4.0.0-beta",
"c3": "^0.4.18",
"font-awesome": "^4.7.0",
"jquery": "^3.2",
"meanie-angular-storage": "^1.3.1",
"moment": "^2.18"
"jquery": "^3.2.1",
"material-design-icons": "^3.0.1",
"moment": "^2.19.1",
"ngx-toastr": "^6.5.0",
"reflect-metadata": "^0.1.10",
"roboto-fontface": "^0.8.0",
"rxjs": "^5.5.2",
"zone.js": "^0.8.17"
},
"scripts": {
"build": "webpack --config webpack.config.js",

View File

@ -1,7 +0,0 @@
/// <reference path="../../node_modules/@types/angular/index.d.ts" />
/// <reference path="../../node_modules/@types/angular-resource/index.d.ts" />
export default function AccountConfig ($resourceProvider) {
// Keep trailing slashes to avoid redirect by flask.
$resourceProvider.defaults.stripTrailingSlashes = false;
};

View File

@ -1,163 +0,0 @@
/// <reference types="angular" />
/// <reference types="angular-ui-notification" />
//import accountFormTmpl from './account.form.tmpl.html';
//import accountDeleteTmpl from './account.delete.tmpl.html';
import IAccount from './account.factory';
export interface IAccountController {
rowClass(account: IAccount) : string;
valueClass(account: IAccount, value: number) : string;
add() : void;
save(account: IAccount) : void;
confirmDelete(account: IAccount): void;
delete(account: IAccount): void;
modify(account: IAccount): void;
};
export default class AccountController {
static $inject = ['AccountResource', 'Notification', '$log', '$modal'];
private accounts: IAccount[];
constructor(
public AccountResource: AccountResource,
public Notification: angular.uiNotification.INotificationService,
public $modal: mgcrea.ngStrap.modal.IModalService) {
// Load accounts.
this.accounts = Account.query();
};
/*
* Return the class for an account current value compared to authorized
* overdraft.
*/
rowClass(account : IAccount): string {
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.
*/
valueClass (account: IAccount, value: number): string {
if (!account || !value) {
return '';
}
if (value < account.authorized_overdraft) {
return 'text-danger';
} else if (value < 0) {
return 'text-warning';
}
};
/*
* Add an empty account.
*/
add(): void {
var account = new Account({
// eslint-disable-next-line camelcase
authorized_overdraft: 0
});
// Insert account at the begining of the array.
this.modify(account);
};
/*
* Save account.
*/
save(account: IAccount): void {
return account.$save().then(function(data) {
this.Notification.success('Account #' + data.id + ' saved.');
this.accounts = Account.query();
}, function(result){
$log.error('Error while saving account', account, result);
this.Notification.error(
'Error while saving account: ' + result.message
);
});
};
confirmDelete(account: IAccount): void {
var title = "Delete account #" + account.id;
$modal({
templateUrl: accountDeleteTmpl,
controller: function($scope, title, account, $delete) {
$scope.title = title;
$scope.account = account;
$scope.$delete = function() {
$scope.$hide();
$delete($scope.account);
};
},
locals: {
title: title,
account: account,
$delete: this.delete
}
});
};
/*
* Delete an account.
*/
delete(account: IAccount): void {
var id = account.id;
account.$delete().then(function() {
this.Notification.success('account #' + id + ' deleted.');
this.accounts = Account.query();
}, function(result) {
this.Notification.error(
'An error occurred while trying to delete account #' +
id + ':<br />' + result
);
});
};
/*
* Open the popup to modify the account, save it on confirm.
*/
modify(account: IAccount): void {
// FIXME Alexis Lahouze 2017-06-15 i18n
var title = "Account";
if (account.id) {
title = title + " #" + account.id;
}
$modal({
templateUrl: accountFormTmpl,
controller: function($scope, title, account, $save) {
$scope.title = title;
$scope.account = account;
$scope.account.authorized_overdraft *= -1;
$scope.$save = function() {
$scope.$hide();
$scope.account.authorized_overdraft *= -1;
$save($scope.account);
};
},
locals: {
title: title,
account: account,
$save: this.save
}
});
};
};

View File

@ -0,0 +1,36 @@
// vim: set tw=80 ts=2 sw=2 sts=2:
import { Injectable } from '@angular/core';
import { DataSource } from '@angular/cdk/collections';
import { Observable } from 'rxjs/Rx';
import { Account } from './account';
import { AccountBalances } from './accountBalances';
import { AccountBalancesService } from './accountBalances.service';
import { AccountService } from './account.service';
@Injectable()
export class AccountDataSource extends DataSource<Account> {
constructor(
private accountService: AccountService,
private accountBalancesService: AccountBalancesService,
) {
super();
}
connect(): Observable<Account[]> {
return this.accountService.query().map((accounts: Account[]) => {
for(let account of accounts) {
this.accountBalancesService
.get(account.id)
.subscribe((accountBalances: AccountBalances) => {
account.balances = accountBalances;
})
}
return accounts;
});
}
disconnect() {}
}

View File

@ -1,23 +0,0 @@
<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">{{ title }}</h3>
</div>
<div class="modal-body" id="modal-body">
<p>Voulez-vous supprimer le compte #{{ account.id }} ayant pour nom :<br/>{{ account.name }}
</p>
</div>
<div class="modal-footer">
<button class="btn btn-danger" type="button" ng-click="$delete()">
Supprimer
</button>
<button class="btn btn-default" type="button" ng-click="$hide()">
Annuler
</button>
</div>
</div>
</div>
</div>

View File

@ -1,19 +0,0 @@
/// <reference types="angular" />
/// <reference types="angular-resource" />
export interface IAccount extends ng.resource.IResource<IAccount> {
id: number;
name: string;
authorized_overdraft: number;
};
export interface IAccountResource extends ng.resource.IResourceClass<IAccount> {
};
export default function AccountResource($resource) {
return <IAccountResource> $resource(
'/api/account/:id', {
id: '@id'
}
);
};

View File

@ -1,72 +0,0 @@
<!-- vim: set tw=80 ts=2 sw=2 sts=2: -->
<!--
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/>.
-->
<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">{{ title }}</h3>
</div>
<form name="form" class="form-horizontal simple-form" novalidate
ng-submit="$save()">
<div class="modal-body" id="modal-body">
<div class="form-group" ng-class="{ 'has-error' : form.name.$invalid && !form.name.$pristine }">
<label class="col-sm-4 control-label" for="name">Account name</label>
<div class="col-sm-8">
<input class="form-control" id="name" name="name"
ng-model="account.name"
type="text" placeholder="Account name"
required />
<div class="help-block" ng-messages="form.name.$error" ng-if="form.name.$invalid">
<p ng-message="required">The account name is required.</p>
</div>
</div>
</div>
<div class="form-group" ng-class="{ 'has-error' : form.authorized_overdraft.$invalid && !form.authorized_overdraft.$pristine }">
<label class="col-sm-4 control-label" for="authorized-overdraft">Authorized overdraft</label>
<div class="col-sm-8">
<div class="input-group">
<div class="input-group-addon">-</div>
<input class="form-control" id="authorized-overdraft"
name="authorized_overdraft"
ng-model="account.authorized_overdraft"
type="number" placeholder="Authorized overdraft"
required min="0"/>
<div class="input-group-addon">.00€</div>
</div>
<div class="help-block" ng-messages="form.authorized_overdraft.$error" ng-if="form.authorized_overdraft.$invalid">
<p ng-message="required">The authorized overdraft is required.</p>
<p ng-message="min">The authorized overdraft must be equal or greater than 0.</p>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<input class="btn btn-primary" type="submit" ng-disabled="form.$invalid"/>
<button class="btn btn-default" type="button" ng-click="$hide()">
Annuler
</button>
</div>
</form>
</div>
</div>
</div>

View File

@ -0,0 +1,67 @@
// 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 {
MdButtonModule,
MdDialogModule,
MdIconModule,
MdInputModule,
MdListModule,
MdTableModule,
} from '@angular/material';
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 { AccountService } from './account.service';
import { AccountBalancesService } from './accountBalances.service';
import { AccountListComponent } from './accountList.component';
import { AccountDataSource } from './account.dataSource';
import { AccountDeleteModalComponent } from './accountDeleteModal.component';
import { AccountEditModalComponent } from './accountEditModal.component';
import { AccountFormComponent } from './accountForm.component';
import { DailyBalanceService } from './dailyBalance.service';
import { AccountListState } from './account.states'
@NgModule({
imports: [
HttpClientModule,
CommonModule,
ReactiveFormsModule,
RouterModule.forChild([
AccountListState
]),
MdButtonModule,
MdDialogModule,
MdIconModule,
MdInputModule,
MdListModule,
MdTableModule,
NgLoggerModule,
ToastrModule,
NgbModule,
],
providers: [
AccountService,
AccountDataSource,
AccountBalancesService,
DailyBalanceService,
],
declarations: [
AccountListComponent,
AccountDeleteModalComponent,
AccountEditModalComponent,
AccountFormComponent,
],
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

@ -1,21 +0,0 @@
import * as angular from 'angular';
import 'angular-resource';
export interface IAccountBalances
extends ng.resource.IResource<IAccountBalances> {
current: number;
pointed: number;
future: number;
};
export interface IAccountBalancesResource
extends ng.resource.IResourceClass<IAccountBalances> {
};
export default function AccountBalancesResource($resource) {
return <IAccountBalancesResource> $resource(
'/api/account/:id/balances', {
id: '@id'
}
);
};

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,30 @@
// vim: set tw=80 ts=2 sw=2 sts=2:
import { Component, Inject } from '@angular/core';
import { MD_DIALOG_DATA } from '@angular/material';
@Component({
selector: 'account-delete-modal',
template: `
<h3 md-dialog-title>Delete account #{{ data.account.id }}</h3>
<md-dialog-content>
Do you really want to delete account #{{ data.account.id }} with name:<br/>
{{ data.account.name }}
</md-dialog-content>
<md-dialog-actions>
<button md-raised-button color="warn" [md-dialog-close]="data.account">
Yes
</button>
<button md-raised-button md-dialog-close>
No
</button>
</md-dialog-actions>
`
})
export class AccountDeleteModalComponent {
constructor(
@Inject(MD_DIALOG_DATA) private data: any
) {}
}

View File

@ -0,0 +1,58 @@
// vim: set tw=80 ts=2 sw=2 sts=2:
import { Component, Inject, ViewChild } from '@angular/core';
import { MdDialogRef, MD_DIALOG_DATA } from '@angular/material';
import { Account } from './account';
import { AccountFormComponent } from './accountForm.component';
@Component({
selector: 'account-edit-modal',
template: `
<h3 md-dialog-title>{{ title() }}</h3>
<md-dialog-content>
<account-form [account]="account" (submit)="submit()" #accountForm="accountForm">
</account-form>
</md-dialog-content>
<md-dialog-actions>
<button md-raised-button color="primary" [disabled]="!accountForm?.form.valid" (click)="submit()">
Save
</button>
<button md-raised-button color="warn" md-dialog-close>
Cancel
</button>
</md-dialog-actions>
`
})
export class AccountEditModalComponent {
private account: Account;
@ViewChild('accountForm') accountForm: AccountFormComponent;
constructor(
@Inject(MD_DIALOG_DATA) public data: any,
public dialogRef: MdDialogRef<AccountEditModalComponent>,
) {
this.account = data.account;
}
title(): string {
if(this.account && 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.dialogRef.close(account);
}
}

View File

@ -0,0 +1,73 @@
// 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">
<md-list>
<md-list-item>
<md-form-field>
<input mdInput formControlName="name" placeholder="Account name">
<md-error *ngIf="name.errors?.required">The account name is required.</md-error>
</md-form-field>
</md-list-item>
<md-list-item>
<md-form-field>
<span mdPrefix>-</span>
<input mdInput formControlName="authorizedOverdraft"
placeholder="Authorized overdraft">
<span mdSuffix>.00€</span>
<md-error *ngIf="authorizedOverdraft.errors?.required">
The authorized overdraft is required.
</md-error>
<md-error *ngIf="authorizedOverdraft.errors?.min">
The authorized overdraft must be less than or equal to 0.
</md-error>
</md-form-field>
</md-list-item>
</md-list>
</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.min(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,191 @@
// vim: set tw=80 ts=2 sw=2 sts=2 :
import { Component } from '@angular/core';
import { MdDialog } from '@angular/material';
import { Observable } from 'rxjs/Rx';
import { Logger } from '@nsalaun/ng-logger';
import { ToastrService } from 'ngx-toastr';
import { Account } from './account';
import { AccountBalances } from './accountBalances';
import { AccountDataSource } from './account.dataSource';
import { AccountService } from './account.service';
import { AccountDeleteModalComponent } from './accountDeleteModal.component';
import { AccountEditModalComponent } from './accountEditModal.component';
@Component({
selector: 'account-list',
template: `
<div class="containerX">
<div class="container">
<button md-fab color="primary" (click)="add()">
<md-icon>add</md-icon>
</button>
</div>
<div class="container">
<md-table #table [dataSource]="accounts">
<ng-container mdColumnDef="name">
<md-header-cell *mdHeaderCellDef>Nom du compte</md-header-cell>
<md-cell *mdCellDef="let account">
<a [routerLink]="['/account', account.id, 'operations']">
{{ account.name }}
</a>
</md-cell>
</ng-container>
<ng-container mdColumnDef="current">
<md-header-cell *mdHeaderCellDef>Solde courant</md-header-cell>
<md-cell *mdCellDef="let account">
<span
[class.warning]="account.authorized_overdraft < 0 && account.balances?.current < 0"
[class.error]="account.balances?.current < account.authorized_overdraft">
{{ account.balances?.current | currency:"EUR":true }}
</span>
</md-cell>
</ng-container>
<ng-container mdColumnDef="pointed">
<md-header-cell *mdHeaderCellDef>Solde pointé</md-header-cell>
<md-cell *mdCellDef="let account">
<span
[class.warning]="account.authorized_overdraft < 0 && account.balances?.pointed < 0"
[class.error]="account.balances?.pointed < account.authorized_overdraft">
{{ account.balances?.pointed | currency:"EUR":true }}
</span>
</md-cell>
</ng-container>
<ng-container mdColumnDef="authorizedOverdraft">
<md-header-cell *mdHeaderCellDef>Découvert autorisé</md-header-cell>
<md-cell *mdCellDef="let account">
{{ account.authorized_overdraft | currency:"EUR":true }}
</md-cell>
</ng-container>
<ng-container mdColumnDef="actions">
<md-header-cell *mdHeaderCellDef>Actions</md-header-cell>
<md-cell *mdCellDef="let account">
<!-- Edit account. -->
<button md-mini-fab color="primary"
(click)="modify(account)">
<md-icon>mode_edit</md-icon>
</button>
<!-- Delete account, with confirm. -->
<button md-mini-fab color="warn"
(click)="confirmDelete(account)">
<md-icon>delete_forever</md-icon>
</button>
<!-- Open account scheduler. -->
<button md-mini-fab
[hidden]="!account.id"
[routerLink]="['/account', account.id, 'scheduler']">
<md-icon>event</md-icon>
</button>
</md-cell>
</ng-container>
<md-header-row *mdHeaderRowDef="displayedColumns"></md-header-row>
<md-row *mdRowDef="let row; columns: displayedColumns;">
</md-row>
</md-table>
</div>
</div>
`,
})
export class AccountListComponent {
displayedColumns: String[] = [
'name', 'current', 'pointed', 'authorizedOverdraft', 'actions'
];
constructor(
private accounts: AccountDataSource,
private accountService: AccountService,
private toastrService: ToastrService,
private logger: Logger,
private mdDialog: MdDialog,
) {
}
/*
* Add an empty account.
*/
add() {
this.modify(new Account());
};
/*
* Modify an account.
*/
modify(account: Account) {
let dialogRef = this.mdDialog.open(AccountEditModalComponent, {
data: {
account: account,
}
});
dialogRef.afterClosed().subscribe((account: Account) => {
if(account) {
this.logger.log("Modal closed => save account", account);
this.save(account);
} else {
this.logger.log("Modal dismissed");
}
}, (reason) => function(reason) {
});
}
/*
* Save account.
*/
save(account) {
this.accountService.create(account).subscribe(account => {
this.toastrService.success('Account #' + account.id + ' saved.');
}, result => {
this.logger.error('Error while saving account', account, result);
this.toastrService.error(
'Error while saving account: ' + result.message
);
});
};
/*
* Show a dialog to confirm account deletion.
*/
confirmDelete(account: Account) {
let dialogRef = this.mdDialog.open(AccountDeleteModalComponent, {
data: {
account: account,
}
});
dialogRef.afterClosed().subscribe((account: Account) => {
if(account) {
this.delete(account);
}
}, reason => {
this.logger.error("Delete dialog failed", reason);
});
};
/*
* Delete an account.
*/
delete(account: Account) {
var id = account.id;
this.accountService.delete(account).subscribe(account => {
this.toastrService.success('account #' + id + ' deleted.');
// FIXME Alexis Lahouze 2017-09-17 Remove from array.
}, function(result) {
this.toastrService.error(
'An error occurred while trying to delete account #' +
id + ':<br />' + result
);
});
};
};

View File

@ -1,90 +0,0 @@
<!DOCTYPE html>
<!-- vim: set tw=80 ts=2 sw=2 sts=2: -->
<!--
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/>.
-->
<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" ng-click="accountsCtrl.add()">
Ajouter
</button>
</td>
</tr>
<tr id="{{ account.id }}"
class="form-inline" ng-class="rowClass(account)"
ng-repeat="account in accountsCtrl.accounts | orderBy:'name'"
ng-init="account.getBalances()">
<td>
<a href="#!/account/{{ account.id }}/operations">{{ account.name }}</a>
</td>
<td>
<span ng-class="accountsCtrl.valueClass(account, account.balances.current)">
{{ account.balances.current | currency : "€" }}
</span>
</td>
<td>
<span ng-class="accountsCtrl.valueClass(account, account.balancess.pointed)">
{{ account.balances.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"
ng-model="accountsCtrl.account"
ng-click="accountsCtrl.modify(account)">
<span class="fa fa-pencil-square-o"></span>
</button>
<!-- Delete account, with confirm. -->
<button type="button" class="btn btn-default"
ng-click="accountsCtrl.confirmDelete(account)">
<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

@ -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;
}

View File

@ -1,47 +0,0 @@
// vim: set tw=80 ts=4 sw=4 sts=4:
/*
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/>.
*/
/* jshint node: true */
'use strict';
var angular = require('angular');
var ngResource = require('angular-resource'),
ngMessages = require('angular-messages'),
ngUiNotification = require('angular-ui-notification'),
ngStrap = require('angular-strap');
var AccountBalancesFactory = require('./accountBalances.factory');
var AccountFactory = require('./account.factory');
var AccountConfig = require('./account.config');
var AccountController = require('./account.controller');
module.exports = angular.module('accountant.accounts', [
ngResource,
ngMessages,
ngUiNotification,
ngStrap,
])
.config(AccountConfig)
.factory('Account', AccountFactory)
.factory('AccountBalances', AccountBalancesFactory)
.controller('AccountController', AccountController)
.name;

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

@ -0,0 +1,23 @@
// vim: set tw=80 ts=2 sw=2 sts=2 :
import { Component } from '@angular/core';
@Component({
selector: 'accountant',
styles: [ require('./main.scss').toString() ],
template: `
<md-toolbar class="mat-elevation-z6" color="primary">
<div class="acc-toolbar">
<a md-button style="text-transform: uppercase"
routerLink="/accounts">
Accountant
</a>
</div>
</md-toolbar>
<div class="acc-content">
<router-outlet></router-outlet>
</div>
`
})
export class AppComponent { }

View File

@ -1,26 +0,0 @@
var operationsTmpl = require('./operations/operations.html');
var accountsTmpl = require('./accounts/accounts.html');
var schedulerTmpl = require('./scheduler/scheduler.html');
module.exports = function($routeProvider) {
// Defining template and controller in function of route.
$routeProvider
.when('/account/:accountId/operations', {
templateUrl: operationsTmpl,
controller: 'OperationController',
controllerAs: 'operationsCtrl'
})
.when('/account/:accountId/scheduler', {
templateUrl: schedulerTmpl,
controller: 'SchedulerController',
controllerAs: 'schedulerCtrl'
})
.when('/accounts', {
templateUrl: accountsTmpl,
controller: 'AccountController',
controllerAs: 'accountsCtrl'
})
.otherwise({
redirectTo: '/accounts'
});
};

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;

View File

@ -1,40 +0,0 @@
// vim: set tw=80 ts=4 sw=4 sts=4:
/*
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/>.
*/
/* jshint node: true */
'use strict';
var angular = require('angular');
var ngRoute = require('angular-route');
var accountModule = require('./accounts'),
loginModule = require('./login'),
operationModule = require('./operations'),
schedulerModule = require('./scheduler');
var routing = require('./app.config');
require('bootstrap-webpack!./bootstrap.config.js');
angular.module('accountant', [
ngRoute,
accountModule,
loginModule,
operationModule,
schedulerModule,
]).config(routing);

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

@ -0,0 +1,63 @@
// vim: set tw=80 ts=2 sw=2 sts=2:
import 'zone.js';
import 'reflect-metadata';
import { NgModule } from '@angular/core';
import {
MdButtonModule,
MdToolbarModule,
MdSidenavModule
} from '@angular/material';
import { FlexLayoutModule } from '@angular/flex-layout';
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 { 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
}),
FlexLayoutModule,
MdButtonModule,
MdToolbarModule,
MdSidenavModule,
LoginModule,
NgLoggerModule.forRoot(LogLevel),
ToastrModule.forRoot(),
NgbModule.forRoot(),
AccountModule,
ScheduleModule,
OperationModule,
],
declarations: [
AppComponent
],
bootstrap: [ AppComponent ]
})
export class AppModule {
constructor() {}
}

View File

@ -1,64 +0,0 @@
/* jshint node: true */
'use strict';
module.exports = {
scripts: {
'transition': true,
'alert': true,
'button': true,
'carousel': true,
'collapse': true,
'dropdown': true,
'modal': true,
'tooltip': true,
'popover': true,
'scrollspy': true,
'tab': true,
'affix': true
},
styles: {
'mixins': true,
'normalize': true,
'print': true,
'scaffolding': true,
'type': true,
'code': true,
'grid': true,
'tables': true,
'forms': true,
'buttons': true,
'component-animations': true,
'glyphicons': true,
'dropdowns': true,
'button-groups': true,
'input-groups': true,
'navs': true,
'navbar': true,
'breadcrumbs': true,
'pagination': true,
'pager': true,
'labels': true,
'badges': true,
'jumbotron': true,
'thumbnails': true,
'alerts': true,
'progress-bars': true,
'media': true,
'list-group': true,
'panels': true,
'wells': true,
'close': true,
'modals': true,
'tooltip': true,
'popovers': true,
'carousel': true,
'utilities': true,
'responsive-utilities': true
}
};

View File

@ -1,3 +0,0 @@
@pre-border-color: @pre-bg; // hide the border.
@import "./main.less";

View File

@ -1,48 +1,18 @@
<!DOCTYPE html>
<!-- vim: set tw=80 ts=2 sw=2 sts=2: -->
<!--
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/>.
-->
<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>
<!-- htmllint attr-bans="false" -->
<body style="padding-bottom: 50px; padding-top: 70px" ng-app="accountant">
<body style="padding-bottom: 50px; padding-top: 70px">
<!-- 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">
<div ng-view></div>
</div>
<!-- Custom Javascript libraries -->
<script src="<% htmlWebpackPlugin.files.js[0] %>"></script>
<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);
});
}
);
}
}

View File

@ -1,50 +0,0 @@
// vim: set tw=80 ts=4 sw=4 sts=4:
/*
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/>.
*/
/* jshint node: true */
'use strict';
var angular = require('angular');
var ngStorage = require('meanie-angular-storage'),
ngHttpAuth = require('angular-http-auth'),
ngStrap = require('angular-strap');
// Note: ngHttpAuth seems to have no module.exports.
ngHttpAuth = 'http-auth-interceptor';
var LoginService = require('./login.service');
var LoginConfig = require('./login.config');
module.exports = angular.module('accountant.login', [
ngHttpAuth,
ngStorage
])
.service('LoginService', LoginService)
.config(LoginConfig)
.run(function($rootScope, LoginService) {
var onAuthLoginRequired = $rootScope.$on('event:auth-loginRequired', LoginService.loginModal);
$rootScope.$on('$destroy', function() {
onAuthLoginRequired = angular.noop();
});
})
.name;

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

@ -0,0 +1,49 @@
// 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 {
MdButtonModule,
MdDialogModule,
MdInputModule,
MdListModule,
} from '@angular/material';
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,
NgLoggerModule,
MdButtonModule,
MdDialogModule,
MdInputModule,
MdListModule,
],
providers: [
LoginService,
{
provide: HTTP_INTERCEPTORS,
useClass: AuthInterceptor,
multi: true
}
],
declarations: [
LoginModalComponent,
LoginFormComponent,
],
entryComponents: [
LoginModalComponent,
]
})
export class LoginModule {};

View File

@ -1,45 +0,0 @@
var base64 = require('base64util');
var loginTmpl = require('./login.tmpl.html');
module.exports = function($storage, $http, authService, $modal) {
var login = function(email, password) {
// Encode authentication data.
var authdata = base64.encode(email + ':' + password);
return $http.post('/api/user/login', {}, {
ignoreAuthModule: true,
headers: {
'authorization': 'Basic ' + authdata
}
}).then(function (result) {
$storage.session.set('refresh_token', result.data.refresh_token);
$storage.session.set('access_token', result.data.access_token);
authService.loginConfirmed();
}, function(result) {
loginModal();
});
};
var loginModal = function () {
$storage.session.clear();
$modal({
templateUrl: loginTmpl,
controller: function($scope, $login) {
$scope.$login = function() {
$scope.$hide();
$login($scope.email, $scope.password);
};
},
locals: {
$login: login,
}
});
};
return {
'loginModal': loginModal,
};
};

View File

@ -0,0 +1,65 @@
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { MdDialog } from '@angular/material';
import { Observable} from 'rxjs/Rx';
import * as base64 from 'base64util';
import { Logger } from '@nsalaun/ng-logger';
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 mdDialog: MdDialog,
) {}
public readonly url: string = '/api/user/login';
login(): Observable<Token> {
let dialogRef = this.mdDialog.open(LoginModalComponent);
sessionStorage.clear();
return dialogRef.afterClosed().flatMap((login: Login) =>
this.doLogin(login)
).map((token: Token): Token => {
this.accessToken = token.access_token;
return token;
});
}
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);
}
};

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,62 @@
// 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">
<md-list>
<md-list-item>
<md-form-field>
<input mdInput type="text"
formControlName="email" placeholder="Nom d'utilisateur">
<md-error *ngIf="email.errors?.required">The email is required.</md-error>
</md-form-field>
</md-list-item>
<md-list-item>
<md-form-field>
<input mdInput type="password"
formControlName="password" placeholder="Mot de passe">
<md-error *ngIf="password.errors?.required">The password is required.</md-error>
</md-form-field>
</md-list-item>
</md-list>
</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,44 @@
// vim: set tw=80 ts=2 sw=2 sts=2:
import { Component, Input, ViewChild } from '@angular/core';
import { MdDialogRef } from '@angular/material';
import { Login } from './login';
import { LoginFormComponent } from './loginForm.component';
@Component({
selector: 'login-modal',
template: `
<h2 md-dialog-title>Authentification requise</h2>
<md-dialog-content>
<login-form (submit)="submit()" #loginForm="loginForm"></login-form>
</md-dialog-content>
<md-dialog-actions>
<button md-button [disabled]="!loginForm.form.valid" (click)="submit()">
Login
</button>
<button md-button md-dialog-close>
Cancel
</button>
</md-dialog-actions>
`
})
export class LoginModalComponent {
@ViewChild('loginForm') loginForm: LoginFormComponent;
constructor(
public dialogRef: MdDialogRef<LoginModalComponent>,
) {}
submit(): void {
let formModel = this.loginForm.form.value;
let login: Login = new Login();
login.email = formModel.email;
login.password = formModel.password;
this.dialogRef.close(login);
}
}

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;
}

View File

@ -1,41 +0,0 @@
@import '~font-awesome/less/font-awesome';
@import '~angular-ui-notification/src/angular-ui-notification';
@import (inline) '~c3/c3.css';
@import (inline) '~bootstrap-additions/dist/bootstrap-additions.css';
.italic {
font-style: italic;
}
.stroke {
text-decoration: line-through;
}
.c3-ygrid-line.zeroline line {
stroke: orange;
}
.c3-ygrid-line.overdraft line {
stroke: #FF0000;
}
// Needed for modal backdrop opacity.
.modal-backdrop.am-fade {
opacity: .5;
transition: opacity .15s linear;
&.ng-enter {
opacity: 0;
&.ng-enter-active {
opacity: .5;
}
}
&.ng-leave {
opacity: .5;
&.ng-leave-active {
opacity: 0;
}
}
}

97
src/main.scss Normal file
View File

@ -0,0 +1,97 @@
// vim: set tw=80 ts=2 sw=2 sts=2:
$fa-font-path: '~font-awesome/fonts';
@import '~font-awesome/scss/font-awesome';
@import '~c3/c3';
@import '~ngx-toastr/toastr';
$roboto-font-path: '~roboto-fontface/fonts/roboto/';
@import '~@angular/material/_theming';
@import '~@angular/material/prebuilt-themes/indigo-pink.css';
@import '~material-design-icons/iconfont/material-icons.css';
/*.fixed-header {
position: fixed;
top: 0;
left: 0;
z-index: 2;
width: 100% !important;
}
body {
font-family: Roboto, 'Helvetica Neue', sans-serif;
// Helps fonts on OSX looks more consistent with other systems
// Isn't currently in button styles due to performance concerns
* {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.acc-content {
padding: 32px;
box-sizing: border-box;
}
.mat-toolbar {
.acc-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}
}
h1 {
font-size: 20px;
}
}
md-dialog > form {
overflow: visible;
}*/
/*
// stretch to screen size in fullscreen mode
.acc-content {
width: 100%;
height: 100%;
box-sizing: border-box;
}
*/
.italic {
font-style: italic;
}
.stroke {
text-decoration: line-through;
}
.c3-ygrid-line.zeroline line {
stroke: orange;
}
.c3-ygrid-line.overdraft line {
stroke: #FF0000;
}
span.warning {
color: mat-color($mat-amber, A700);
}
.mat-row.warning {
background-color: mat-color($mat-amber, 100);
}
span.error {
color: mat-color($mat-red, A700);
}
.mat-row.error {
background-color: mat-color($mat-red, 100);
}

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

@ -1,190 +0,0 @@
// vim: set tw=80 ts=4 sw=4 sts=4:
/*
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/>.
*/
/* jshint node: true */
'use strict';
var moment = require('moment'),
c3 = require('c3');
var angular = require('angular');
var ngResource = require('angular-resource');
module.exports = angular.module('balanceChartModule', [
ngResource
])
.component('balanceChart', {
template: '<div></div>',
bindings: {
account: '<',
onUpdate: '&'
},
controller: function($routeParams, Balances, $element) {
var vm = this;
vm.loadData = function() {
Balances.query({
id: $routeParams.accountId
}, function(results) {
var headers = [['date', 'balances', 'expenses', 'revenues']];
var rows = results.map(function(result) {
return [
result.operation_date,
result.balance,
result.expenses,
result.revenues
];
});
vm.chart.unload();
vm.chart.load({
rows: headers.concat(rows)
});
var x = vm.chart.x();
var balances = x.balances;
vm.onUpdate(balances[0], balances[balances.length - 1]);
});
};
vm.$onInit = function() {
var tomorrow = moment().endOf('day').valueOf();
vm.chart = c3.generate({
bindto: $element[0].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: function(domain) {
vm.onUpdate({minDate: domain[0], maxDate: domain[1]});
}
}
});
vm.loadData();
};
vm.setLines = function(account) {
if(vm.chart) {
vm.chart.ygrids([
{ value: 0, axis: 'y2' },
{ value: 0, axis: 'y', class: 'zeroline'},
]);
vm.chart.ygrids.add({
value: account.authorized_overdraft,
axis: 'y',
class: 'overdraft'
});
}
};
vm.$onChanges = function(changes) {
if('account' in changes) {
if('$promise' in vm.account && vm.account.$resolved === false) {
vm.account.$promise.then(function(account) {
vm.setLines(account);
return account;
});
} else {
vm.setLines(vm.account);
}
}
};
}
})
.factory('Balances', function($resource) {
return $resource(
'/api/account/:id/daily_balances', {
id: '@id'
}
);
})
.name;

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

@ -1,137 +0,0 @@
// vim: set tw=80 ts=4 sw=4 sts=4:
/*
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/>.
*/
/* jshint node: true */
'use strict';
var moment = require('moment'),
c3 = require('c3');
var angular = require('angular');
var ngResource = require('angular-resource');
module.exports = angular.module('categoryChartModule', [
ngResource
])
.component('categoryChart', {
template: '<div></div>',
bindings: {
minDate: '<',
maxDate: '<'
},
controller: function($routeParams, $element, Categories, Incomes) {
var vm = this;
vm.loadData = function() {
Categories.query({
id: $routeParams.accountId,
begin: vm.minDate ? moment(vm.minDate).format('YYYY-MM-DD') : null,
end: vm.maxDate ? moment(vm.maxDate).format('YYYY-MM-DD') : null
}, function(results) {
var expenses=[],
revenues=[],
colors={},
names={};
var revenuesColor = 'green',
expensesColor = 'orange';
angular.forEach(results, function(result) {
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;
}
});
vm.chart.unload();
vm.chart.load({
columns: revenues.concat(expenses),
names: names,
colors: colors
});
});
};
vm.$onInit = function() {
vm.chart = c3.generate({
bindto: $element[0].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
}
});
//vm.loadData();
};
vm.$onChanges = function() {
vm.loadData();
};
}
})
.factory('Categories', function($resource) {
return $resource(
'/api/account/:id/category', {
id: '@id'
}
);
})
.factory('Incomes', function($resource) {
return $resource(
'/api/account/:id/income', {
id: '@id'
}
);
})
.name;

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

@ -1,54 +0,0 @@
// vim: set tw=80 ts=4 sw=4 sts=4:
/*
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/>.
*/
/* jshint node: true */
'use strict';
var moment = require('moment');
var angular = require('angular');
var ngResource = require('angular-resource'),
ngMessages = require('angular-messages'),
ngUiNotification = require('angular-ui-notification'),
ngStrap = require('angular-strap');
var balanceChartModule = require('./balance-chart.component.js'),
categoryChartModule = require('./category-chart.component.js'),
accountModule = require('../accounts');
var OperationFactory = require('./operation.factory');
var OperationConfig = require('./operation.config');
var OperationController = require('./operation.controller');
module.exports = angular.module('accountant.operations', [
ngResource,
ngMessages,
ngUiNotification,
ngStrap,
accountModule,
balanceChartModule,
categoryChartModule
])
.config(OperationConfig)
.factory('Operation', OperationFactory)
.controller('OperationController', OperationController)
.name;

View File

@ -1,4 +0,0 @@
module.exports = function($resourceProvider) {
// Keep trailing slashes to avoid redirect by flask..
$resourceProvider.defaults.stripTrailingSlashes = false;
};

View File

@ -1,149 +0,0 @@
var operationFormTmpl = require('./operation.form.tmpl.html'),
operationDeleteTmpl = require('./operation.delete.tmpl.html');
module.exports = function($routeParams, $modal, Notification, Operation,
Account) {
var vm = this;
/*
* Add an empty operation.
*/
vm.add = function() {
var operation = new Operation({
// eslint-disable-next-line camelcase
account_id: $routeParams.accountId
});
return vm.modify(operation);
};
/*
* Load operations.
*/
vm.load = function(minDate, maxDate) {
vm.minDate = minDate;
vm.maxDate = maxDate;
return Operation.query({
// eslint-disable-next-line camelcase
account_id: $routeParams.accountId,
begin: minDate ? moment(minDate).format('YYYY-MM-DD') : null,
end: maxDate ? moment(maxDate).format('YYYY-MM-DD') : null
});
};
/*
* Toggle pointed indicator for an operation.
*/
vm.togglePointed = function(operation, rowform) {
operation.pointed = !operation.pointed;
vm.save(operation);
};
/*
* Toggle cancel indicator for an operation.
*/
vm.toggleCanceled = function(operation) {
operation.canceled = !operation.canceled;
vm.save(operation);
};
/*
* Save an operation and return a promise.
*/
vm.save = function(operation) {
operation.confirmed = true;
return operation.$save().then(function(operation) {
Notification.success('Operation #' + operation.id + ' saved.');
vm.operations = vm.load();
return operation;
}, function(result){
Notification.error(
'Error while saving operation: ' + result.message
);
});
};
/*
* Delete an operation and return a promise.
*/
vm.confirmDelete = function(operation) {
var title = "Delete operation #" + operation.id;
$modal({
templateUrl: operationDeleteTmpl,
controller: function($scope, title, operation, $delete) {
$scope.title = title;
$scope.operation = operation;
$scope.$delete = function() {
$scope.$hide();
$delete($scope.operation);
};
},
locals: {
title: title,
operation: operation,
$delete: vm.delete
}
});
};
vm.delete = function(operation) {
var id = operation.id;
return operation.$delete().then(function() {
Notification.success('Operation #' + id + ' deleted.');
vm.operations = vm.load();
return operation;
}, function(result) {
Notification.error(
'An error occurred while trying to delete operation #' +
id + ':<br />' + result
);
});
};
/*
* Open the popup to modify the operation, save it on confirm.
* @returns a promise.
*/
vm.modify = function(operation) {
// FIXME Alexis Lahouze 2017-06-15 i18n
var title = "Operation";
if (operation.id) {
title = title + " #" + operation.id;
}
$modal({
templateUrl: operationFormTmpl,
controller: function($scope, title, operation, $save) {
$scope.title = title;
$scope.operation = operation;
$scope.$save = function() {
$scope.$hide();
$save($scope.operation);
};
},
locals: {
title: title,
operation: operation,
$save: vm.save
}
});
};
vm.onUpdate = function(minDate, maxDate) {
vm.operations = vm.load(minDate, maxDate);
};
vm.account = Account.get({id: $routeParams.accountId});
};

View File

@ -1,25 +0,0 @@
<!-- 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">{{ title }}</h3>
</div>
<div class="modal-body" id="modal-body">
<p>Voulez-vous supprimer l'opération #{{ operation.id }} ayant pour libellé :<br/>{{ operation.label }}
</p>
</div>
<div class="modal-footer">
<button class="btn btn-danger" type="button" ng-click="$delete()">
Supprimer
</button>
<button class="btn btn-default" type="button" ng-click="$hide()">
Annuler
</button>
</div>
</div>
</div>
</div>

View File

@ -1,7 +0,0 @@
module.exports = function($resource) {
return $resource(
'/api/operation/:id', {
id: '@id'
}
);
};

View File

@ -1,77 +0,0 @@
<!-- vim: set tw=80 ts=2 sw=2 sts=2: -->
<!-- kate: space-indent on; indent-width 2; mixedindent off; -->
<!--
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/>.
-->
<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">{{ title }}</h3>
</div>
<div class="modal-body" id="modal-body">
<form class="form-horizontal simple-form">
<div class="form-group">
<label class="col-sm-4 control-label" for="operation-date">Date</label>
<div class="col-sm-8">
<input class="form-control" id="operation-date" name="operation_date"
type="text" ng-model="operation.operation_date"
bs-datepicker data-date-format="yyyy-MM-dd" data-timezone="UTC"
placeholder="Operation date">
</div>
</div>
<div class="form-group">
<label class="col-sm-4 control-label" for="label">Label</label>
<div class="col-sm-8">
<input class="form-control" id="label" name="label"
ng-model="operation.label" type="text" placeholder="Label">
</input>
</div>
</div>
<div class="form-group">
<label class="col-sm-4 control-label" for="value">Montant</label>
<div class="col-sm-8">
<input class="form-control" id="value" name="value"
ng-model="operation.value" type="number" placeholder="Value">
</input>
</div>
</div>
<div class="form-group">
<label class="col-sm-4 control-label" for="category">Catégorie</label>
<div class="col-sm-8">
<input class="form-control" id="category" name="category"
ng-model="operation.category" type="text" placeholder="Category">
</input>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button class="btn btn-primary" type="button" ng-click="$save()">
OK
</button>
<button class="btn btn-default" type="button" ng-click="$hide()">
Annuler
</button>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,57 @@
// 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 { 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
],
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,150 @@
// 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-md-9">
<balance-chart (onUpdate)="onUpdate($event)"
[account]="account"></balance-chart>
</div>
<div class="col-md-3">
<category-chart
[minDate]="minDate"
[maxDate]="maxDate"
[account]="account"></category-chart>
</div>
</div>
<div class="row">
<table class="table table-striped table-condensed table-hover">
<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="6">
<button class="btn btn-success" (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>
`
})
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.warning]": "operation.balance < 0",
"[class.danger]": "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.text-warning]="operation.balance < 0"
[class.text-danger]="operation.balance < account.authorized_overdraft">
{{ operation.balance | currency:'EUR':true }}
</td>
<td>{{ operation.category }}</td>
<td>
<div class="btn-group btn-group-sm">
<!-- Edit operation, for non-canceled operation. -->
<button type="button" class="btn btn-success"
*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 type="button" class="btn btn-secondary"
*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 type="button" class="btn btn-warning"
(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 type="button" class="btn btn-danger"
(click)="confirmDelete(operation)"
*ngIf="operation.id && !operation.scheduled_operation_id">
<span class="fa fa-trash-o"></span>
</button>
</div>
</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

@ -1,115 +0,0 @@
<!-- vim: set tw=80 ts=2 sw=2 sts=2: -->
<!--
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/>.
-->
<div>
<div class="row">
<div class="col-md-9">
<balance-chart on-update="operationsCtrl.onUpdate(minDate, maxDate)"
account="operationsCtrl.account"/>
</div>
<div class="col-md-3">
<category-chart
min-date="operationsCtrl.minDate"
max-date="operationsCtrl.maxDate"/>
</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="operationsCtrl.add()">
Ajouter
</button>
</td>
</tr>
<tr id="{{ operation.id }}" class="form-inline"
ng-class="{stroke: operation.canceled, italic: !operation.confirmed,
warning: operation.balance < 0, danger: operation.balance < operationsCtrl.account.authorized_overdraft}"
ng-repeat="operation in operationsCtrl.operations | orderBy:'+':true">
<td>
{{ operation.operation_date | date:"yyyy-MM-dd" }}
</td>
<td>
{{ operation.label }}
</td>
<td>
{{ operation.value | currency:"€" }}
</td>
<td ng-class="{'text-warning': operation.balance < 0, 'text-danger':
operation.balance < operationsCtrl.account.authorized_overdraft}">
{{ operation.balance | currency:"€" }}
</td>
<td>
{{ operation.category }}
</td>
<td>
<div class="btn-group btn-group-xs">
<!-- Edit operation, for non-canceled operation. -->
<button type="button" class="btn btn-default"
ng-if="!operation.canceled"
ng-click="operationsCtrl.modify(operation)" title="edit">
<span class="fa fa-pencil-square-o"></span>
</button>
<!-- Toggle pointed operation, for non-canceled operations. -->
<button type="button" class="btn btn-default"
ng-if="!operation.canceled"
ng-click="operationsCtrl.togglePointed(operation)"
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. -->
<button type="button" class="btn btn-default"
ng-click="operationsCtrl.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="operationsCtrl.confirmDelete(operation)">
<span class="fa fa-trash-o"></span>
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>

View File

@ -1,43 +0,0 @@
// vim: set tw=80 ts=4 sw=4 sts=4:
/*
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/>.
*/
/* jshint node: true */
'use strict';
var angular = require('angular');
var ngMessages = require('angular-messages'),
ngUiNotification = require('angular-ui-notification'),
ngStrap = require('angular-strap');
var ScheduleConfig = require('./schedule.config.js');
var ScheduleController = require('./schedule.controller.js');
var ScheduleFactory = require('./schedule.factory.js');
module.exports = angular.module('accountant.scheduler', [
ngMessages,
ngUiNotification,
ngStrap
])
.config(ScheduleConfig)
.factory('ScheduledOperation', ScheduleFactory)
.controller('SchedulerController', ScheduleController)
.name;

View File

@ -1,4 +0,0 @@
module.exports = function($resourceProvider) {
// Keep trailing slashes to avoid redirect by flask..
$resourceProvider.defaults.stripTrailingSlashes = false;
};

View File

@ -1,127 +0,0 @@
var scheduleFormTmpl = require('./schedule.form.tmpl.html'),
scheduleDeleteTmpl = require('./schedule.delete.tmpl.html');
module.exports= function($rootScope, $routeParams, Notification, ScheduledOperation, $log, $modal) {
var vm = this;
// Operation store.
vm.operations = [];
/*
* Add a new operation at the beginning of th array.
*/
vm.add = function() {
var operation = new ScheduledOperation({
// eslint-disable-next-line camelcase
account_id: $routeParams.accountId
});
return vm.modify(operation);
};
/*
* Load operations.
*/
vm.load = function() {
return ScheduledOperation.query({
// eslint-disable-next-line camelcase
account_id: $routeParams.accountId
});
};
/*
* Save operation.
*/
vm.save = function(operation) {
return operation.$save().then(function(operation) {
Notification.success('Scheduled operation #' + operation.id + ' saved.');
vm.operations = vm.load();
return operation;
}, function(result){
$log.error('Error while saving scheduled operation', operation, result);
Notification.error(
'Error while saving scheduled operation: ' + result.message
);
});
};
/*
* Delete an operation and return a promise.
*/
vm.confirmDelete = function(operation) {
var title = "Delete operation #" + operation.id;
$modal({
templateUrl: scheduleDeleteTmpl,
controller: function($scope, title, operation, $delete) {
$scope.title = title;
$scope.operation = operation;
$scope.$delete = function() {
$scope.$hide();
$delete($scope.operation);
};
},
locals: {
title: title,
operation: operation,
$delete: vm.delete
}
});
};
/*
* Delete operation.
*/
vm.delete = function(operation) {
var id = operation.id;
return operation.$delete().then(function() {
Notification.success('Scheduled operation #' + id + ' deleted.');
vm.operations = vm.load();
return operation;
}, function(result) {
Notification.error(
'An error occurred while trying to delete scheduled operation #' +
id + ':<br />' + result
);
});
};
/*
* Open the popup to modify the operation, save it on confirm.
* @returns a promise.
*/
vm.modify = function(operation) {
// FIXME Alexis Lahouze 2017-06-15 i18n
var title = "Operation";
if (operation.id) {
title = title + " #" + operation.id;
}
$modal({
templateUrl: scheduleFormTmpl,
controller: function($scope, title, operation, $save) {
$scope.title = title;
$scope.operation = operation;
$scope.$save = function() {
$scope.$hide();
$save($scope.operation);
};
},
locals: {
title: title,
operation: operation,
$save: vm.save
}
});
};
// Load operations on controller initialization.
vm.operations = vm.load();
};

View File

@ -0,0 +1,40 @@
// vim: set tw=80 ts=2 sw=2 sts=2:
import { Injectable } from '@angular/core';
import { DataSource } from '@angular/cdk/collections';
import { Observable } from 'rxjs';
import { BehaviorSubject } from 'rxjs';
import { Logger } from '@nsalaun/ng-logger';
import { Schedule } from './schedule';
import { ScheduleService } from './schedule.service';
@Injectable()
export class ScheduleDataSource extends DataSource<Schedule> {
private subject: BehaviorSubject<number> = new BehaviorSubject<number>(null);
constructor(
private scheduleService: ScheduleService,
private logger: Logger,
) {
super();
}
load(accountId: number): void {
this.logger.log("In load", accountId);
this.subject.next(accountId);
}
connect(): Observable<Schedule[]> {
return this.subject.asObservable().concatMap((accountId: number) => {
this.logger.log("In connect", accountId);
if(accountId) {
return this.scheduleService.query(accountId);
}
});
}
disconnect() {}
}

View File

@ -1,25 +0,0 @@
<!-- 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">{{ title }}</h3>
</div>
<div class="modal-body" id="modal-body">
<p>Voulez-vous supprimer l'opération #{{ operation.id }} ayant pour libellé :<br/>{{ operation.label }}
</p>
</div>
<div class="modal-footer">
<button class="btn btn-danger" type="button" ng-click="$delete()">
Supprimer
</button>
<button class="btn btn-default" type="button" ng-click="$hide()">
Annuler
</button>
</div>
</div>
</div>
</div>

View File

@ -1,7 +0,0 @@
module.exports = function($resource) {
return $resource(
'/api/scheduled_operation/:id', {
id: '@id'
}
);
};

View File

@ -1,89 +0,0 @@
<!-- 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">{{ title }}</h3>
</div>
<div class="modal-body" id="modal-body">
<form class="form-horizontal simple-form">
<div class="form-group">
<label class="col-sm-4 control-label" for="start-date">Date de début</label>
<div class="col-sm-8">
<input class="form-control" id="start-date" name="start_date"
type="text" ng-model="operation.start_date"
bs-datepicker data-date-format="yyyy-MM-dd" data-timezone="UTC"
placeholder="Scheduled operation start date">
</div>
</div>
<div class="form-group">
<label class="col-sm-4 control-label" for="stop-date">Date de fin</label>
<div class="col-sm-8">
<input class="form-control" id="stop-date" name="stop_date"
type="text" ng-model="operation.stop_date"
bs-datepicker data-date-format="yyyy-MM-dd" data-timezone="UTC"
placeholder="Scheduled operation stop date">
</div>
</div>
<div class="form-group">
<label class="col-sm-4 control-label" for="day">Jour</label>
<div class="col-sm-8">
<input class="form-control" id="day" name="day"
ng-model="operation.day" type="number" placeholder="Day">
</input>
</div>
</div>
<div class="form-group">
<label class="col-sm-4 control-label" for="frequency">Fréquence</label>
<div class="col-sm-8">
<input class="form-control" id="frequency" name="frequency"
ng-model="operation.frequency" type="number" placeholder="Frequency">
</input>
</div>
</div>
<div class="form-group">
<label class="col-sm-4 control-label" for="label">Label</label>
<div class="col-sm-8">
<input class="form-control" id="label" name="label"
ng-model="operation.label" type="text" placeholder="Label">
</input>
</div>
</div>
<div class="form-group">
<label class="col-sm-4 control-label" for="value">Montant</label>
<div class="col-sm-8">
<input class="form-control" id="value" name="value"
ng-model="operation.value" type="number" placeholder="Value">
</input>
</div>
</div>
<div class="form-group">
<label class="col-sm-4 control-label" for="category">Catégorie</label>
<div class="col-sm-8">
<input class="form-control" id="category" name="category"
ng-model="operation.category" type="text" placeholder="Category">
</input>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button class="btn btn-primary" type="button" ng-click="$save()">
OK
</button>
<button class="btn btn-default" type="button" ng-click="$hide()">
Annuler
</button>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,65 @@
// 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 {
MdButtonModule,
MdDialogModule,
MdIconModule,
MdInputModule,
MdListModule,
MdTableModule,
} from '@angular/material';
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 { ScheduleService } from './schedule.service';
import { ScheduleDataSource } from './schedule.dataSource';
import { ScheduleDeleteModalComponent } from './scheduleDeleteModal.component';
import { ScheduleEditModalComponent } from './scheduleEditModal.component';
import { ScheduleFormComponent } from './scheduleForm.component';
import { ScheduleListComponent } from './scheduleList.component';
import { ScheduleListState } from './schedule.states';
@NgModule({
imports: [
HttpClientModule,
CommonModule,
ReactiveFormsModule,
RouterModule.forChild([
ScheduleListState
]),
MdButtonModule,
MdDialogModule,
MdIconModule,
MdInputModule,
MdListModule,
MdTableModule,
NgLoggerModule,
ToastrModule,
NgbModule,
TextMaskModule
],
providers: [
ScheduleService,
ScheduleDataSource,
],
declarations: [
ScheduleDeleteModalComponent,
ScheduleEditModalComponent,
ScheduleFormComponent,
ScheduleListComponent,
],
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,34 @@
// vim: set tw=80 ts=2 sw=2 sts=2:
import { Component, Inject } from '@angular/core';
import { MD_DIALOG_DATA } from '@angular/material';
@Component({
selector: 'schedule-delete-modal',
template: `
<h3 md-dialog-title>{{ title() }}</h3>
<md-dialog-content>
Do you really want to delete schedule #{{ data.schedule.id }} with label:<br/>
{{ data.schedule.label }}
</md-dialog-content>
<md-dialog-actions>
<button md-raised-button color="warn" [md-dialog-close]="data.schedule">
Yes
</button>
<button md-raised-button md-dialog-close>
No
</button>
</md-dialog-actions>
`
})
export class ScheduleDeleteModalComponent {
constructor(
@Inject(MD_DIALOG_DATA) private data: any
) {}
title(): string {
return "Delete schedule #" + this.data.schedule.id;
}
}

View File

@ -0,0 +1,64 @@
// vim: set tw=80 ts=2 sw=2 sts=2:
import { Component, Inject, ViewChild } from '@angular/core';
import { MdDialogRef, MD_DIALOG_DATA } from '@angular/material';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { Schedule } from './schedule';
import { ScheduleFormComponent } from './scheduleForm.component';
@Component({
selector: 'schedule-edit-modal',
template: `
<h3 md-dialog-title>{{ title() }}</h3>
<md-dialog-content>
<schedule-form [schedule]="schedule" (submit)="submit()" #scheduleForm="scheduleForm"></schedule-form>
</md-dialog-content>
<md-dialog-actions>
<button md-raised-button color="primary" [disabled]="!scheduleForm?.form.valid" (click)="submit()">
Save
</button>
<button md-raised-button color="warn" md-dialog-close>
Cancel
</button>
</md-dialog-actions>
`
})
export class ScheduleEditModalComponent {
private schedule: Schedule;
@ViewChild('scheduleForm') scheduleForm: ScheduleFormComponent;
constructor(
@Inject(MD_DIALOG_DATA) public data: any,
public dialogRef: MdDialogRef<ScheduleEditModalComponent>,
) {
this.schedule = data.schedule;
}
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.dialogRef.close(schedule);
}
}

View File

@ -0,0 +1,147 @@
// 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">
<md-list>
<md-list-item>
<md-form-field>
<input mdInput formControlName="startDate"
[textMask]="{mask: dateMask}"
placeholder="Schedule start date">
<md-error *ngIf="startDate.errors?.required">The start date is required.</md-error>
</md-form-field>
</md-list-item>
<md-list-item>
<md-form-field>
<input mdInput formControlName="stopDate"
[textMask]="{mask: dateMask}"
placeholder="Schedule stop date">
<md-error *ngIf="stopDate.errors?.required">The stop date is required.</md-error>
</md-form-field>
</md-list-item>
<md-list-item>
<md-form-field>
<input mdInput formControlName="day"
type="number" placeholder="Day">
<md-error *ngIf="day.errors?.required">The day is required.</md-error>
<md-error *ngIf="day.errors?.min">The day must be greater than 0.</md-error>
<md-error *ngIf="day.errors?.max">The day must be less than or equal to 31.</md-error>
</md-form-field>
</md-list-item>
<md-list-item>
<md-form-field>
<input mdInput formControlName="frequency"
type="number" placeholder="Frequency">
<md-error *ngIf="frequency.errors?.required">The frequency is required.</md-error>
<md-error *ngIf="frequency.errors?.min">The frequency must be positive.</md-error>
</md-form-field>
</md-list-item>
<md-list-item>
<md-form-field>
<input mdInput formControlName="label" placeholder="Label">
<md-error *ngIf="label.errors?.required">The label is required.</md-error>
</md-form-field>
</md-list-item>
<md-list-item>
<md-form-field>
<input mdInput formControlName="value" type="number" placeholder="Value">
<md-error *ngIf="value.errors?.required">The value is required.</md-error>
</md-form-field>
</md-list-item>
<md-list-item>
<md-form-field>
<input mdInput formControlName="category"
placeholder="Category">
<md-error *ngIf="category.errors?.required">The category is required.</md-error>
</md-form-field>
</md-list-item>
</md-list>
</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,201 @@
// vim: set tw=80 ts=2 sw=2 sts=2 :
import { Component, Inject, OnInit } from '@angular/core';
import { MdDialog } from '@angular/material';
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 { ScheduleDataSource } from './schedule.dataSource';
import { ScheduleDeleteModalComponent } from './scheduleDeleteModal.component';
import { ScheduleEditModalComponent } from './scheduleEditModal.component';
import { ScheduleService } from './schedule.service';
import { Schedule } from './schedule';
@Component({
selector: 'schedule-list',
template: `
<div class="containerX">
<div class="container">
<button md-fab color="primary" (click)="add()">
<md-icon>add</md-icon>
</button>
</div>
<div class="container">
<md-table #table [dataSource]="schedules">
<ng-container mdColumnDef="start_date">
<md-header-cell *mdHeaderCellDef>Date de d&eacute;but</md-header-cell>
<md-cell *mdCellDef="let schedule">
{{ schedule.start_date | date: "yyyy-MM-dd" }}
</md-cell>
</ng-container>
<ng-container mdColumnDef="stop_date">
<md-header-cell *mdHeaderCellDef>Date de fin</md-header-cell>
<md-cell *mdCellDef="let schedule">
{{ schedule.stop_date | date: "yyyy-MM-dd" }}
</md-cell>
</ng-container>
<ng-container mdColumnDef="day">
<md-header-cell *mdHeaderCellDef>Jour</md-header-cell>
<md-cell *mdCellDef="let schedule">
{{ schedule.day }}
</md-cell>
</ng-container>
<ng-container mdColumnDef="frequency">
<md-header-cell *mdHeaderCellDef>Fr&eacute;q.</md-header-cell>
<md-cell *mdCellDef="let schedule">
{{ schedule.frequency }}
</md-cell>
</ng-container>
<ng-container mdColumnDef="label">
<md-header-cell *mdHeaderCellDef>Libell&eacute; de l'op&eacute;ration</md-header-cell>
<md-cell *mdCellDef="let schedule">
{{ schedule.label }}
</md-cell>
</ng-container>
<ng-container mdColumnDef="value">
<md-header-cell *mdHeaderCellDef>Montant</md-header-cell>
<md-cell *mdCellDef="let schedule">
{{ schedule.value | currency: "EUR":true }}
</md-cell>
</ng-container>
<ng-container mdColumnDef="category">
<md-header-cell *mdHeaderCellDef>Cat&eacute;gorie</md-header-cell>
<md-cell *mdCellDef="let schedule">
{{ schedule.category }}
</md-cell>
</ng-container>
<ng-container mdColumnDef="actions">
<md-header-cell *mdHeaderCellDef>Actions</md-header-cell>
<md-cell *mdCellDef="let schedule">
<!-- Edit operation. -->
<button md-mini-fab color="primary" (click)="modify(schedule)">
<md-icon>mode_edit</md-icon>
</button>
<!-- Remove operation. -->
<button md-mini-fab color="warn" [hidden]="!schedule.id"
(click)="confirmDelete(schedule)">
<md-icon>delete_forever</md-icon>
</button>
</md-cell>
</ng-container>
<md-header-row *mdHeaderRowDef="displayedColumns"></md-header-row>
<md-row *mdRowDef="let row; columns: displayedColumns;">
</md-row>
</md-table>
</div>
</div>
`
})
export class ScheduleListComponent implements OnInit {
private accountId: number;
private displayedColumns: String[] = [
'start_date', 'stop_date', 'day', 'frequency',
'label', 'value', 'category', 'actions'
];
constructor(
private toastrService: ToastrService,
private scheduleService: ScheduleService,
private logger: Logger,
private ngbModal: NgbModal,
private route: ActivatedRoute,
private schedules: ScheduleDataSource,
private mdDialog: MdDialog,
) {}
ngOnInit() {
this.logger.log("ngOnInit");
this.accountId = +this.route.snapshot.paramMap.get('accountId')
// Load operations on controller initialization.
this.load();
}
load() {
this.logger.log("Loading schedules for accountId", this.accountId);
if(!this.accountId) {
return;
}
this.schedules.load(this.accountId);
}
/*
* Add a new operation at the beginning of th array.
*/
add() {
this.modify(new Schedule());
};
modify(schedule: Schedule) {
let dialogRef = this.mdDialog.open(ScheduleEditModalComponent, {
data: {
schedule: schedule,
}
});
dialogRef.afterClosed().subscribe((schedule: Schedule) => {
if(schedule) {
this.save(schedule);
}
}, (reason) => function(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
);
});
};
confirmDelete(schedule: Schedule) {
let dialogRef = this.mdDialog.open(ScheduleDeleteModalComponent, {
data: {
schedule: schedule,
}
});
dialogRef.afterClosed().subscribe((schedule: Schedule) => {
if(schedule) {
this.delete(schedule);
}
}, (reason) => function(reason) {
this.logger.error("Delete dialog failed", reason);
});
}
delete(schedule: Schedule) {
var id = schedule.id;
return this.scheduleService.delete(schedule).subscribe(() => {
this.toastrService.success('Schedule #' + id + ' deleted.');
this.load();
}, result => {
this.toastrService.error(
'An error occurred while trying to delete schedule #' + id + ':<br />'
+ result.message
);
});
}
};

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 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="schedulerCtrl.add()">
Ajouter
</button>
</td>
</tr>
<tr id="{{ operation.id }}" class="form-inline"
ng-repeat="operation in schedulerCtrl.operations">
<td class="col-md-1">
{{ operation.start_date | date: "yyyy-MM-dd" }}
</td>
<td>
{{ operation.stop_date | date: "yyyy-MM-dd" }}
</td>
<td>
{{ operation.day }}
</td>
<td>
{{ operation.frequency }}
</td>
<td>
{{ operation.label }}
</td>
<td>
{{ operation.value | currency : "€" }}
</td>
<td>
{{ operation.category }}
</td>
<td>
<div class="btn-group btn-group-xs">
<!-- Edit operation. -->
<button type="button" class="btn btn-default"
ng-click="schedulerCtrl.modify(operation)" title="edit">
<span class="fa fa-pencil-square-o"></span>
</button>
<!-- Remove operation. -->
<button type="button" class="btn btn-default"
ng-if="operation.id"
ng-click="schedulerCtrl.confirmDelete(operation)"
title="remove">
<span class="fa fa-trash"></span>
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>

View File

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

View File

@ -5,11 +5,17 @@ const webpack = require('webpack');
module.exports = {
context: path.resolve(__dirname, 'src'),
entry: './app.js',
entry: {
"main": [
'./main.ts'
],
"styles": [
'./main.scss'
]
},
devtool: 'source-map',
resolve: {
// Add '.ts' and '.tsx' as a resolvable extension.
extensions: ['.webpack.js', '.web.js', '.ts', '.tsx', '.js', '.html']
extensions: ['.js', '.ts', '.html'],
},
module: {
rules: [{
@ -28,37 +34,11 @@ module.exports = {
indent: ['error', 4]
},
},
}, {
// typescript linting
enforce: 'pre',
test: /\.tsx?$/,
exclude: /(node_modules|bootstrap)/,
loader: 'tslint-loader',
options: {
configFile: false,
emitErrors: true,
baseConfig: 'angular',
allowJs: true,
configuration: {
extends: ["angular-tslint-rules"],
include: [
'src/**/*'
],
rules: {
indent: ['error', 4]
},
},
},
}, {
test: /\.tsx?$/,
exclude: /node_modules/,
loader: 'ts-loader'
}, {
// Javascript
enforce: 'pre',
test: /\.jsx?$/,
test: /\.js$/,
//include: path.resolve(__dirname, 'src'),
exclude: /(node_modules|bootstrap)/,
loader: 'eslint-loader',
options: {
useEslintrc: false,
@ -80,45 +60,81 @@ module.exports = {
]
},
}, {
test: /\.jsx?$/,
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader'
}, {
test: /\.html$/,
use: [
'ngtemplate-loader?relativeTo=/accountant-ui/src',
'html-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: /\.less$/,
use: [
'style-loader',
'css-loader',
'less-loader',
]
test: /\.ts$/,
exclude: /node_modules/,
loaders: ['awesome-typescript-loader', 'angular2-template-loader?keepUrl=true']
}, {
test: /\.html$/,
loader: 'raw-loader'
}, {
test: /\.css$/,
use: [
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 HtmlWebpackPlugin({
title: 'Accountant',
template: 'index.ejs'
}),
new webpack.ProvidePlugin({
"window.jQuery": "jquery"
}),
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: 'js/bundle.js',
filename: '[name].bundle.js',
chunkFilename: '[name].chunk.js'
//publicPath: 'js'
},
devServer: {