Compare commits

..

258 Commits
2.0.2 ... 3.1.0

Author SHA1 Message Date
2b03bc798e new build 310 2020-05-21 11:26:26 +05:30
482556d378 Merge branch 'resend-email' into 'master'
Add Resend Email Option

See merge request mohit.panjvani/crater-web!236
2020-05-21 05:51:36 +00:00
deb525af6e build 310 2020-05-19 13:22:33 +05:30
cb2bfbb91c Add steps for update app 2020-05-19 07:50:58 +00:00
654395a175 Add steps for update app 2020-05-19 07:50:58 +00:00
e31b60bc48 Refactor Self Update Endpoints and Add Update Console Command 2020-05-17 07:04:43 +00:00
183953f4c4 Refactor Self Update Endpoints and Add Update Console Command 2020-05-17 07:04:43 +00:00
a24d8d3ebc fix updater issue 2020-05-13 16:50:45 +05:30
4ca574c581 fix migration errors 2020-05-13 12:47:06 +05:30
c7ce8c87dd new build 2020-05-13 11:31:37 +05:30
f64d546672 refactor 310 update listener 2020-05-13 11:12:33 +05:30
d4a1f1a784 fix item selection and item loader 2020-05-12 13:23:06 +05:30
e07532961e reset items on selection 2020-05-12 13:11:29 +05:30
450c265ded Merge branch 'refactor-report-pdfs' into 'master'
Refactor report pdfs

See merge request mohit.panjvani/crater-web!233
2020-05-11 15:48:49 +00:00
f8502c3ca8 Merge branch 'customer-delete' into 'master'
Customer delete label

See merge request mohit.panjvani/crater-web!237
2020-05-11 15:47:33 +00:00
899da6990d Merge branch 'new-issues' into 'master'
New issues

See merge request mohit.panjvani/crater-web!239
2020-05-11 15:47:03 +00:00
e7675f938e refactor estimate to invoice endpoint 2020-05-11 19:05:50 +05:30
b08138e9e0 refactor estimate to invoice endpoint 2020-05-11 18:34:20 +05:30
5df4abdc4b add aws-sdk-php package 2020-05-11 18:15:36 +05:30
a2fa8afa72 fix translation issue 2020-05-11 18:09:34 +05:30
7670cd67dc Merge branch 'template-refactor' into 'master'
refactor invoice2

See merge request mohit.panjvani/crater-web!238
2020-05-11 12:24:22 +00:00
8446ac2b27 refactor invoice2 2020-05-11 16:19:02 +05:30
63a80e44d5 Customer delete label 2020-05-11 14:33:11 +05:30
076df75322 Merge branch 'template-refactor' into 'master'
Template refactor

See merge request mohit.panjvani/crater-web!232
2020-05-11 07:03:45 +00:00
11db99da73 refactor invoice 1 style 2020-05-09 18:50:58 +05:30
050dca5a50 Add copy pdf url option 2020-05-09 14:37:22 +05:30
3c096f1386 Add Resend Email Option 2020-05-09 14:22:13 +05:30
0f3e8fce3b refactor heading text in payment 2020-05-08 20:55:00 +05:30
325f90bba5 Fix issues on invoice template 2020-05-08 18:49:02 +05:30
a739a938fc Fix estimate template issues 2020-05-08 16:52:48 +05:30
b30e3a9b11 merge with github 2020-05-08 11:11:57 +05:30
fc1a7c7438 refactor reports pdfs 2020-05-07 18:59:50 +05:30
6046113cb1 Refactor Invoice and Payment templates 2020-05-07 16:42:04 +05:30
611ffafec5 refactor sales customers pdf 2020-05-06 20:41:15 +05:30
c497b906df refactor taxt report pdf 2020-05-06 20:32:10 +05:30
c68fce19f9 refactor expenses pdf 2020-05-06 19:02:02 +05:30
f8913531b6 refactor profit loss pdfs 2020-05-06 18:09:28 +05:30
30f76e2088 Refactor Estimate templates 2020-05-06 18:02:27 +05:30
39556892cd refactor sales customers 2020-05-05 20:58:20 +05:30
2fd66bf748 refactor sales items and customer pdfs 2020-05-05 20:49:51 +05:30
d4f1428d5f refactor sales customers pdf template 2020-05-05 20:03:51 +05:30
189141c84d Refactor estimate pdf template 2020-05-05 18:21:25 +05:30
f8ccfece09 update templates 2020-05-05 13:12:26 +05:30
9a7c926d53 fix 310 update issues 2020-05-02 18:53:51 +05:30
c5c1674153 Merge branch 'master' 2020-05-02 13:37:36 +05:30
8562ee5414 Merge branch 'settings-customization-payments' into 'master'
Payments and UnitItems in doubles issue fixed...

See merge request mohit.panjvani/crater-web!218
2020-04-27 16:09:25 +00:00
06c66a756c Payments and UnitItems in doubles issue fixed... 2020-04-27 18:54:24 +05:30
b66d07d21b Merge pull request #201 from rexlManu/master
Updated German Language
2020-04-27 12:14:23 +05:30
05001b6a79 Merge pull request #170 from RobinDev/patch-1
Update french language
2020-04-21 14:52:02 +05:30
f02f4ba9d3 Updated German Language 2020-04-21 07:31:33 +02:00
fbace98aac add missing translation 2020-04-20 21:12:03 +02:00
8f0af3dcd6 Merge branch 'master' of https://github.com/bytefury/crater 2020-04-20 11:23:00 +05:30
e8e44c5dc8 Merge pull request #180 from alessandrofuda/master
IT Italian translation
2020-04-18 13:38:21 +05:30
ac33164342 Merge branch '3.1.0' into 'master'
add listener 310

See merge request mohit.panjvani/crater-web!202
2020-04-18 07:29:08 +00:00
5c7c0d84ea Merge branch 'refactor-crater-3.0.0' into 'master'
Refactor line-chart

See merge request mohit.panjvani/crater-web!203
2020-04-18 07:15:52 +00:00
7ca725ac37 refactor line-chart 2020-04-17 19:53:08 +05:30
510a4b3dbb add listener 310 2020-04-17 19:05:10 +05:30
0f130ab1b8 Update fr langugage 2020-04-17 09:57:38 +02:00
25114009e3 remove unused components and update eslint + prettier config 2020-03-30 11:40:45 +05:30
79c16d74ce IT Italian translation 2020-03-20 18:18:27 +01:00
742e1e445a Merge branch 'expense-refactor' into 'master'
add customer in expense

See merge request mohit.panjvani/crater-web!172
2020-03-20 07:12:39 +00:00
386f96d60e Merge branch 'version301' into 'master'
add new minor version update 3.0.1

See merge request mohit.panjvani/crater-web!169
2020-03-20 07:12:27 +00:00
82d85af672 refactor and merge backend 2020-03-18 18:21:53 +05:30
4d1b267688 Merge branch 'expense-refactor' of https://gitlab.com/mohit.panjvani/crater-web into expense-refactor 2020-03-18 17:41:15 +05:30
bc99ad63a6 add user to expense 2020-03-18 17:40:03 +05:30
4bb44f8c93 add customer filter on index 2020-03-18 17:19:09 +05:30
c72265ed50 add languages 2020-03-17 19:38:59 +05:30
b8958c9eb6 add customer in expense 2020-03-17 19:20:42 +05:30
e33e314cb7 new frontend build 2020-03-09 15:20:59 +05:30
84cebee9da Merge branch 'master' of https://github.com/bytefury/crater 2020-03-09 15:19:54 +05:30
34d3cf7ae8 Merge branch 'item-refactor' into 'master'
refactor item search problem

See merge request mohit.panjvani/crater-web!171
2020-03-09 09:49:26 +00:00
93d0da836a Merge pull request #172 from digitalsign/master
Change project name in composer.json
2020-02-26 14:35:21 +05:30
fd51276948 Change project name in composer.json 2020-02-26 13:31:57 +08:00
d6274854ba refactor search problem 2020-02-19 19:10:43 +05:30
ea4bd1a31d Merge pull request #154 from lukasmu/bugfix/expenses-note
Fixed small bug that might occur when an expense is updated
2020-02-09 12:01:43 +05:30
286e047963 Merge pull request #129 from deanhouseholder/master
Detect if no .env file exists and add it on composer commands.
2020-02-09 12:01:25 +05:30
be16f48f3d Merge pull request #153 from MakerLab-Dev/patch-3
Improved error handling
2020-02-09 12:00:23 +05:30
ebea1e0813 add new minor version update 3.0.1 2020-02-07 17:20:32 +05:30
cc8d08f829 Merge pull request #142 from miraro3/master
Added Kyrgyzstani som
2020-02-07 16:42:15 +05:30
aacffc22eb fix merge conflict 2020-02-03 21:15:07 +05:30
d71ca4ffb9 Fixed small bug that might occur when an expense is updated
Previoulsy a string with contents "null" is transmitted and saved to the database when updating an expense with an empty note. This fix prevents that.
Please note that this fix still needs to be compiled with npm.
2020-02-01 14:52:07 +01:00
186004f7f8 fix merge conflicts 2020-01-31 14:28:16 +05:30
c2eb22d666 Merge pull request #137 from MakerLab-Dev/patch-1
cross-env should only be in devDependencies
2020-01-31 01:26:32 +05:30
af189b15b6 Small fix 2020-01-30 20:48:16 +01:00
1c19be85c3 Improved error handling 2020-01-30 20:47:51 +01:00
4bb4362d23 Removed unnecessary notification
Removed unnecessary notification as the improved error handler will show it
2020-01-30 20:45:39 +01:00
f6f66b3ae6 Merge pull request #152 from mdpoulter/email-subject
More descriptive email fields.
2020-01-31 00:21:29 +05:30
b4ccecbcf1 Merge pull request #149 from proea/master
Fix for "Empty PDF rendered in Docker #69"
2020-01-31 00:11:51 +05:30
92f1f196bb Update Dockerfile 2020-01-27 22:38:47 +03:00
ee14070a7b Update Dockerfile
slight change
2020-01-27 22:37:41 +03:00
8ce7e14a02 Add name in From field. 2020-01-27 20:37:25 +02:00
3401ca049e Make viewed emails more descriptive. 2020-01-27 14:47:02 +02:00
5dcc7b9efd Add more descriptive subject lines. 2020-01-27 14:45:40 +02:00
06a538bb81 Merge branch 'build-version300' into 'master'
Build version300

See merge request mohit.panjvani/crater-web!162
2020-01-27 09:26:58 +00:00
7ab0419f27 build version300 2020-01-27 14:47:45 +05:30
a7275aaa42 fix report download & payments delete issue 2020-01-27 14:46:40 +05:30
bc4e6a05ea fix Surround DB_PASSWORD with quotes #104 issue 2020-01-27 14:22:21 +05:30
ca170f5a87 fix dashbord & using FROM_MAIL_ADDRESS issues 2020-01-27 13:43:48 +05:30
22e7e96dfa fix new lines in address field issue 2020-01-27 11:57:47 +05:30
fcfd1ddb7a build 300 2020-01-27 11:19:22 +05:30
daf8c9265b fix tab & language issue 2020-01-27 11:18:11 +05:30
406d098172 Update Dockerfile
Generation does not work due to:
iconv(): Wrong charset, conversion from `UTF-8' to `UTF-8//IGNORE' is not allowed

Installing the library gnu-libiconv will solve the problem
2020-01-26 23:18:15 +03:00
824d2e3e8d update app version 2020-01-26 12:32:57 +05:30
3cd975dbbd new build 2020-01-25 13:33:50 +05:30
8e50c36a71 Merge branch 'master' 2020-01-25 13:12:39 +05:30
b499741ab4 change button style of primary actions on view pages 2020-01-25 13:12:20 +05:30
b409cdb913 Merge branch 'auto-update' into 'master'
fix auto update issue in dev

See merge request mohit.panjvani/crater-web!160
2020-01-22 16:10:36 +00:00
13e56105e3 fix auto update issue in dev 2020-01-22 13:39:59 +05:30
f68e86e4cf Added Kyrgyzstani som 2020-01-20 22:37:03 +01:00
da996c1f33 Merge branch 'master' of https://github.com/bytefury/crater 2020-01-19 11:49:35 +05:30
0990ce4678 cross-env should only be in devDependencies 2020-01-18 16:29:48 +01:00
bb6fb2f49d Merge pull request #131 from jjoseba/master
Spanish corrections
2020-01-08 15:39:16 +05:30
400296575e Updated Spanish translation 2020-01-08 10:58:26 +01:00
d58c790b1f Merge branch 'build-300' into 'master'
fix mailgun config issue & build 300

See merge request mohit.panjvani/crater-web!153
2020-01-07 08:32:04 +00:00
c674c2ab9e build verison 300 2020-01-07 13:10:55 +05:30
d79692cf3b mail configuration issue 2020-01-07 12:33:03 +05:30
353c2479f1 Detect if no .env file exists and add it on composer commands. 2020-01-06 10:55:34 -07:00
0176a854b8 Merge branch 'refactor-master' into 'master'
refactor payment view and mutation

See merge request mohit.panjvani/crater-web!151
2020-01-06 16:08:53 +00:00
00548ea908 refactor payment view 2020-01-06 19:03:08 +05:30
09e335a8a7 fix conflicts 2020-01-06 14:48:08 +05:30
53e2ed253f Merge branch 'master' 2020-01-06 14:47:06 +05:30
06b035d9ac Merge branch 'build-300' into 'master'
fix payment issue & build version 300

See merge request mohit.panjvani/crater-web!150
2020-01-06 09:16:49 +00:00
5c88cbcc42 Merge branch 'master' 2020-01-06 14:45:09 +05:30
d9b175a676 fix payment issue & build version 300 2020-01-06 14:08:29 +05:30
f5b9bc95c6 Merge branch 'build-300' into 'master'
build version 300

See merge request mohit.panjvani/crater-web!149
2020-01-06 06:47:08 +00:00
586dcdea0d build version 300 2020-01-06 12:02:12 +05:30
50957fc179 Merge pull request #127 from hypnodev/typo-fix-crop-popup
Typo fix crop popup
2020-01-05 23:41:51 +05:30
c725e4744b Compiled assets 2020-01-05 19:00:29 +01:00
7479ce237e Typo fix #126 2020-01-05 18:57:46 +01:00
84420441c0 Merge pull request #2 from bytefury/master
Merge pull request #2 from bytefury/master
2020-01-05 14:03:26 +01:00
ac96721e87 build 220 2020-01-05 13:50:08 +05:30
db4c7f5e32 fix merge conflicts 2020-01-05 13:45:10 +05:30
87dc78eea0 Merge pull request #115 from hypnodev/add-available-lang-to-onboarding
Added available languages to Onboarding
2020-01-05 13:39:02 +05:30
e9c2898056 Merge pull request #119 from hypnodev/fix-http-mixed-request
Fix http mixed request
2020-01-05 13:37:27 +05:30
b9c4570137 Merge pull request #121 from MakerLab-Dev/translations-updates
Translations Updates
2020-01-05 13:35:01 +05:30
8e2525cc6c new build 220 2020-01-05 13:34:01 +05:30
8862a93f23 fix border-left jump on active selection 2020-01-05 13:08:42 +05:30
82efd88920 Merge branch 'sidebar-view' into 'master'
sidebar-view modified

See merge request mohit.panjvani/crater-web!132
2020-01-05 07:24:12 +00:00
0ef528d296 Merge branch 'payment_receipt' into 'master' 2020-01-05 07:22:36 +00:00
4c33a5d88c Fix Invoice/Estimate template issues and Add Payment Receipt, Custom Payment Modes and Item units 2020-01-05 07:22:36 +00:00
d64d06181b Translations Updates
Spanish translation: 100%
Removed a blank space in English translation
Changed the copyright year to 2020 in all translations
2020-01-03 17:21:43 +01:00
ef9cf2db22 Fix mixed content when use endpoint for categories list 2020-01-03 00:14:05 +01:00
96c295a003 Added available languages to Onboarding 2019-12-31 10:20:13 +01:00
22528f5b66 Merge pull request #1 from bytefury/master
Same as bytefury/crater
2019-12-31 10:15:17 +01:00
1cd9c72537 Merge pull request #108 from hypnodev/fix-postgresql-case-sensitiive-bug
Fix case sensitive PostgreSQL bug
2019-12-31 09:26:46 +05:30
7253b43eb4 Fix case sensitive PostgreSQL bug 2019-12-29 14:50:03 +01:00
181964cf03 Merge pull request #105 from PHClement/patch-1
Update fr.json
2019-12-28 21:13:42 +05:30
5914245ae4 Merge pull request #106 from cilas/feature/translate-app-to-portuguese
Feature/translate app to portuguese
2019-12-28 21:11:32 +05:30
1bf3d28d4e Update fr.json
Correct some mistake, please approve it, thank you ;)
2019-12-28 01:45:25 +01:00
dbcbd93ace Translate to Brazilian Portuguese Complete 2019-12-27 21:45:15 -03:00
1729c6a308 Translated partial wizard section 2019-12-27 17:42:54 -03:00
019493cbfa Translated settings section 2019-12-27 15:55:34 -03:00
60540ba966 Translated reports section 2019-12-27 13:14:53 -03:00
601ad419ec Translated login section 2019-12-27 12:54:21 -03:00
80e7bab891 Translated expenses section 2019-12-27 12:50:37 -03:00
f5c8befbf0 Translated payments section 2019-12-27 12:33:56 -03:00
09c984baa7 Translated credit_notes section 2019-12-27 12:27:42 -03:00
0d93260672 Translated invoice section 2019-12-27 12:24:59 -03:00
3e9e217f92 Translate estimate session 2019-12-27 00:53:16 -03:00
3cd8859c62 add Brazilian Portuguese language to CompanyController 2019-12-27 00:14:57 -03:00
4493b4a419 add language pt_BR to plugins 2019-12-27 00:11:41 -03:00
79e3e70bd6 add pt_BR language option 2019-12-26 22:47:09 -03:00
56a955befd delete yarnlock file 2019-12-25 19:29:22 +05:30
496c28f80d remove transifex from readme 2019-12-25 19:27:41 +05:30
7a59f3fe0c refactor variable in estimate view scss 2019-12-23 21:05:48 +05:30
3d6875a532 refactor css variables 2019-12-23 21:00:26 +05:30
15bf380f4f Merge pull request #93 from surajjdhv/master
Fixed incorrectly named language (ar.json) file
2019-12-23 12:54:04 +05:30
0b910db039 Fixed incorrectly named language (ar.json) file 2019-12-22 21:47:30 +05:30
0e5e8f602f update error message for db file permission issue 2019-12-21 23:02:05 +05:30
d9db0f9401 Merge pull request #80 from eboye/master
Add Serbian Dinar currency
2019-12-21 14:08:10 +05:30
c0da7c7339 Merge pull request #89 from claretnnamocha/master
Docs
2019-12-20 19:15:16 +05:30
7e7599b4a7 Merge branch 'master' into master 2019-12-20 09:35:03 +01:00
99cd88e6c6 Revert "Update CompanyController.php"
This reverts commit 053a06229c.
2019-12-20 09:32:29 +01:00
053a06229c Update CompanyController.php 2019-12-20 09:30:15 +01:00
1a6e8280a8 update readme 2019-12-18 20:58:44 +05:30
125e8be83c Merge pull request #87 from hsnapps/master
Added the Arabic language localization
2019-12-18 20:56:06 +05:30
4d89ca2101 Added the Arabic language localization 2019-12-18 10:18:04 +03:00
e8aee3bb32 Add Serbian Dinar currency 2019-12-17 11:32:58 +01:00
d3c7ca75f0 Documenting web routes 2019-12-16 20:41:59 +01:00
694d5f56d5 Documenting Controllers 2019-12-16 20:17:43 +01:00
a48439785c Merge pull request #68 from BirkhoffLee/master
Docker: fix update functionality
2019-12-14 21:23:24 +05:30
7c9bd84f00 Docker: fix update functionality
Added php zip extension
2019-12-14 15:43:01 +00:00
bd5a93d81c Merge branch 'master' of https://github.com/bytefury/crater 2019-12-14 19:56:24 +05:30
858e10953b fix grammar 2019-12-14 19:13:59 +05:30
3617032735 Merge branch 'build210' into 'master'
fix installation issue & build 210

See merge request mohit.panjvani/crater-web!143
2019-12-14 13:27:26 +00:00
75ddc51b1e fix installation issue & build 210 2019-12-14 18:50:13 +05:30
1dfa36e396 Merge branch 'build210' into 'master'
fix estimate & invoice tax issue

See merge request mohit.panjvani/crater-web!142
2019-12-14 13:06:38 +00:00
d9e9a5a540 fix estimate & invoice tax issue 2019-12-14 18:29:46 +05:30
ea6e11c324 Merge branch 'build210' into 'master'
fix version update issue

See merge request mohit.panjvani/crater-web!141
2019-12-14 12:48:37 +00:00
f55dfe0b46 fix version update issue 2019-12-14 17:12:47 +05:30
3eac3b8af5 Merge branch 'build210' into 'master'
fix auto auto generate issue & build210

See merge request mohit.panjvani/crater-web!140
2019-12-14 11:20:01 +00:00
2b2bd4351a fix auto auto generate issue & build210 2019-12-14 15:45:26 +05:30
b9c32bbdc1 Merge branch 'build-version210' into 'master'
Build version210

See merge request mohit.panjvani/crater-web!139
2019-12-14 09:49:21 +00:00
bceffbf6a0 build version210 2019-12-14 14:43:55 +05:30
7c6a40374d fix tax percent & pdf money format issue 2019-12-14 14:42:34 +05:30
d04e142a3e build version210 2019-12-14 13:59:59 +05:30
f11436736b add settings use in Update listener 2019-12-14 13:57:18 +05:30
9271ceba45 Merge branch 'master' of https://gitlab.com/mohit.panjvani/crater-web into build-version210 2019-12-14 13:55:26 +05:30
c88eb24265 fix pdf & installation issue 2019-12-14 13:54:35 +05:30
5eb0a04378 Update issue templates 2019-12-14 13:03:42 +05:30
d926073095 add ios link to readme 2019-12-14 12:55:08 +05:30
a691969025 Merge branch 'master' of https://github.com/bytefury/crater 2019-12-14 12:35:40 +05:30
7df06fb005 fix invoice number validation 2019-12-14 12:35:32 +05:30
45db850025 Merge pull request #59 from vkiranmaniya/master
Removed JSON SQL datatype, Replaced with TEXT Type
2019-12-14 12:32:31 +05:30
18a50315ba Removed json SQL datatype, Replaced with text
The JSON SQL datatype is not supported in some of SQL server(i.e. 5.6.44). Replaced it with text datatype.
2019-12-13 17:24:07 +05:30
bce1b4bb3e build 210 2019-12-13 17:03:55 +05:30
ddd204105f fix installation issue 2019-12-13 17:03:44 +05:30
e030d4b9d0 Merge branch 'build-210' into 'master'
build version210

See merge request mohit.panjvani/crater-web!138
2019-12-13 10:48:54 +00:00
302968225a build version210 2019-12-13 15:22:37 +05:30
e59bf288ce Merge branch 'pdf-issues' into 'master'
Fix PDF issues

See merge request mohit.panjvani/crater-web!137
2019-12-13 09:43:32 +00:00
b1fcd90b62 refactor format_money_pdf helpers 2019-12-13 14:40:17 +05:30
ddb0ff1b8a fix pdf currency display issue 2019-12-13 14:34:21 +05:30
ce99fa3d82 Fix PDF issues 2019-12-13 13:31:26 +05:30
e28c89085d Merge branch 'master' 2019-12-12 20:42:17 +05:30
34f7e33abc Merge branch 'master' of https://github.com/bytefury/crater 2019-12-12 20:31:57 +05:30
80be7a492d Merge pull request #50 from nhedger/master
Add missing currency code to customer create/edit form
2019-12-12 20:27:45 +05:30
387cb4490d Merge branch 'add-customization' into 'master'
update invoice, estimate & payment vars

See merge request mohit.panjvani/crater-web!135
2019-12-12 14:56:10 +00:00
09829a559e Merge branch 'pdf-issue' into 'master'
fix Pdf issue

See merge request mohit.panjvani/crater-web!136
2019-12-12 14:55:04 +00:00
b06fc5f0b9 Add missing currency code to customer create form 2019-12-12 14:37:16 +01:00
5f7401f622 update invoice, estimate & payment vars 2019-12-12 16:04:12 +05:30
4f6dae919b Merge branch 'add-customization' into 'master'
Add customization

See merge request mohit.panjvani/crater-web!134
2019-12-12 10:05:16 +00:00
36be395579 Remove unwanted code 2019-12-12 14:59:54 +05:30
79e77f9e16 fix pdf lang issue 2019-12-12 14:20:31 +05:30
cbf0af2120 Refactor Customization Page 2019-12-12 13:46:09 +05:30
92f754e888 Merge pull request #43 from nhedger/master
Prepend currency code to label to allow searching by currency code
2019-12-12 13:25:21 +05:30
a6896eaa01 Merge branch 'master' of https://gitlab.com/mohit.panjvani/crater-web into pdf-issue 2019-12-12 13:17:52 +05:30
01f3646869 add meta tag 2019-12-12 11:50:25 +05:30
b2918e9dbb Merge pull request #44 from JaxWilko/fix-incorrect-headers-sent
Removed a space that forced apache to send the wrong headers
2019-12-12 08:59:00 +05:30
0a064ec5ba Removed a space that forced apache to send the wrong headers 2019-12-12 00:50:45 +00:00
5f0b4b3496 Prepend currency code to selector label. 2019-12-11 20:36:46 +01:00
17b59f0d19 Merge branch 'master' 2019-12-11 20:16:23 +05:30
283b910cc3 remove state & city routes from api.php and fix cancel spelling mistake 2019-12-11 20:16:11 +05:30
81739827c0 Add customization 2019-12-11 17:02:28 +05:30
90edc3a85e Merge branch 'dashboard-initial-loading' into 'master'
refactor initial data loading in dashboard

See merge request mohit.panjvani/crater-web!133
2019-12-11 10:18:58 +00:00
655c2a7849 Merge branch 'master' of https://github.com/bytefury/crater 2019-12-11 13:35:32 +05:30
ca833d174e update readme 2019-12-11 13:35:13 +05:30
33e8381fc4 Merge pull request #8 from BirkhoffLee/master
Initial Docker support
2019-12-11 13:04:43 +05:30
9b5125d440 Merge branch 'master' of https://github.com/bytefury/crater 2019-12-11 12:45:03 +08:00
2899021804 Update example env in accordance w/ Docker setup 2019-12-10 23:53:28 +08:00
d8f6d03d1e Docker: rename nginx container name 2019-12-10 23:52:23 +08:00
c474e98925 Docker: copy example env file when building 2019-12-10 23:40:59 +08:00
799d212d9b refactor classes on invoice and estimate view page 2019-12-10 11:15:49 +05:30
9424dc6c27 refactor initial data loading in dashboard 2019-12-09 19:27:21 +05:30
fa15502ce7 refactor primary color-var 2019-12-09 19:20:11 +05:30
1f4d3bf784 sidebar-view modified 2019-12-09 18:58:53 +05:30
3692373cd2 Docker: switch from passport:install to passport:keys during build 2019-12-09 19:25:57 +08:00
0c71356f59 Docker: remove migration instruction 2019-12-04 18:31:06 +08:00
e539bb501d Docker: persistant database storage in example docker-compose.yaml 2019-12-04 17:22:59 +08:00
2fcd169270 Downgrade Docker image php version to 7.3.12 2019-12-04 17:19:16 +08:00
14d71fedb3 Rename docker-compose.yml.example -> docker-compose.yaml.example 2019-12-04 14:54:28 +08:00
7fe9a4c2a2 Fix Dockerfile and docker-compose.yml
1. Generate a testing SQLite db on build
2. Optimize image layer caching
3. Fix permissions
4. Simplify nginx configuration
5. Fix nginx infinite redirect loop (couldn't access app folder)
2019-12-04 14:49:57 +08:00
7b697a477e Fix MariaDB config in Docker compose
by specifying a root password.
2019-12-04 11:20:50 +08:00
146cf835b9 Fix composer install in Dockerfile 2019-12-04 08:59:01 +08:00
ec87e72547 Add .dockerignore file
This excludes unnecessary files for production use on Docker image
build.
2019-12-04 08:26:31 +08:00
bf2e8c9c99 Use multi-stage build for Docker image
1. Allows to build the composer dependencies with official composer img
2. php-fpm --nodaemonize: Force to stay in foreground and ignore daemo-
   nize option from configuration file
2019-12-04 08:23:11 +08:00
b6096aadfa Add MariaDB to docker-compose example
1. Gives an example set up of a MySQL database in docker-compose
2. `php` now depends on `db`, and `nginx` depends on `php`. This
ensures the integrity of the stack.
2019-12-03 21:31:56 +08:00
1cbc41c3ce Simplify docker-ext-install in Dockerfile 2019-12-03 20:24:54 +08:00
8bc5ea2d5e Initial Docker support
This commit adds:
1. A Dockerfile that runs PHP 7.2 FPM on Alpine Linux
2. A example docker-compose file that simplifies deployment
2019-11-21 12:54:22 +08:00
213 changed files with 14851 additions and 18029 deletions

10
.dockerignore Normal file
View File

@ -0,0 +1,10 @@
.dockerignore
.gitignore
*.md
.git/
.idea/
.DS_Store/
docker-compose.*
LICENSE
nginx.conf
yarn.lock

View File

@ -1,15 +1,15 @@
APP_ENV=production
APP_KEY=base64:kgk/4DW1vEVy7aEvet5FPp5un6PIGe/so8H0mvoUtW0=
APP_DEBUG=false
APP_DEBUG=true
APP_LOG_LEVEL=debug
APP_URL=http://crater.test
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_HOST=db
DB_PORT=3306
DB_DATABASE=crater
DB_USERNAME=root
DB_PASSWORD=bytefury
DB_USERNAME=crater
DB_PASSWORD="crater"
BROADCAST_DRIVER=log
CACHE_DRIVER=file

View File

@ -2,9 +2,17 @@
"root": true,
"extends": [
"plugin:vue/recommended",
"standard"
"eslint:recommended",
"prettier/vue",
"plugin:prettier/recommended"
],
"rules": {
"vue/max-attributes-per-line" : 3
"vue/max-attributes-per-line": ["error", {
"singleline": 10,
"multiline": {
"max": 1,
"allowFirstLine": false
}
}]
}
}

26
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@ -0,0 +1,26 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Please complete the following information:**
- Crater version:
- PHP version:
- Database type and version:
**Optional info**
- OS: [e.g. Ubuntu]
- Browser: [e.g. chrome, safari]

View File

@ -0,0 +1,17 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.

2
.gitignore vendored
View File

@ -11,3 +11,5 @@ Homestead.yaml
.rnd
/.expo
/.vscode
docker-compose.yml
docker-compose.yaml

5
.prettierrc.json Normal file
View File

@ -0,0 +1,5 @@
{
"semi": false,
"singleQuote": true,
"tabWidth": 2
}

53
Dockerfile Normal file
View File

@ -0,0 +1,53 @@
##### STAGE 1 #####
FROM composer as composer
# Copy composer files from project root into composer container's working dir
COPY composer.* /app/
# Copy database directory for autoloader optimization
COPY database /app/database
# Run composer to build dependencies in vendor folder
RUN composer install --no-scripts --no-suggest --no-interaction --prefer-dist --optimize-autoloader
# Copy everything from project root into composer container's working dir
COPY . /app
RUN composer dump-autoload --optimize --classmap-authoritative
##### STAGE 2 #####
FROM php:7.3.12-fpm-alpine
# Use the default production configuration
RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"
RUN apk add --no-cache libpng-dev libxml2-dev oniguruma-dev libzip-dev gnu-libiconv && \
docker-php-ext-install bcmath ctype json gd mbstring pdo pdo_mysql tokenizer xml zip
ENV LD_PRELOAD /usr/lib/preloadable_libiconv.so php
# Set container's working dir
WORKDIR /app
# Copy everything from project root into php container's working dir
COPY . /app
# Copy vendor folder from composer container into php container
COPY --from=composer /app/vendor /app/vendor
RUN touch database/database.sqlite && \
cp .env.example .env && \
php artisan config:cache && \
php artisan passport:keys && \
php artisan key:generate && \
chown -R www-data:www-data . && \
chmod -R 755 . && \
chmod -R 775 storage/framework/ && \
chmod -R 775 storage/logs/ && \
chmod -R 775 bootstrap/cache/
EXPOSE 9000
CMD ["php-fpm", "--nodaemonize"]

View File

@ -7,8 +7,8 @@ use Crater\Country;
class Address extends Model
{
const BILLING_TYPE = 'BILLING';
const SHIPPING_TYPE = 'SHIPPING';
const BILLING_TYPE = 'billing';
const SHIPPING_TYPE = 'shipping';
protected $fillable = [
'name',

View File

@ -0,0 +1,51 @@
<?php
namespace Crater\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Filesystem\Filesystem;
class ResetApp extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'reset:app';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Clean database, database_created and public/storage folder';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
if ($this->confirm('Do you wish to continue? This will delete your tables')) {
Artisan::call('migrate:reset --force');
\Storage::disk('local')->delete('database_created');
// $file = new Filesystem;
// $file->cleanDirectory('public/storage');
}
}
}

View File

@ -0,0 +1,189 @@
<?php
namespace Crater\Console\Commands;
use Illuminate\Console\Command;
use Crater\Space\Updater;
use Crater\Setting;
class UpdateCommand extends Command
{
public $installed;
public $version;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'crater:update';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Automatically update your crater app';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*/
public function handle()
{
set_time_limit(3600); // 1 hour
$this->installed = $this->getInstalledVersion();
$this->version = $this->getLatestVersion();
if (!$this->version) {
$this->info('No Update Available! You are already on the latest version.');
return;
}
if (!$this->confirm("Do you wish to update to {$this->version}?")) {
return;
}
if (!$path = $this->download()) {
return;
}
if (!$path = $this->unzip($path)) {
return;
}
if (!$this->copyFiles($path)) {
return;
}
if (!$this->migrateUpdate()) {
return;
}
if (!$this->finish()) {
return;
}
$this->info('Successfully updated to ' . $this->version);
}
public function getInstalledVersion()
{
return Setting::getSetting('version');
}
public function getLatestVersion()
{
$this->info('Your currently installed version is ' . $this->installed);
$this->line('');
$this->info('Checking for update...');
try {
$response = Updater::checkForUpdate($this->installed);
if ($response->success) {
return $response->version->version;
}
return false;
} catch (\Exception $e) {
$this->error($e->getMessage());
return false;
}
}
public function download()
{
$this->info('Downloading update...');
try {
$path = Updater::download($this->version);
if (!is_string($path)) {
$this->error('Download exception');
return false;
}
} catch (\Exception $e) {
$this->error($e->getMessage());
return false;
}
return $path;
}
public function unzip($path)
{
$this->info('Unzipping update package...');
try {
$path = Updater::unzip($path);
if (!is_string($path)) {
$this->error('Unzipping exception');
return false;
}
} catch (\Exception $e) {
$this->error($e->getMessage());
return false;
}
return $path;
}
public function copyFiles($path)
{
$this->info('Copying update files...');
try {
Updater::copyFiles($path);
} catch (\Exception $e) {
$this->error($e->getMessage());
return false;
}
return true;
}
public function migrateUpdate()
{
$this->info('Running Migrations...');
try {
Updater::migrateUpdate();
} catch (\Exception $e) {
$this->error($e->getMessage());
return false;
}
return true;
}
public function finish()
{
$this->info('Finishing update...');
try {
Updater::finishUpdate($this->installed, $this->version);
} catch (\Exception $e) {
$this->error($e->getMessage());
return false;
}
return true;
}
}

View File

@ -12,7 +12,8 @@ class Kernel extends ConsoleKernel
* @var array
*/
protected $commands = [
Commands\ResetApp::class,
Commands\UpdateCommand::class
];
/**

View File

@ -49,15 +49,20 @@ class Estimate extends Model
];
protected $casts = [
'total' => 'float',
'tax' => 'float',
'sub_total' => 'float'
'total' => 'integer',
'tax' => 'integer',
'sub_total' => 'integer',
'discount' => 'float',
'discount_val' => 'integer',
];
public static function getNextEstimateNumber()
public static function getNextEstimateNumber($value)
{
// Get the last created order
$lastOrder = Estimate::orderBy('created_at', 'desc')->first();
// Get the last created order
$lastOrder = Estimate::where('estimate_number', 'LIKE', $value . '-%')
->orderBy('created_at', 'desc')
->first();
if (!$lastOrder) {
// We get here if there is no order at all
// If there is no number set it to 0, which will be 1 at the end.
@ -99,10 +104,16 @@ class Estimate extends Model
public function getEstimateNumAttribute()
{
$position = $this->strposX($this->estimate_number, "-", 2) + 1;
$position = $this->strposX($this->estimate_number, "-", 1) + 1;
return substr($this->estimate_number, $position);
}
public function getEstimatePrefixAttribute()
{
$prefix = explode("-",$this->estimate_number)[0];
return $prefix;
}
private function strposX($haystack, $needle, $number)
{
if ($number == '1') {

View File

@ -25,7 +25,7 @@ class UpdateFinished
*/
public function __construct($old, $new)
{
$this->old = $old;
$this->new = $new;
$this->old = $old;
$this->new = $new;
}
}

View File

@ -5,6 +5,7 @@ use Illuminate\Database\Eloquent\Model;
use Spatie\MediaLibrary\HasMedia\HasMedia;
use Spatie\MediaLibrary\HasMedia\HasMediaTrait;
use Crater\ExpenseCategory;
use Crater\User;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
@ -16,6 +17,7 @@ class Expense extends Model implements HasMedia
'expense_category_id',
'amount',
'company_id',
'user_id',
'expense_date',
'notes',
'attachment_receipt'
@ -32,6 +34,11 @@ class Expense extends Model implements HasMedia
return $this->belongsTo(ExpenseCategory::class, 'expense_category_id');
}
public function user()
{
return $this->belongsTo(User::class);
}
public function getFormattedExpenseDateAttribute($value)
{
$dateFormat = CompanySetting::getSetting('carbon_date_format', $this->company_id);
@ -81,6 +88,11 @@ class Expense extends Model implements HasMedia
return $query->where('expenses.expense_category_id', $categoryId);
}
public function scopeWhereUser($query, $user_id)
{
return $query->where('expenses.user_id', $user_id);
}
public function scopeApplyFilters($query, array $filters)
{
$filters = collect($filters);
@ -89,6 +101,10 @@ class Expense extends Model implements HasMedia
$query->whereCategory($filters->get('expense_category_id'));
}
if ($filters->get('user_id')) {
$query->whereUser($filters->get('user_id'));
}
if ($filters->get('from_date') && $filters->get('to_date')) {
$start = Carbon::createFromFormat('d/m/Y', $filters->get('from_date'));
$end = Carbon::createFromFormat('d/m/Y', $filters->get('to_date'));

View File

@ -20,11 +20,22 @@ use Crater\CompanySetting;
class CompanyController extends Controller
{
/**
* Retrive the Admin account.
* @return \Crater\User
*/
public function getAdmin()
{
return User::find(1);
}
/**
* Update the Admin profile.
* Includes name, email and (or) password
*
* @param \Crater\Http\Requests\ProfileRequest $request
* @return \Illuminate\Http\JsonResponse
*/
public function updateAdminProfile(ProfileRequest $request)
{
$verifyEmail = User::where('email', $request->email)->first();
@ -54,6 +65,14 @@ class CompanyController extends Controller
]);
}
/**
* Get Admin Account alongside the country from the addresses table and
* The company from companies table
*
* @return \Illuminate\Http\JsonResponse
*/
public function getAdminCompany()
{
$user = User::with(['addresses', 'addresses.country', 'company'])->find(1);
@ -63,6 +82,13 @@ class CompanyController extends Controller
]);
}
/**
* Update Admin Company Details
* @param \Crater\Http\Requests\CompanyRequest $request
* @return \Illuminate\Http\JsonResponse
*/
public function updateAdminCompany(CompanyRequest $request)
{
$user = User::find(1);
@ -85,6 +111,11 @@ class CompanyController extends Controller
]);
}
/**
* Retrieve General App Settings
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function getGeneralSettings(Request $request)
{
$date_formats = DateFormatter::get_list();
@ -112,10 +143,14 @@ class CompanyController extends Controller
$currency = CompanySetting::getSetting('currency', $request->header('company'));
$fiscal_year = CompanySetting::getSetting('fiscal_year', $request->header('company'));
$languages = [
$languages = [ // alphabetical order
["code"=>"pt_BR", "name" => "Brazilian Portuguese"],
["code"=>"en", "name" => "English"],
["code"=>"fr", "name" => "French"],
["code"=>"es", "name" => "Spanish"]
["code"=>"de", "name" => "German"],
["code"=>"it", "name" => "Italian"],
["code"=>"es", "name" => "Spanish"],
["code"=>"ar", "name" => "العربية"],
];
return response()->json([
@ -133,6 +168,13 @@ class CompanyController extends Controller
]);
}
/**
* Update General App Settings
* @param \Crater\Http\Requests\CompanySettingRequest $request
* @return \Illuminate\Http\JsonResponse
*/
public function updateGeneralSettings(CompanySettingRequest $request)
{
$sets = [
@ -153,6 +195,63 @@ class CompanyController extends Controller
]);
}
public function getCustomizeSetting (Request $request)
{
$invoice_prefix = CompanySetting::getSetting('invoice_prefix', $request->header('company'));
$invoice_auto_generate = CompanySetting::getSetting('invoice_auto_generate', $request->header('company'));
$estimate_prefix = CompanySetting::getSetting('estimate_prefix', $request->header('company'));
$estimate_auto_generate = CompanySetting::getSetting('estimate_auto_generate', $request->header('company'));
$payment_prefix = CompanySetting::getSetting('payment_prefix', $request->header('company'));
$payment_auto_generate = CompanySetting::getSetting('payment_auto_generate', $request->header('company'));
return response()->json([
'invoice_prefix' => $invoice_prefix,
'invoice_auto_generate' => $invoice_auto_generate,
'estimate_prefix' => $estimate_prefix,
'estimate_auto_generate' => $estimate_auto_generate,
'payment_prefix' => $payment_prefix,
'payment_auto_generate' => $payment_auto_generate,
]);
}
public function updateCustomizeSetting (Request $request)
{
$sets = [];
if ($request->type == "PAYMENTS") {
$sets = [
'payment_prefix'
];
}
if ($request->type == "INVOICES") {
$sets = [
'invoice_prefix',
];
}
if ($request->type == "ESTIMATES") {
$sets = [
'estimate_prefix',
];
}
foreach ($sets as $key) {
CompanySetting::setSetting($key, $request->$key, $request->header('company'));
}
return response()->json([
'success' => true
]);
}
/**
* Update a specific Company Setting
* @param \Crater\Http\Requests\SettingRequest $request
* @return \Illuminate\Http\JsonResponse
*/
public function updateSetting(SettingRequest $request)
{
CompanySetting::setSetting($request->key, $request->value, $request->header('company'));
@ -162,6 +261,11 @@ class CompanyController extends Controller
]);
}
/**
* Retrieve Specific Company Setting
* @param \Crater\Http\Requests\SettingKeyRequest $request
* @return \Illuminate\Http\JsonResponse
*/
public function getSetting(SettingKeyRequest $request)
{
$setting = CompanySetting::getSetting($request->key, $request->header('company'));
@ -171,6 +275,12 @@ class CompanyController extends Controller
]);
}
/**
* Retrieve App Colors
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function getColors(Request $request)
{
$colors = [
@ -205,7 +315,7 @@ class CompanyController extends Controller
* Upload the company logo to storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
* @return \Illuminate\Http\JsonResponse
*/
public function uploadCompanyLogo(Request $request)
{
@ -232,7 +342,7 @@ class CompanyController extends Controller
* Upload the Admin Avatar to public storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
* @return \Illuminate\Http\JsonResponse
*/
public function uploadAdminAvatar(Request $request)
{

View File

@ -19,7 +19,7 @@ class CustomersController extends Controller
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
* @return \Illuminate\Http\JsonResponse
*/
public function index(Request $request)
{
@ -53,7 +53,7 @@ class CustomersController extends Controller
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
* @return \Illuminate\Http\JsonResponse
*/
public function store(Requests\CustomerRequest $request)
{
@ -104,7 +104,7 @@ class CustomersController extends Controller
* Display the specified resource.
*
* @param int $id
* @return \Illuminate\Http\Response
* @return \Illuminate\Http\JsonResponse
*/
public function show($id)
{
@ -124,7 +124,7 @@ class CustomersController extends Controller
* Show the form for editing the specified resource.
*
* @param int $id
* @return \Illuminate\Http\Response
* @return \Illuminate\Http\JsonResponse
*/
public function edit($id)
{
@ -144,7 +144,7 @@ class CustomersController extends Controller
*
* @param \Illuminate\Http\Request $request
* @param int $id
* @return \Illuminate\Http\Response
* @return \Illuminate\Http\JsonResponse
*/
public function update($id, Requests\CustomerRequest $request)
{
@ -177,6 +177,7 @@ class CustomersController extends Controller
$customer->enable_portal = $request->enable_portal;
$customer->save();
$customer->addresses()->delete();
if ($request->addresses) {
foreach ($request->addresses as $address) {
$newAddress = $customer->addresses()->firstOrNew(['type' => $address["type"]]);
@ -203,10 +204,10 @@ class CustomersController extends Controller
}
/**
* Remove the specified resource from storage.
* Remove the specified Customer along side all his/her resources (ie. Estimates, Invoices, Payments and Addresses)
*
* @param int $id
* @return \Illuminate\Http\Response
* @return \Illuminate\Http\JsonResponse
*/
public function destroy($id)
{
@ -217,6 +218,13 @@ class CustomersController extends Controller
]);
}
/**
* Remove a list of Customers along side all their resources (ie. Estimates, Invoices, Payments and Addresses)
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function delete(Request $request)
{
foreach ($request->id as $id) {

View File

@ -15,6 +15,12 @@ use Illuminate\Support\Facades\DB;
class DashboardController extends Controller
{
/**
* Retrieve Dashboard details
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function index(Request $request)
{
$invoiceTotals = [];
@ -30,7 +36,8 @@ class DashboardController extends Controller
$start = Carbon::now();
$end = Carbon::now();
$terms = explode('-', $fiscalYear);
if ($terms[0] < $start->month) {
if ($terms[0] <= $start->month) {
$startDate->month($terms[0])->startOfMonth();
$start->month($terms[0])->startOfMonth();
$end->month($terms[0])->endOfMonth();
@ -87,6 +94,7 @@ class DashboardController extends Controller
}
$start->subMonth()->endOfMonth();
$salesTotal = Invoice::whereCompany($request->header('company'))
->whereBetween(
'invoice_date',
@ -137,6 +145,11 @@ class DashboardController extends Controller
]);
}
/**
* Retrive Expense Chart data
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function getExpenseChartData(Request $request)
{
$expensesCategories = Expense::with('category')

View File

@ -56,25 +56,43 @@ class EstimatesController extends Controller
public function create(Request $request)
{
$nextEstimateNumber = 'EST-'.Estimate::getNextEstimateNumber();
$estimate_prefix = CompanySetting::getSetting('estimate_prefix', $request->header('company'));
$estimate_num_auto_generate = CompanySetting::getSetting('estimate_auto_generate', $request->header('company'));
$nextEstimateNumberAttribute = null;
$nextEstimateNumber = Estimate::getNextEstimateNumber($estimate_prefix);
if ($estimate_num_auto_generate == "YES") {
$nextEstimateNumberAttribute = $nextEstimateNumber;
}
$tax_per_item = CompanySetting::getSetting('tax_per_item', $request->header('company'));
$discount_per_item = CompanySetting::getSetting('discount_per_item', $request->header('company'));
$customers = User::where('role', 'customer')->get();
return response()->json([
'customers' => $customers,
'nextEstimateNumber' => $nextEstimateNumber,
'nextEstimateNumberAttribute' => $nextEstimateNumberAttribute,
'nextEstimateNumber' => $estimate_prefix.'-'.$nextEstimateNumber,
'taxes' => Tax::whereCompany($request->header('company'))->latest()->get(),
'items' => Item::whereCompany($request->header('company'))->get(),
'tax_per_item' => $tax_per_item,
'discount_per_item' => $discount_per_item,
'estimateTemplates' => EstimateTemplate::all(),
'shareable_link' => ''
'shareable_link' => '',
'estimate_prefix' => $estimate_prefix
]);
}
public function store(EstimatesRequest $request)
{
$estimate_number = explode("-",$request->estimate_number);
$number_attributes['estimate_number'] = $estimate_number[0].'-'.sprintf('%06d', intval($estimate_number[1]));
Validator::make($number_attributes, [
'estimate_number' => 'required|unique:estimates,estimate_number'
])->validate();
$estimate_date = Carbon::createFromFormat('d/m/Y', $request->estimate_date);
$expiry_date = Carbon::createFromFormat('d/m/Y', $request->expiry_date);
$status = Estimate::STATUS_DRAFT;
@ -101,7 +119,7 @@ class EstimatesController extends Controller
$estimate = Estimate::create([
'estimate_date' => $estimate_date,
'expiry_date' => $expiry_date,
'estimate_number' => $request->estimate_number,
'estimate_number' => $number_attributes['estimate_number'],
'reference_number' => $request->reference_number,
'user_id' => $request->user_id,
'company_id' => $request->header('company'),
@ -127,7 +145,7 @@ class EstimatesController extends Controller
if (array_key_exists('taxes', $estimateItem) && $estimateItem['taxes']) {
foreach ($estimateItem['taxes'] as $tax) {
if ($tax['amount']) {
if (gettype($tax['amount']) !== "NULL") {
$tax['company_id'] = $request->header('company');
$item->taxes()->create($tax);
}
@ -137,7 +155,7 @@ class EstimatesController extends Controller
if ($request->has('taxes')) {
foreach ($request->taxes as $tax) {
if ($tax['amount']) {
if (gettype($tax['amount']) !== "NULL") {
$tax['company_id'] = $request->header('company');
$estimate->taxes()->create($tax);
}
@ -150,10 +168,6 @@ class EstimatesController extends Controller
$data['user'] = User::find($userId)->toArray();
$data['company'] = Company::find($estimate->company_id);
$email = $data['user']['email'];
$notificationEmail = CompanySetting::getSetting(
'notification_email',
$request->header('company')
);
if (!$email) {
return response()->json([
@ -161,13 +175,7 @@ class EstimatesController extends Controller
]);
}
if (!$notificationEmail) {
return response()->json([
'error' => 'notification_email_does_not_exist'
]);
}
\Mail::to($email)->send(new EstimatePdf($data, $notificationEmail));
\Mail::to($email)->send(new EstimatePdf($data));
}
$estimate = Estimate::with([
@ -216,26 +224,33 @@ class EstimatesController extends Controller
return response()->json( [
'customers' => $customers,
'nextEstimateNumber' => $estimate->estimate_number,
'nextEstimateNumber' => $estimate->getEstimateNumAttribute(),
'taxes' => Tax::latest()->whereCompany($request->header('company'))->get(),
'estimate' => $estimate,
'items' => Item::whereCompany($request->header('company'))->latest()->get(),
'estimateTemplates' => EstimateTemplate::all(),
'tax_per_item' => $estimate->tax_per_item,
'discount_per_item' => $estimate->discount_per_item,
'shareable_link' => url('/estimates/pdf/'.$estimate->unique_hash)
'shareable_link' => url('/estimates/pdf/'.$estimate->unique_hash),
'estimate_prefix' => $estimate->getEstimatePrefixAttribute()
]);
}
public function update(EstimatesRequest $request, $id)
{
$estimate_number = explode("-",$request->estimate_number);
$number_attributes['estimate_number'] = $estimate_number[0].'-'.sprintf('%06d', intval($estimate_number[1]));
Validator::make($number_attributes, [
'estimate_number' => 'required|unique:estimates,estimate_number'.','.$id
])->validate();
$estimate_date = Carbon::createFromFormat('d/m/Y', $request->estimate_date);
$expiry_date = Carbon::createFromFormat('d/m/Y', $request->expiry_date);
$estimate = Estimate::find($id);
$estimate->estimate_date = $estimate_date;
$estimate->expiry_date = $expiry_date;
$estimate->estimate_number = $request->estimate_number;
$estimate->estimate_number = $number_attributes['estimate_number'];
$estimate->reference_number = $request->reference_number;
$estimate->user_id = $request->user_id;
$estimate->estimate_template_id = $request->estimate_template_id;
@ -266,7 +281,7 @@ class EstimatesController extends Controller
if (array_key_exists('taxes', $estimateItem) && $estimateItem['taxes']) {
foreach ($estimateItem['taxes'] as $tax) {
if ($tax['amount']) {
if (gettype($tax['amount']) !== "NULL") {
$tax['company_id'] = $request->header('company');
$item->taxes()->create($tax);
}
@ -276,7 +291,7 @@ class EstimatesController extends Controller
if ($request->has('taxes')) {
foreach ($request->taxes as $tax) {
if ($tax['amount']) {
if (gettype($tax['amount']) !== "NULL") {
$tax['company_id'] = $request->header('company');
$estimate->taxes()->create($tax);
}
@ -315,10 +330,6 @@ class EstimatesController extends Controller
$data['company'] = Company::find($estimate->company_id);
$email = $data['user']['email'];
$notificationEmail = CompanySetting::getSetting(
'notification_email',
$request->header('company')
);
if (!$email) {
return response()->json([
@ -326,13 +337,7 @@ class EstimatesController extends Controller
]);
}
if (!$notificationEmail) {
return response()->json([
'error' => 'notification_email_does_not_exist'
]);
}
\Mail::to($email)->send(new EstimatePdf($data, $notificationEmail));
\Mail::to($email)->send(new EstimatePdf($data));
if ($estimate->status == Estimate::STATUS_DRAFT) {
$estimate->status = Estimate::STATUS_SENT;
@ -380,8 +385,12 @@ class EstimatesController extends Controller
public function estimateToInvoice(Request $request, $id)
{
$estimate = Estimate::with(['items', 'items.taxes', 'user', 'estimateTemplate', 'taxes'])->find($id);
$invoice_date = Carbon::parse($estimate->estimate_date);
$due_date = Carbon::parse($estimate->estimate_date)->addDays(7);
$invoice_date = Carbon::now();
$invoice_prefix = CompanySetting::getSetting(
'invoice_prefix',
$request->header('company')
);
$due_date = Carbon::now()->addDays(7);
$tax_per_item = CompanySetting::getSetting(
'tax_per_item',
$request->header('company')
@ -400,7 +409,7 @@ class EstimatesController extends Controller
$invoice = Invoice::create([
'invoice_date' => $invoice_date,
'due_date' => $due_date,
'invoice_number' => "INV-".Invoice::getNextInvoiceNumber(),
'invoice_number' => $invoice_prefix."-".Invoice::getNextInvoiceNumber($invoice_prefix),
'reference_number' => $estimate->reference_number,
'user_id' => $estimate->user_id,
'company_id' => $request->header('company'),

View File

@ -1,4 +1,5 @@
<?php
namespace Crater\Http\Controllers;
use Crater\Expense;
@ -17,16 +18,18 @@ class ExpensesController extends Controller
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
* @return \Illuminate\Http\JsonResponse
*/
public function index(Request $request)
{
$limit = $request->has('limit') ? $request->limit : 10;
$expenses = Expense::with('category')
->leftJoin('users', 'users.id', '=', 'expenses.user_id')
->join('expense_categories', 'expense_categories.id', '=', 'expenses.expense_category_id')
->applyFilters($request->only([
'expense_category_id',
'user_id',
'search',
'from_date',
'to_date',
@ -34,11 +37,16 @@ class ExpensesController extends Controller
'orderBy'
]))
->whereCompany($request->header('company'))
->select('expenses.*', 'expense_categories.name')
->select('expenses.*', 'expense_categories.name', 'users.name as user_name')
->paginate($limit);
$customers = User::customer()
->whereCompany($request->header('company'))
->get();
return response()->json([
'expenses' => $expenses,
'customers' => $customers,
'currency' => Currency::findOrFail(
CompanySetting::getSetting('currency', $request->header('company'))
)
@ -48,14 +56,18 @@ class ExpensesController extends Controller
/**
* Show the form for creating a new resource.
*
* @return \Illuminate\Http\Response
* @return \Illuminate\Http\JsonResponse
*/
public function create(Request $request)
{
$categories = ExpenseCategory::whereCompany($request->header('company'))->get();
$customers = User::customer()
->whereCompany($request->header('company'))
->get();
return response()->json([
'categories' => $categories
'categories' => $categories,
'customers' => $customers
]);
}
@ -63,7 +75,7 @@ class ExpensesController extends Controller
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
* @return \Illuminate\Http\JsonResponse
*/
public function store(ExpenseRequest $request)
{
@ -72,6 +84,7 @@ class ExpensesController extends Controller
$expense = new Expense();
$expense->notes = $request->notes;
$expense->expense_category_id = $request->expense_category_id;
$expense->user_id = $request->user_id;
$expense->amount = $request->amount;
$expense->company_id = $request->header('company');
$expense->expense_date = $expense_date;
@ -91,7 +104,7 @@ class ExpensesController extends Controller
* Display the specified resource.
*
* @param \Crater\Expense $expense
* @return \Illuminate\Http\Response
* @return \Illuminate\Http\JsonResponse
*/
public function show(Expense $expense)
{
@ -102,12 +115,14 @@ class ExpensesController extends Controller
* Show the form for editing the specified resource.
*
* @param $id
* @return \Illuminate\Http\Response
* @return \Illuminate\Http\JsonResponse
*/
public function edit(Request $request,$id)
public function edit(Request $request, $id)
{
$categories = ExpenseCategory::whereCompany($request->header('company'))->get();
$customers = User::where('role', 'customer')->whereCompany($request->header('company'))->get();
$customers = User::customer()
->whereCompany($request->header('company'))
->get();
$expense = Expense::with('category')->where('id', $id)->first();
return response()->json([
@ -122,7 +137,7 @@ class ExpensesController extends Controller
*
* @param \Illuminate\Http\Request $request
* @param \Crater\Expense $expense
* @return \Illuminate\Http\Response
* @return \Illuminate\Http\JsonResponse
*/
public function update(ExpenseRequest $request, Expense $expense)
{
@ -132,6 +147,7 @@ class ExpensesController extends Controller
$expense->notes = $request->notes;
$expense->expense_category_id = $request->expense_category_id;
$expense->amount = $request->amount;
$expense->user_id = $request->user_id;
$expense->expense_date = $expense_date;
$expense->save();
@ -150,7 +166,7 @@ class ExpensesController extends Controller
* Remove the specified resource from storage.
*
* @param \Crater\Expense $expense
* @return \Illuminate\Http\Response
* @return \Illuminate\Http\JsonResponse
*/
public function destroy(Expense $expense)
{
@ -175,17 +191,17 @@ class ExpensesController extends Controller
*
* @param \Illuminate\Http\Request $request
* @param $id
* @return \Illuminate\Http\Response
* @return \Illuminate\Http\JsonResponse
*/
public function uploadReceipts(Request $request, $id)
{
$data = json_decode($request->attachment_receipt);
if($data) {
if ($data) {
$expense = Expense::find($id);
if($expense) {
if($request->type === 'edit') {
if ($expense) {
if ($request->type === 'edit') {
$expense->clearMediaCollection('receipts');
}
@ -200,14 +216,20 @@ class ExpensesController extends Controller
]);
}
/**
* Retrive details of an expense receipt from storage.
* @param int $id
* @return \Illuminate\Http\JsonResponse
*/
public function showReceipt($id)
{
$expense = Expense::find($id);
$imagePath = null;
if($expense) {
if ($expense) {
$media = $expense->getFirstMedia('receipts');
if($media) {
if ($media) {
$imagePath = $media->getPath();
} else {
return response()->json([
@ -218,7 +240,7 @@ class ExpensesController extends Controller
$type = \File::mimeType($imagePath);
$image = 'data:'.$type.';base64,'.base64_encode(file_get_contents($imagePath));
$image = 'data:' . $type . ';base64,' . base64_encode(file_get_contents($imagePath));
return response()->json([
'image' => $image,
@ -226,6 +248,14 @@ class ExpensesController extends Controller
]);
}
/**
* Download an expense receipt from storage.
* @param int $id
* @param strig $hash
* @return \Symfony\Component\HttpFoundation\BinaryFileResponse | \Illuminate\Http\JsonResponse
*/
public function downloadReceipt($id, $hash)
{
$company = Company::where('unique_hash', $hash)->first();
@ -235,17 +265,10 @@ class ExpensesController extends Controller
->first();
$imagePath = null;
if($expense) {
if ($expense) {
$media = $expense->getFirstMedia('receipts');
if($media) {
if ($media) {
$imagePath = $media->getPath();
$filename = $media->getPath();
$type = \File::mimeType($imagePath);
$headers = array(
'Content-Type' => $type,
);
$response = \Response::download($imagePath, $media->file_name);
ob_end_clean();
return $response;
@ -257,4 +280,3 @@ class ExpensesController extends Controller
]);
}
}

View File

@ -6,6 +6,7 @@ use Crater\Invoice;
use PDF;
use Crater\CompanySetting;
use Crater\Estimate;
use Crater\Payment;
use Crater\User;
use Crater\Company;
use Crater\InvoiceTemplate;
@ -118,6 +119,11 @@ class FrontendController extends Controller
return $pdf->stream();
}
/**
*
* @return \Illuminate\Http\Response
*/
public function getCustomerInvoicePdf($id)
{
$invoice = Invoice::with([
@ -371,4 +377,34 @@ class FrontendController extends Controller
return $pdf->stream();
}
public function getPaymentPdf($id)
{
$payment = Payment::with([
'user',
'invoice',
'paymentMethod'
])
->where('unique_hash', $id)
->first();
$company = Company::find($payment->company_id);
$companyAddress = User::with(['addresses', 'addresses.country'])->find(1);
$logo = $company->getMedia('logo')->first();
if($logo) {
$logo = $logo->getFullUrl();
}
view()->share([
'payment' => $payment,
'company_address' => $companyAddress,
'logo' => $logo ?? null
]);
$pdf = PDF::loadView('app.pdf.payment.payment');
return $pdf->stream();
}
}

View File

@ -12,7 +12,7 @@ use Crater\Invoice;
use Crater\InvoiceItem;
use Carbon\Carbon;
use Crater\Item;
use Crater\Mail\invoicePdf;
use Crater\Mail\InvoicePdf;
use function MongoDB\BSON\toJSON;
use Illuminate\Support\Facades\Log;
use Crater\User;
@ -27,7 +27,7 @@ class InvoicesController extends Controller
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
* @return \Illuminate\Http\JsonResponse
*/
public function index(Request $request)
{
@ -60,20 +60,30 @@ class InvoicesController extends Controller
/**
* Show the form for creating a new resource.
*
* @return \Illuminate\Http\Response
* @return \Illuminate\Http\JsonResponse
*/
public function create(Request $request)
{
$tax_per_item = CompanySetting::getSetting('tax_per_item', $request->header('company'));
$discount_per_item = CompanySetting::getSetting('discount_per_item', $request->header('company'));
$nextInvoiceNumber = "INV-".Invoice::getNextInvoiceNumber();
$invoice_prefix = CompanySetting::getSetting('invoice_prefix', $request->header('company'));
$invoice_num_auto_generate = CompanySetting::getSetting('invoice_auto_generate', $request->header('company'));
$nextInvoiceNumberAttribute = null;
$nextInvoiceNumber = Invoice::getNextInvoiceNumber($invoice_prefix);
if ($invoice_num_auto_generate == "YES") {
$nextInvoiceNumberAttribute = $nextInvoiceNumber;
}
return response()->json([
'nextInvoiceNumber' => $nextInvoiceNumber,
'nextInvoiceNumberAttribute' => $nextInvoiceNumberAttribute,
'nextInvoiceNumber' => $invoice_prefix.'-'.$nextInvoiceNumber,
'items' => Item::with('taxes')->whereCompany($request->header('company'))->get(),
'invoiceTemplates' => InvoiceTemplate::all(),
'tax_per_item' => $tax_per_item,
'discount_per_item' => $discount_per_item
'discount_per_item' => $discount_per_item,
'invoice_prefix' => $invoice_prefix
]);
}
@ -81,10 +91,17 @@ class InvoicesController extends Controller
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
* @return \Illuminate\Http\JsonResponse
*/
public function store(Requests\InvoicesRequest $request)
{
$invoice_number = explode("-",$request->invoice_number);
$number_attributes['invoice_number'] = $invoice_number[0].'-'.sprintf('%06d', intval($invoice_number[1]));
Validator::make($number_attributes, [
'invoice_number' => 'required|unique:invoices,invoice_number'
])->validate();
$invoice_date = Carbon::createFromFormat('d/m/Y', $request->invoice_date);
$due_date = Carbon::createFromFormat('d/m/Y', $request->due_date);
$status = Invoice::STATUS_DRAFT;
@ -99,7 +116,7 @@ class InvoicesController extends Controller
$invoice = Invoice::create([
'invoice_date' => $invoice_date,
'due_date' => $due_date,
'invoice_number' => $request->invoice_number,
'invoice_number' => $number_attributes['invoice_number'],
'reference_number' => $request->reference_number,
'user_id' => $request->user_id,
'company_id' => $request->header('company'),
@ -128,8 +145,7 @@ class InvoicesController extends Controller
if (array_key_exists('taxes', $invoiceItem) && $invoiceItem['taxes']) {
foreach ($invoiceItem['taxes'] as $tax) {
$tax['company_id'] = $request->header('company');
if ($tax['amount']) {
if (gettype($tax['amount']) !== "NULL") {
$item->taxes()->create($tax);
}
}
@ -140,7 +156,7 @@ class InvoicesController extends Controller
foreach ($request->taxes as $tax) {
$tax['company_id'] = $request->header('company');
if ($tax['amount']) {
if (gettype($tax['amount']) !== "NULL") {
$invoice->taxes()->create($tax);
}
}
@ -151,11 +167,6 @@ class InvoicesController extends Controller
$data['user'] = User::find($request->user_id)->toArray();
$data['company'] = Company::find($invoice->company_id);
$notificationEmail = CompanySetting::getSetting(
'notification_email',
$request->header('company')
);
$email = $data['user']['email'];
if (!$email) {
@ -164,13 +175,7 @@ class InvoicesController extends Controller
]);
}
if (!$notificationEmail) {
return response()->json([
'error' => 'notification_email_does_not_exist'
]);
}
\Mail::to($email)->send(new invoicePdf($data, $notificationEmail));
\Mail::to($email)->send(new InvoicePdf($data));
}
$invoice = Invoice::with(['items', 'user', 'invoiceTemplate', 'taxes'])->find($invoice->id);
@ -185,7 +190,7 @@ class InvoicesController extends Controller
* Display the specified resource.
*
* @param int $id
* @return \Illuminate\Http\Response
* @return \Illuminate\Http\JsonResponse
*/
public function show(Request $request, $id)
{
@ -209,7 +214,7 @@ class InvoicesController extends Controller
* Show the form for editing the specified resource.
*
* @param int $id
* @return \Illuminate\Http\Response
* @return \Illuminate\Http\JsonResponse
*/
public function edit(Request $request,$id)
{
@ -222,12 +227,13 @@ class InvoicesController extends Controller
])->find($id);
return response()->json([
'nextInvoiceNumber' => $invoice->invoice_number,
'nextInvoiceNumber' => $invoice->getInvoiceNumAttribute(),
'invoice' => $invoice,
'invoiceTemplates' => InvoiceTemplate::all(),
'tax_per_item' => $invoice->tax_per_item,
'discount_per_item' => $invoice->discount_per_item,
'shareable_link' => url('/invoices/pdf/'.$invoice->unique_hash)
'shareable_link' => url('/invoices/pdf/'.$invoice->unique_hash),
'invoice_prefix' => $invoice->getInvoicePrefixAttribute()
]);
}
@ -236,10 +242,17 @@ class InvoicesController extends Controller
*
* @param \Illuminate\Http\Request $request
* @param int $id
* @return \Illuminate\Http\Response
* @return \Illuminate\Http\JsonResponse
*/
public function update(Requests\InvoicesRequest $request, $id)
{
$invoice_number = explode("-",$request->invoice_number);
$number_attributes['invoice_number'] = $invoice_number[0].'-'.sprintf('%06d', intval($invoice_number[1]));
Validator::make($number_attributes, [
'invoice_number' => 'required|unique:invoices,invoice_number'.','.$id
])->validate();
$invoice_date = Carbon::createFromFormat('d/m/Y', $request->invoice_date);
$due_date = Carbon::createFromFormat('d/m/Y', $request->due_date);
@ -268,7 +281,7 @@ class InvoicesController extends Controller
$invoice->invoice_date = $invoice_date;
$invoice->due_date = $due_date;
$invoice->invoice_number = $request->invoice_number;
$invoice->invoice_number = $number_attributes['invoice_number'];
$invoice->reference_number = $request->reference_number;
$invoice->user_id = $request->user_id;
$invoice->invoice_template_id = $request->invoice_template_id;
@ -292,7 +305,6 @@ class InvoicesController extends Controller
foreach ($oldTaxes as $oldTax) {
Tax::destroy($oldTax['id']);
}
foreach ($invoiceItems as $invoiceItem) {
$invoiceItem['company_id'] = $request->header('company');
$item = $invoice->items()->create($invoiceItem);
@ -300,8 +312,7 @@ class InvoicesController extends Controller
if (array_key_exists('taxes', $invoiceItem) && $invoiceItem['taxes']) {
foreach ($invoiceItem['taxes'] as $tax) {
$tax['company_id'] = $request->header('company');
if ($tax['amount']) {
if (gettype($tax['amount']) !== "NULL") {
$item->taxes()->create($tax);
}
}
@ -312,7 +323,7 @@ class InvoicesController extends Controller
foreach ($request->taxes as $tax) {
$tax['company_id'] = $request->header('company');
if ($tax['amount']) {
if (gettype($tax['amount']) !== "NULL") {
$invoice->taxes()->create($tax);
}
}
@ -331,7 +342,7 @@ class InvoicesController extends Controller
* Remove the specified resource from storage.
*
* @param int $id
* @return \Illuminate\Http\Response
* @return \Illuminate\Http\JsonResponse
*/
public function destroy($id)
{
@ -369,6 +380,14 @@ class InvoicesController extends Controller
]);
}
/**
* Mail a specific invoice to the correponding cusitomer's email address.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function sendInvoice(Request $request)
{
$invoice = Invoice::findOrFail($request->id);
@ -378,10 +397,6 @@ class InvoicesController extends Controller
$data['user'] = User::find($userId)->toArray();
$data['company'] = Company::find($invoice->company_id);
$email = $data['user']['email'];
$notificationEmail = CompanySetting::getSetting(
'notification_email',
$request->header('company')
);
if (!$email) {
return response()->json([
@ -389,13 +404,7 @@ class InvoicesController extends Controller
]);
}
if (!$notificationEmail) {
return response()->json([
'error' => 'notification_email_does_not_exist'
]);
}
\Mail::to($email)->send(new invoicePdf($data, $notificationEmail));
\Mail::to($email)->send(new InvoicePdf($data));
if ($invoice->status == Invoice::STATUS_DRAFT) {
$invoice->status = Invoice::STATUS_SENT;
@ -409,6 +418,13 @@ class InvoicesController extends Controller
]);
}
/**
* Mark a specific invoice as sent.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function markAsSent(Request $request)
{
$invoice = Invoice::findOrFail($request->id);
@ -421,6 +437,13 @@ class InvoicesController extends Controller
]);
}
/**
* Mark a specific invoice as paid.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function markAsPaid(Request $request)
{
$invoice = Invoice::findOrFail($request->id);
@ -434,6 +457,14 @@ class InvoicesController extends Controller
]);
}
/**
* Retrive a specified user's unpaid invoices from storage.
*
* @param \Illuminate\Http\Request $request
* @param int $id
* @return \Illuminate\Http\JsonResponse
*/
public function getCustomersUnpaidInvoices(Request $request, $id)
{
$invoices = Invoice::where('paid_status', '<>', Invoice::STATUS_PAID)
@ -445,4 +476,94 @@ class InvoicesController extends Controller
'invoices' => $invoices
]);
}
public function cloneInvoice(Request $request)
{
$oldInvoice = Invoice::with([
'items.taxes',
'user',
'invoiceTemplate',
'taxes.taxType'
])
->find($request->id);
$date = Carbon::now();
$invoice_prefix = CompanySetting::getSetting(
'invoice_prefix',
$request->header('company')
);
$tax_per_item = CompanySetting::getSetting(
'tax_per_item',
$request->header('company')
) ? CompanySetting::getSetting(
'tax_per_item',
$request->header('company')
) : 'NO';
$discount_per_item = CompanySetting::getSetting(
'discount_per_item',
$request->header('company')
) ? CompanySetting::getSetting(
'discount_per_item',
$request->header('company')
) : 'NO';
$invoice = Invoice::create([
'invoice_date' => $date,
'due_date' => $date,
'invoice_number' => $invoice_prefix."-".Invoice::getNextInvoiceNumber($invoice_prefix),
'reference_number' => $oldInvoice->reference_number,
'user_id' => $oldInvoice->user_id,
'company_id' => $request->header('company'),
'invoice_template_id' => 1,
'status' => Invoice::STATUS_DRAFT,
'paid_status' => Invoice::STATUS_UNPAID,
'sub_total' => $oldInvoice->sub_total,
'discount' => $oldInvoice->discount,
'discount_type' => $oldInvoice->discount_type,
'discount_val' => $oldInvoice->discount_val,
'total' => $oldInvoice->total,
'due_amount' => $oldInvoice->total,
'tax_per_item' => $oldInvoice->tax_per_item,
'discount_per_item' => $oldInvoice->discount_per_item,
'tax' => $oldInvoice->tax,
'notes' => $oldInvoice->notes,
'unique_hash' => str_random(60)
]);
$invoiceItems = $oldInvoice->items->toArray();
foreach ($invoiceItems as $invoiceItem) {
$invoiceItem['company_id'] = $request->header('company');
$invoiceItem['name'] = $invoiceItem['name'];
$item = $invoice->items()->create($invoiceItem);
if (array_key_exists('taxes', $invoiceItem) && $invoiceItem['taxes']) {
foreach ($invoiceItem['taxes'] as $tax) {
$tax['company_id'] = $request->header('company');
if ($tax['amount']) {
$item->taxes()->create($tax);
}
}
}
}
if ($oldInvoice->taxes) {
foreach ($oldInvoice->taxes->toArray() as $tax) {
$tax['company_id'] = $request->header('company');
$invoice->taxes()->create($tax);
}
}
$invoice = Invoice::with([
'items',
'user',
'invoiceTemplate',
'taxes'
])->find($invoice->id);
return response()->json([
'invoice' => $invoice
]);
}
}

View File

@ -14,14 +14,17 @@ class ItemsController extends Controller
{
$limit = $request->has('limit') ? $request->limit : 10;
$items = Item::applyFilters($request->only([
$items = Item::with(['taxes'])
->leftJoin('units', 'units.id', '=', 'items.unit_id')
->applyFilters($request->only([
'search',
'price',
'unit',
'unit_id',
'orderByField',
'orderBy'
]))
->whereCompany($request->header('company'))
->select('items.*', 'units.name as unit_name')
->latest()
->paginate($limit);
@ -33,7 +36,7 @@ class ItemsController extends Controller
public function edit(Request $request, $id)
{
$item = Item::with('taxes')->find($id);
$item = Item::with(['taxes', 'unit'])->find($id);
return response()->json([
'item' => $item,
@ -43,11 +46,18 @@ class ItemsController extends Controller
]);
}
/**
* Create Item.
*
* @param Crater\Http\Requests\ItemsRequest $request
* @return \Illuminate\Http\JsonResponse
*/
public function store(Requests\ItemsRequest $request)
{
$item = new Item();
$item->name = $request->name;
$item->unit = $request->unit;
$item->unit_id = $request->unit_id;
$item->description = $request->description;
$item->company_id = $request->header('company');
$item->price = $request->price;
@ -67,11 +77,18 @@ class ItemsController extends Controller
]);
}
/**
* Update an existing Item.
*
* @param Crater\Http\Requests\ItemsRequest $request
* @param int $id
* @return \Illuminate\Http\JsonResponse
*/
public function update(Requests\ItemsRequest $request, $id)
{
$item = Item::find($id);
$item->name = $request->name;
$item->unit = $request->unit;
$item->unit_id = $request->unit_id;
$item->description = $request->description;
$item->price = $request->price;
$item->save();
@ -96,6 +113,13 @@ class ItemsController extends Controller
]);
}
/**
* Delete an existing Item.
*
* @param int $id
* @return \Illuminate\Http\JsonResponse
*/
public function destroy($id)
{
$data = Item::deleteItem($id);
@ -111,12 +135,20 @@ class ItemsController extends Controller
]);
}
/**
* Delete a list of existing Items.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function delete(Request $request)
{
$items = [];
foreach ($request->id as $id) {
$item = Item::deleteItem($id);
if (!$item) {
if ($item) {
array_push($items, $id);
}
}

View File

@ -6,6 +6,12 @@ use Crater\Country;
class LocationController extends Controller
{
/**
* Retrive a list of Countries.
*
* @return \Illuminate\Http\JsonResponse
*/
public function getCountries()
{
return response()->json([

View File

@ -18,6 +18,13 @@ use Illuminate\Support\Facades\Artisan;
class OnboardingController extends Controller
{
/**
* Retrieve Onboarding data.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function getOnboardingData(Request $request)
{
if (!\Storage::disk('local')->has('database_created')) {
@ -37,9 +44,14 @@ class OnboardingController extends Controller
$date_formats = DateFormatter::get_list();
$time_zones = TimeZones::get_list();
$languages = [
["code"=>"ar", "name" => "Arabic"],
["code"=>"en", "name" => "English"],
["code"=>"fr", "name" => "French"],
["code"=>"es", "name" => "Spanish"]
["code"=>"es", "name" => "Spanish"],
["code"=>"ar", "name" => "العربية"],
["code"=>"de", "name" => "German"],
["code"=>"pt-br", "name" => "Portuguese (Brazilian)"],
["code"=>"it", "name" => "Italian"],
];
$fiscal_years = [
['key' => 'january-december' , 'value' => '1-12'],
@ -72,6 +84,13 @@ class OnboardingController extends Controller
]);
}
/**
* Setup Admin Profile.
*
* @param \Crater\Http\Requests\ProfileRequest $request
* @return \Illuminate\Http\JsonResponse
*/
public function adminProfile(ProfileRequest $request)
{
$setting = Setting::getSetting('profile_complete');
@ -97,6 +116,12 @@ class OnboardingController extends Controller
]);
}
/**
* Setup Admin Avatar.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function uploadAdminAvatar(Request $request)
{
$setting = Setting::getSetting('profile_complete');
@ -123,6 +148,12 @@ class OnboardingController extends Controller
]);
}
/**
* Setup Admin Company.
*
* @param \Crater\Http\Requests\CompanyRequest $request
* @return \Illuminate\Http\JsonResponse
*/
public function adminCompany(CompanyRequest $request)
{
$setting = Setting::getSetting('profile_complete');
@ -174,6 +205,13 @@ class OnboardingController extends Controller
]);
}
/**
* Setup Company Settings.
*
* @param \Crater\Http\Requests\CompanySettingRequest $request
* @return \Illuminate\Http\JsonResponse
*/
public function companySettings(CompanySettingRequest $request)
{
$setting = Setting::getSetting('profile_complete');
@ -202,6 +240,45 @@ class OnboardingController extends Controller
);
}
$invoices = [
'invoice_auto_generate' => 'YES',
'invoice_prefix' => 'INV'
];
foreach ($invoices as $key => $value) {
CompanySetting::setSetting(
$key,
$value,
$user->company_id
);
}
$estimates = [
'estimate_prefix' => 'EST',
'estimate_auto_generate' => 'YES'
];
foreach ($estimates as $key => $value) {
CompanySetting::setSetting(
$key,
$value,
$user->company_id
);
}
$payments = [
'payment_prefix' => 'PAY',
'payment_auto_generate' => 'YES'
];
foreach ($payments as $key => $value) {
CompanySetting::setSetting(
$key,
$value,
$user->company_id
);
}
$colors = [
'primary_text_color' => '#5851D8',
'heading_text_color' => '#595959',
@ -232,13 +309,24 @@ class OnboardingController extends Controller
Artisan::call('passport:install --force');
Artisan::call('db:seed', ['--class' => 'PaymentMethodSeeder', '--force' => true]);
Artisan::call('db:seed', ['--class' => 'UnitSeeder', '--force' => true]);
$client = DB::table('oauth_clients')->find(2);
$path = base_path('.env');
if (file_exists($path)) {
file_put_contents($path, str_replace(
'PROXY_OAUTH_CLIENT_SECRET='.config('auth.proxy.client_secret'), 'PROXY_OAUTH_CLIENT_SECRET='.$client->secret, file_get_contents($path)
'PROXY_OAUTH_CLIENT_SECRET='.config('auth.proxy.client_secret'),
'PROXY_OAUTH_CLIENT_SECRET='.$client->secret,
file_get_contents($path)
));
file_put_contents($path, str_replace(
'APP_DEBUG=true',
'APP_DEBUG=false',
file_get_contents($path)
));
}

View File

@ -4,12 +4,16 @@ namespace Crater\Http\Controllers;
use Illuminate\Http\Request;
use Crater\CompanySetting;
use Crater\Currency;
use Crater\Company;
use Crater\Invoice;
use Crater\Payment;
use Crater\PaymentMethod;
use Carbon\Carbon;
use function MongoDB\BSON\toJSON;
use Crater\User;
use Crater\Http\Requests\PaymentRequest;
use Validator;
use Crater\Mail\PaymentPdf;
class PaymentController extends Controller
{
@ -22,19 +26,20 @@ class PaymentController extends Controller
{
$limit = $request->has('limit') ? $request->limit : 10;
$payments = Payment::with('user', 'invoice')
$payments = Payment::with(['user', 'invoice', 'paymentMethod'])
->join('users', 'users.id', '=', 'payments.user_id')
->leftJoin('invoices', 'invoices.id', '=', 'payments.invoice_id')
->leftJoin('payment_methods', 'payment_methods.id', '=', 'payments.payment_method_id')
->applyFilters($request->only([
'search',
'payment_number',
'payment_mode',
'payment_method_id',
'customer_id',
'orderByField',
'orderBy'
]))
->whereCompany($request->header('company'))
->select('payments.*', 'users.name', 'invoices.invoice_number')
->select('payments.*', 'users.name', 'invoices.invoice_number', 'payment_methods.name as payment_mode')
->latest()
->paginate($limit);
@ -50,13 +55,27 @@ class PaymentController extends Controller
*/
public function create(Request $request)
{
$nextPaymentNumber = 'PAY-'.Payment::getNextPaymentNumber();
$payment_prefix = CompanySetting::getSetting('payment_prefix', $request->header('company'));
$payment_num_auto_generate = CompanySetting::getSetting('payment_auto_generate', $request->header('company'));
$nextPaymentNumberAttribute = null;
$nextPaymentNumber = Payment::getNextPaymentNumber($payment_prefix);
if ($payment_num_auto_generate == "YES") {
$nextPaymentNumberAttribute = $nextPaymentNumber;
}
return response()->json([
'customers' => User::where('role', 'customer')
->whereCompany($request->header('company'))
->get(),
'nextPaymentNumber' => $nextPaymentNumber
'paymentMethods' => PaymentMethod::whereCompany($request->header('company'))
->latest()
->get(),
'nextPaymentNumberAttribute' => $nextPaymentNumberAttribute,
'nextPaymentNumber' => $payment_prefix.'-'.$nextPaymentNumber,
'payment_prefix' => $payment_prefix
]);
}
@ -68,6 +87,13 @@ class PaymentController extends Controller
*/
public function store(PaymentRequest $request)
{
$payment_number = explode("-",$request->payment_number);
$number_attributes['payment_number'] = $payment_number[0].'-'.sprintf('%06d', intval($payment_number[1]));
Validator::make($number_attributes, [
'payment_number' => 'required|unique:payments,payment_number'
])->validate();
$payment_date = Carbon::createFromFormat('d/m/Y', $request->payment_date);
if ($request->has('invoice_id') && $request->invoice_id != null) {
@ -90,17 +116,19 @@ class PaymentController extends Controller
$payment = Payment::create([
'payment_date' => $payment_date,
'payment_number' => $request->payment_number,
'payment_number' => $number_attributes['payment_number'],
'user_id' => $request->user_id,
'company_id' => $request->header('company'),
'invoice_id' => $request->invoice_id,
'payment_mode' => $request->payment_mode,
'payment_method_id' => $request->payment_method_id,
'amount' => $request->amount,
'notes' => $request->notes,
'unique_hash' => str_random(60)
]);
return response()->json([
'payment' => $payment,
'shareable_link' => url('/payments/pdf/'.$payment->unique_hash),
'success' => true
]);
}
@ -113,7 +141,12 @@ class PaymentController extends Controller
*/
public function show($id)
{
//
$payment = Payment::with(['user', 'invoice', 'paymentMethod'])->find($id);
return response()->json([
'payment' => $payment,
'shareable_link' => url('/payments/pdf/'.$payment->unique_hash)
]);
}
/**
@ -124,7 +157,7 @@ class PaymentController extends Controller
*/
public function edit(Request $request, $id)
{
$payment = Payment::with('user', 'invoice')->find($id);
$payment = Payment::with(['user', 'invoice', 'paymentMethod'])->find($id);
$invoices = Invoice::where('paid_status', '<>', Invoice::STATUS_PAID)
->where('user_id', $payment->user_id)->where('due_amount', '>', 0)
@ -135,7 +168,12 @@ class PaymentController extends Controller
'customers' => User::where('role', 'customer')
->whereCompany($request->header('company'))
->get(),
'nextPaymentNumber' => $payment->payment_number,
'paymentMethods' => PaymentMethod::whereCompany($request->header('company'))
->latest()
->get(),
'nextPaymentNumber' => $payment->getPaymentNumAttribute(),
'payment_prefix' => $payment->getPaymentPrefixAttribute(),
'shareable_link' => url('/payments/pdf/'.$payment->unique_hash),
'payment' => $payment,
'invoices' => $invoices
]);
@ -150,6 +188,13 @@ class PaymentController extends Controller
*/
public function update(PaymentRequest $request, $id)
{
$payment_number = explode("-",$request->payment_number);
$number_attributes['payment_number'] = $payment_number[0].'-'.sprintf('%06d', intval($payment_number[1]));
Validator::make($number_attributes, [
'payment_number' => 'required|unique:payments,payment_number'.','.$id
])->validate();
$payment_date = Carbon::createFromFormat('d/m/Y', $request->payment_date);
$payment = Payment::find($id);
@ -178,16 +223,17 @@ class PaymentController extends Controller
}
$payment->payment_date = $payment_date;
$payment->payment_number = $request->payment_number;
$payment->payment_number = $number_attributes['payment_number'];
$payment->user_id = $request->user_id;
$payment->invoice_id = $request->invoice_id;
$payment->payment_mode = $request->payment_mode;
$payment->payment_method_id = $request->payment_method_id;
$payment->amount = $request->amount;
$payment->notes = $request->notes;
$payment->save();
return response()->json([
'payment' => $payment,
'shareable_link' => url('/payments/pdf/'.$payment->unique_hash),
'success' => true
]);
}
@ -249,4 +295,27 @@ class PaymentController extends Controller
'success' => true
]);
}
public function sendPayment(Request $request)
{
$payment = Payment::findOrFail($request->id);
$data['payment'] = $payment->toArray();
$userId = $data['payment']['user_id'];
$data['user'] = User::find($userId)->toArray();
$data['company'] = Company::find($payment->company_id);
$email = $data['user']['email'];
if (!$email) {
return response()->json([
'error' => 'user_email_does_not_exist'
]);
}
\Mail::to($email)->send(new PaymentPdf($data));
return response()->json([
'success' => true
]);
}
}

View File

@ -0,0 +1,119 @@
<?php
namespace Crater\Http\Controllers;
use Crater\PaymentMethod;
use Illuminate\Http\Request;
use Crater\Http\Requests\PaymentMethodRequest;
class PaymentMethodController extends Controller
{
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index(Request $request)
{
$paymentMethods = PaymentMethod::whereCompany($request->header('company'))
->latest()
->get();
return response()->json([
'paymentMethods' => $paymentMethods
]);
}
/**
* Show the form for creating a new resource.
*
* @return \Illuminate\Http\Response
*/
public function create()
{
//
}
/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(PaymentMethodRequest $request)
{
$paymentMethod = new PaymentMethod;
$paymentMethod->name = $request->name;
$paymentMethod->company_id = $request->header('company');
$paymentMethod->save();
return response()->json([
'paymentMethod' => $paymentMethod
]);
}
/**
* Display the specified resource.
*
* @param \Crater\PaymentMethod $paymentMethod
* @return \Illuminate\Http\Response
*/
public function show(PaymentMethod $paymentMethod)
{
//
}
/**
* Show the form for editing the specified resource.
*
* @param \Crater\PaymentMethod $paymentMethod
* @return \Illuminate\Http\Response
*/
public function edit(PaymentMethod $paymentMethod)
{
return response()->json([
'paymentMethod' => $paymentMethod
]);
}
/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param \Crater\PaymentMethod $paymentMethod
* @return \Illuminate\Http\Response
*/
public function update(PaymentMethodRequest $request, PaymentMethod $paymentMethod)
{
$paymentMethod->name = $request->name;
$paymentMethod->company_id = $request->header('company');
$paymentMethod->save();
return response()->json([
'paymentMethod' => $paymentMethod
]);
}
/**
* Remove the specified resource from storage.
*
* @param \Crater\PaymentMethod $paymentMethod
* @return \Illuminate\Http\Response
*/
public function destroy(PaymentMethod $paymentMethod)
{
$payments = $paymentMethod->payments;
if ($payments->count() > 0) {
return response()->json([
'error' => 'payments_attached'
]);
}
$paymentMethod->delete();
return response()->json([
'success' => 'Payment method deleted successfully'
]);
}
}

View File

@ -4,9 +4,18 @@ namespace Crater\Http\Controllers;
use Illuminate\Http\Request;
use Crater\Setting;
use Crater\Mail\TestMail;
use Mail;
class SettingsController extends Controller
{
/**
* Retrive App Version.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function getAppVersion(Request $request)
{
$version = Setting::getSetting('version');
@ -16,4 +25,18 @@ class SettingsController extends Controller
]);
}
public function testEmailConfig(Request $request)
{
$this->validate($request, [
'to' => 'required|email',
'subject' => 'required',
'message' => 'required'
]);
Mail::to($request->to)->send(new TestMail($request->subject, $request->message));
return response()->json([
'success' => true
]);
}
}

View File

@ -0,0 +1,119 @@
<?php
namespace Crater\Http\Controllers;
use Crater\Unit;
use Illuminate\Http\Request;
use Crater\Http\Requests\UnitRequest;
class UnitController extends Controller
{
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index(Request $request)
{
$units = Unit::whereCompany($request->header('company'))
->latest()
->get();
return response()->json([
'units' => $units
]);
}
/**
* Show the form for creating a new resource.
*
* @return \Illuminate\Http\Response
*/
public function create()
{
//
}
/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(UnitRequest $request)
{
$unit = new Unit;
$unit->name = $request->name;
$unit->company_id = $request->header('company');
$unit->save();
return response()->json([
'unit' => $unit
]);
}
/**
* Display the specified resource.
*
* @param \Crater\Unit $unit
* @return \Illuminate\Http\Response
*/
public function show(Unit $unit)
{
//
}
/**
* Show the form for editing the specified resource.
*
* @param \Crater\Unit $unit
* @return \Illuminate\Http\Response
*/
public function edit(Unit $unit)
{
return response()->json([
'unit' => $unit
]);
}
/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param \Crater\Unit $unit
* @return \Illuminate\Http\Response
*/
public function update(UnitRequest $request, Unit $unit)
{
$unit->name = $request->name;
$unit->company_id = $request->header('company');
$unit->save();
return response()->json([
'unit' => $unit
]);
}
/**
* Remove the specified resource from storage.
*
* @param \Crater\Unit $unit
* @return \Illuminate\Http\Response
*/
public function destroy(Unit $unit)
{
$items = $unit->items;
if ($items->count() > 0) {
return response()->json([
'error' => 'items_attached'
]);
}
$unit->delete();
return response()->json([
'success' => 'Unit deleted successfully'
]);
}
}

View File

@ -2,23 +2,81 @@
namespace Crater\Http\Controllers;
use Crater\Setting;
use Illuminate\Http\Request;
use Crater\Space\Updater;
use Crater\Space\SiteApi;
use Illuminate\Support\Facades\Artisan;
class UpdateController extends Controller
{
public function update(Request $request)
public function download(Request $request)
{
set_time_limit(600); // 10 minutes
$request->validate([
'version' => 'required',
]);
$json = Updater::update($request->installed, $request->version);
$path = Updater::download($request->version);
return response()->json($json);
return response()->json([
'success' => true,
'path' => $path
]);
}
public function unzip(Request $request)
{
$request->validate([
'path' => 'required',
]);
try {
$path = Updater::unzip($request->path);
return response()->json([
'success' => true,
'path' => $path
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'error' => $e->getMessage()
], 500);
}
}
public function copyFiles(Request $request)
{
$request->validate([
'path' => 'required',
]);
$path = Updater::copyFiles($request->path);
return response()->json([
'success' => true,
'path' => $path
]);
}
public function migrate(Request $request)
{
Updater::migrateUpdate();
return response()->json([
'success' => true
]);
}
public function finishUpdate(Request $request)
{
$request->validate([
'installed' => 'required',
'version' => 'required',
]);
$json = Updater::finishUpdate($request->installed, $request->version);
return response()->json($json);
@ -28,7 +86,7 @@ class UpdateController extends Controller
{
set_time_limit(600); // 10 minutes
$json = Updater::checkForUpdate();
$json = Updater::checkForUpdate(Setting::getSetting('version'));
return response()->json($json);
}

View File

@ -7,6 +7,8 @@ use Crater\User;
use Crater\Currency;
use Crater\Setting;
use Crater\Item;
use Crater\PaymentMethod;
use Crater\Unit;
use Crater\TaxType;
use DB;
use Carbon\Carbon;
@ -46,10 +48,18 @@ class UsersController extends Controller
$request->header('company')
);
$items = Item::all();
$items = Item::with('taxes')->get();
$taxTypes = TaxType::latest()->get();
$paymentMethods = PaymentMethod::whereCompany($request->header('company'))
->latest()
->get();
$units = Unit::whereCompany($request->header('company'))
->latest()
->get();
return response()->json([
'user' => $user,
'customers' => $customers,
@ -61,6 +71,8 @@ class UsersController extends Controller
'items' => $items,
'taxTypes' => $taxTypes,
'moment_date_format' => $moment_date_format,
'paymentMethods' => $paymentMethods,
'units' => $units,
'fiscal_year' => $fiscal_year,
]);
}

View File

@ -25,7 +25,6 @@ class EstimatesRequest extends FormRequest
$rules = [
'estimate_date' => 'required',
'expiry_date' => 'required',
'estimate_number' => 'required|unique:estimates,estimate_number',
'user_id' => 'required',
'discount' => 'required',
'discount_val' => 'required',
@ -41,10 +40,6 @@ class EstimatesRequest extends FormRequest
'items.*.price' => 'required'
];
if ($this->getMethod() == 'PUT') {
$rules['estimate_number'] = $rules['estimate_number'].','.$this->get('id');
}
return $rules;
}
}

View File

@ -25,7 +25,6 @@ class InvoicesRequest extends FormRequest
$rules = [
'invoice_date' => 'required',
'due_date' => 'required',
'invoice_number' => 'required|unique:invoices,invoice_number',
'user_id' => 'required',
'discount' => 'required',
'discount_val' => 'required',
@ -41,10 +40,6 @@ class InvoicesRequest extends FormRequest
'items.*.price' => 'required'
];
if ($this->getMethod() == 'PUT') {
$rules['invoice_number'] = $rules['invoice_number'].','.$this->get('id');
}
return $rules;
}
}

View File

@ -40,12 +40,9 @@ class MailEnvironmentRequest extends FormRequest
case 'mailgun':
return [
'mail_driver' => 'required|string',
'mail_host' => 'required|string',
'mail_port' => 'required',
'mail_mailgun_domain' => 'required|string',
'mail_mailgun_secret' => 'required|string',
'mail_mailgun_endpoint' => 'required|string',
'mail_encryption' => 'required|string',
'from_name' => 'required|string',
'from_mail' => 'required|string',
];

View File

@ -0,0 +1,40 @@
<?php
namespace Crater\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class PaymentMethodRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
$data = [
'name' => 'required|unique:payment_methods,name'
];
if ($this->getMethod() == 'PUT') {
$data['name'] = [
'required',
Rule::unique('payment_methods')->ignore($this->route('payment_method'), 'id')
];
}
return $data;
}
}

View File

@ -24,15 +24,10 @@ class PaymentRequest extends FormRequest
{
$rules = [
'payment_date' => 'required',
'payment_number' => 'required|unique:payments,payment_number',
'user_id' => 'required',
'amount' => 'required',
];
if ($this->getMethod() == 'PUT') {
$rules['payment_number'] = $rules['payment_number'].','.$this->route('payment');
}
return $rules;
}
}

View File

@ -30,7 +30,7 @@ class ProfileRequest extends FormRequest
case 'POST':
return [
'name' => 'required',
'password' => 'required',
'password' => 'required|min:8',
'address_street_1' => 'max:255',
'address_street_2' => 'max:255',
'email' => [

View File

@ -0,0 +1,40 @@
<?php
namespace Crater\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class UnitRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
$data = [
'name' => 'required|unique:units,name'
];
if ($this->getMethod() == 'PUT') {
$data['name'] = [
'required',
Rule::unique('units')->ignore($this->route('unit'), 'id')
];
}
return $data;
}
}

View File

@ -66,10 +66,14 @@ class Invoice extends Model
'formattedDueDate'
];
public static function getNextInvoiceNumber()
public static function getNextInvoiceNumber($value)
{
// Get the last created order
$lastOrder = Invoice::orderBy('created_at', 'desc')->first();
$lastOrder = Invoice::where('invoice_number', 'LIKE', $value . '-%')
->orderBy('created_at', 'desc')
->first();
if (!$lastOrder) {
// We get here if there is no order at all
// If there is no number set it to 0, which will be 1 at the end.
@ -143,10 +147,15 @@ class Invoice extends Model
public function getInvoiceNumAttribute()
{
$position = $this->strposX($this->invoice_number, "-", 2) + 1;
$position = $this->strposX($this->invoice_number, "-", 1) + 1;
return substr($this->invoice_number, $position);
}
public function getInvoicePrefixAttribute () {
$prefix = explode("-", $this->invoice_number)[0];
return $prefix;
}
public function getFormattedCreatedAtAttribute($value)
{
$dateFormat = CompanySetting::getSetting('carbon_date_format', $this->company_id);

View File

@ -24,19 +24,24 @@ class Item extends Model
'formattedCreatedAt'
];
public function unit()
{
return $this->belongsTo(Unit::class);
}
public function scopeWhereSearch($query, $search)
{
return $query->where('name', 'LIKE', '%'.$search.'%');
return $query->where('items.name', 'LIKE', '%'.$search.'%');
}
public function scopeWherePrice($query, $price)
{
return $query->where('price', $price);
return $query->where('items.price', $price);
}
public function scopeWhereUnit($query, $unit)
public function scopeWhereUnit($query, $unit_id)
{
return $query->where('unit', $unit);
return $query->where('items.unit_id', $unit_id);
}
public function scopeWhereOrder($query, $orderByField, $orderBy)
@ -56,8 +61,8 @@ class Item extends Model
$query->wherePrice($filters->get('price'));
}
if ($filters->get('unit')) {
$query->whereUnit($filters->get('unit'));
if ($filters->get('unit_id')) {
$query->whereUnit($filters->get('unit_id'));
}
if ($filters->get('orderByField') || $filters->get('orderBy')) {
@ -80,7 +85,7 @@ class Item extends Model
public function scopeWhereCompany($query, $company_id)
{
$query->where('company_id', $company_id);
$query->where('items.company_id', $company_id);
}
public function invoiceItems()

View File

@ -6,7 +6,6 @@ use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Database\Schema\Blueprint;
use Crater\Listeners\Updates\Listener;
use Crater\Listeners\Updates\v2\Version200;
use Crater\Events\UpdateFinished;
use Crater\Setting;
use Crater\Address;

View File

@ -0,0 +1,64 @@
<?php
namespace Crater\Listeners\Updates\v2;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Crater\Events\UpdateFinished;
use Crater\Listeners\Updates\Listener;
use Crater\Setting;
use Crater\CompanySetting;
class Version210 extends Listener
{
const VERSION = '2.1.0';
/**
* Create the event listener.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Handle the event.
*
* @param object $event
* @return void
*/
public function handle(UpdateFinished $event)
{
if ($this->isListenerFired($event)) {
return;
}
// Add initial auto generate value
$this->addAutoGenerateSettings();
// Update Crater app version
Setting::setSetting('version', static::VERSION);
}
private function addAutoGenerateSettings()
{
$settings = [
'invoice_auto_generate' => 'YES',
'invoice_prefix' => 'INV',
'estimate_prefix' => 'EST',
'estimate_auto_generate' => 'YES',
'payment_prefix' => 'PAY',
'payment_auto_generate' => 'YES'
];
foreach ($settings as $key => $value) {
CompanySetting::setSetting(
$key,
$value,
auth()->user()->company->id
);
}
}
}

View File

@ -0,0 +1,162 @@
<?php
namespace Crater\Listeners\Updates\v3;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Crater\Setting;
use Crater\Unit;
use Crater\PaymentMethod;
use Crater\Currency;
use Crater\Payment;
use Crater\Item;
use Crater\User;
use Crater\Listeners\Updates\Listener;
use Illuminate\Database\Schema\Blueprint;
class Version300 extends Listener
{
const VERSION = '3.0.0';
/**
* Create the event listener.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Handle the event.
*
* @param object $event
* @return void
*/
public function handle($event)
{
if ($this->isListenerFired($event)) {
return;
}
$this->changeMigrations();
$this->addSeederData();
$this->databaseChanges();
$this->changeMigrations(true);
Setting::setSetting('version', static::VERSION);
}
public function changeMigrations($removeColumn = false)
{
if ($removeColumn) {
\Schema::table('items', function (Blueprint $table) {
$table->dropColumn('unit');
});
\Schema::table('payments', function (Blueprint $table) {
$table->dropColumn('payment_mode');
});
return true;
}
\Schema::create('units', function (Blueprint $table) {
$table->increments('id');
$table->string('name');
$table->integer('company_id')->unsigned()->nullable();
$table->foreign('company_id')->references('id')->on('companies');
$table->timestamps();
});
\Schema::table('items', function (Blueprint $table) {
$table->integer('unit_id')->unsigned()->nullable();
$table->foreign('unit_id')->references('id')->on('units')->onDelete('cascade');
});
\Schema::create('payment_methods', function (Blueprint $table) {
$table->increments('id');
$table->string('name');
$table->integer('company_id')->unsigned()->nullable();
$table->foreign('company_id')->references('id')->on('companies');
$table->timestamps();
});
\Schema::table('payments', function (Blueprint $table) {
$table->string('unique_hash')->nullable();
$table->integer('payment_method_id')->unsigned()->nullable();
$table->foreign('payment_method_id')->references('id')->on('payment_methods')->onDelete('cascade');
});
return true;
}
public function addSeederData()
{
$company_id = User::where('role', 'admin')->first()->company_id;
Unit::create(['name' => 'box', 'company_id' => $company_id]);
Unit::create(['name' => 'cm', 'company_id' => $company_id]);
Unit::create(['name' => 'dz', 'company_id' => $company_id]);
Unit::create(['name' => 'ft', 'company_id' => $company_id]);
Unit::create(['name' => 'g', 'company_id' => $company_id]);
Unit::create(['name' => 'in', 'company_id' => $company_id]);
Unit::create(['name' => 'kg', 'company_id' => $company_id]);
Unit::create(['name' => 'km', 'company_id' => $company_id]);
Unit::create(['name' => 'lb', 'company_id' => $company_id]);
Unit::create(['name' => 'mg', 'company_id' => $company_id]);
Unit::create(['name' => 'pc', 'company_id' => $company_id]);
PaymentMethod::create(['name' => 'Cash', 'company_id' => $company_id]);
PaymentMethod::create(['name' => 'Check', 'company_id' => $company_id]);
PaymentMethod::create(['name' => 'Credit Card', 'company_id' => $company_id]);
PaymentMethod::create(['name' => 'Bank Transfer', 'company_id' => $company_id]);
Currency::create([
'name' => 'Serbian Dinar',
'code' => 'RSD',
'symbol' => 'RSD',
'precision' => '2',
'thousand_separator' => '.',
'decimal_separator' => ','
]);
}
public function databaseChanges()
{
$payments = Payment::all();
if ($payments) {
foreach ($payments as $payment) {
$payment->unique_hash = str_random(60);
$payment->save();
$paymentMethod = PaymentMethod::where('name', $payment->payment_mode)
->first();
if ($paymentMethod) {
$payment->payment_method_id = $paymentMethod->id;
$payment->save();
}
}
}
$items = Item::all();
if ($items) {
foreach ($items as $item) {
$unit = Unit::where('name', $item->unit)
->first();
if ($unit) {
$item->unit_id = $unit->id;
$item->save();
}
}
}
}
}

View File

@ -0,0 +1,52 @@
<?php
namespace Crater\Listeners\Updates\v3;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Crater\Listeners\Updates\Listener;
use Illuminate\Database\Schema\Blueprint;
use Crater\Events\UpdateFinished;
use Crater\Setting;
use Crater\Currency;
use Schema;
use Artisan;
class Version310 extends Listener
{
const VERSION = '3.1.0';
/**
* Handle the event.
*
* @param UpdateFinished $event
* @return void
*/
public function handle(UpdateFinished $event)
{
if ($this->isListenerFired($event)) {
return;
}
Currency::firstOrCreate(
[
'name' => 'Kyrgyzstani som',
'code' => 'KGS'
],
[
'name' => 'Kyrgyzstani som',
'code' => 'KGS',
'symbol' => 'С̲ ',
'precision' => '2',
'thousand_separator' => '.',
'decimal_separator' => ','
]
);
Artisan::call('migrate', ['--force' => true]);
// Update Crater app version
Setting::setSetting('version', static::VERSION);
}
}

View File

@ -12,17 +12,14 @@ class EstimatePdf extends Mailable
public $data = [];
public $notificationEmail = '';
/**
* Create a new message instance.
*
* @return void
*/
public function __construct($data, $notificationEmail)
public function __construct($data)
{
$this->data = $data;
$this->notificationEmail = $notificationEmail;
}
/**
@ -32,6 +29,9 @@ class EstimatePdf extends Mailable
*/
public function build()
{
return $this->from($this->notificationEmail)->markdown('emails.send.estimate', ['data', $this->data]);
$company = $this->data['company']['name'];
return $this->subject("Estimate from $company")
->markdown('emails.send.estimate', ['data', $this->data]);
}
}

View File

@ -31,6 +31,8 @@ class EstimateViewed extends Mailable
public function build()
{
$email = $this->data['user']['email'];
return $this->from($email)->markdown('emails.viewed.estimate', ['data', $this->data]);
$name = $this->data['user']['name'];
return $this->from($email, $name)
->markdown('emails.viewed.estimate', ['data', $this->data]);
}
}

View File

@ -6,23 +6,20 @@ use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Contracts\Queue\ShouldQueue;
class invoicePdf extends Mailable
class InvoicePdf extends Mailable
{
use Queueable, SerializesModels;
public $data = [];
public $notificationEmail = '';
/**
* Create a new message instance.
*
* @return void
*/
public function __construct($data, $notificationEmail)
public function __construct($data)
{
$this->data = $data;
$this->notificationEmail = $notificationEmail;
}
/**
@ -32,6 +29,9 @@ class invoicePdf extends Mailable
*/
public function build()
{
return $this->from($this->notificationEmail)->markdown('emails.send.invoice', ['data', $this->data]);
$company = $this->data['company']['name'];
return $this->subject("Invoice from $company")
->markdown('emails.send.invoice', ['data', $this->data]);
}
}

View File

@ -31,6 +31,8 @@ class InvoiceViewed extends Mailable
public function build()
{
$email = $this->data['user']['email'];
return $this->from($email)->markdown('emails.viewed.invoice', ['data', $this->data]);
$name = $this->data['user']['name'];
return $this->from($email, $name)
->markdown('emails.viewed.invoice', ['data', $this->data]);
}
}

38
app/Mail/PaymentPdf.php Normal file
View File

@ -0,0 +1,38 @@
<?php
namespace Crater\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
class PaymentPdf extends Mailable
{
use Queueable, SerializesModels;
public $data = [];
/**
* Create a new message instance.
*
* @return void
*/
public function __construct($data)
{
$this->data = $data;
}
/**
* Build the message.
*
* @return $this
*/
public function build()
{
$company = $this->data['company']['name'];
return $this->subject("Payment from $company")
->markdown('emails.send.payment', ['data', $this->data]);
}
}

38
app/Mail/TestMail.php Normal file
View File

@ -0,0 +1,38 @@
<?php
namespace Crater\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Contracts\Queue\ShouldQueue;
class TestMail extends Mailable
{
use Queueable, SerializesModels;
public $subject;
public $message;
/**
* Create a new message instance.
*
* @param $subject
* @param $message
*/
public function __construct($subject, $message)
{
$this->subject = $subject;
$this->message = $message;
}
/**
* Build the message.
*
* @return $this
*/
public function build()
{
return $this->subject($this->subject)->markdown('emails.test')->with([
'my_message' => $this->message
]);
}
}

View File

@ -5,6 +5,7 @@ use Crater\User;
use Crater\Invoice;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Model;
use Crater\PaymentMethod;
class Payment extends Model
{
@ -19,9 +20,11 @@ class Payment extends Model
protected $fillable = [
'user_id',
'invoice_id',
'payment_method_id',
'payment_date',
'company_id',
'notes',
'unique_hash',
'payment_number',
'payment_mode',
'amount'
@ -32,10 +35,34 @@ class Payment extends Model
'formattedPaymentDate'
];
public static function getNextPaymentNumber()
private function strposX($haystack, $needle, $number)
{
if ($number == '1') {
return strpos($haystack, $needle);
} elseif ($number > '1') {
return strpos(
$haystack,
$needle,
$this->strposX($haystack, $needle, $number - 1) + strlen($needle)
);
} else {
return error_log('Error: Value for parameter $number is out of range');
}
}
public function getPaymentNumAttribute()
{
$position = $this->strposX($this->payment_number, "-", 1) + 1;
return substr($this->payment_number, $position);
}
public static function getNextPaymentNumber($value)
{
// Get the last created order
$payment = Payment::orderBy('created_at', 'desc')->first();
$payment = Payment::where('payment_number', 'LIKE', $value . '-%')
->orderBy('created_at', 'desc')
->first();
if (!$payment) {
// We get here if there is no order at all
// If there is no number set it to 0, which will be 1 at the end.
@ -54,6 +81,12 @@ class Payment extends Model
return sprintf('%06d', intval($number) + 1);
}
public function getPaymentPrefixAttribute ()
{
$prefix= explode("-",$this->payment_number)[0];
return $prefix;
}
public function invoice()
{
return $this->belongsTo(Invoice::class);
@ -64,6 +97,11 @@ class Payment extends Model
return $this->belongsTo(User::class);
}
public function paymentMethod()
{
return $this->belongsTo(PaymentMethod::class);
}
public function getFormattedCreatedAtAttribute($value)
{
$dateFormat = CompanySetting::getSetting('carbon_date_format', $this->company_id);
@ -92,9 +130,9 @@ class Payment extends Model
return $query->where('payments.payment_number', 'LIKE', '%'.$paymentNumber.'%');
}
public function scopePaymentMode($query, $paymentMode)
public function scopePaymentMethod($query, $paymentMethodId)
{
return $query->where('payments.payment_mode', $paymentMode);
return $query->where('payments.payment_method_id', $paymentMethodId);
}
public function scopeApplyFilters($query, array $filters)
@ -109,8 +147,8 @@ class Payment extends Model
$query->paymentNumber($filters->get('payment_number'));
}
if ($filters->get('payment_mode')) {
$query->paymentMode($filters->get('payment_mode'));
if ($filters->get('payment_method_id')) {
$query->paymentMethod($filters->get('payment_method_id'));
}
if ($filters->get('customer_id')) {

25
app/PaymentMethod.php Normal file
View File

@ -0,0 +1,25 @@
<?php
namespace Crater;
use Illuminate\Database\Eloquent\Model;
class PaymentMethod extends Model
{
protected $fillable = ['name', 'company_id'];
public function payments()
{
return $this->hasMany(Payment::class);
}
public function company()
{
return $this->belongsTo(Company::class);
}
public function scopeWhereCompany($query, $company_id)
{
$query->where('company_id', $company_id);
}
}

View File

@ -1,4 +1,5 @@
<?php
namespace Crater\Providers;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
@ -9,6 +10,9 @@ use Crater\Listeners\Updates\v1\Version110;
use Crater\Listeners\Updates\v2\Version200;
use Crater\Listeners\Updates\v2\Version201;
use Crater\Listeners\Updates\v2\Version202;
use Crater\Listeners\Updates\v2\Version210;
use Crater\Listeners\Updates\v3\Version300;
use Crater\Listeners\Updates\v3\Version310;
class EventServiceProvider extends ServiceProvider
{
@ -18,11 +22,14 @@ class EventServiceProvider extends ServiceProvider
* @var array
*/
protected $listen = [
UpdateFinished::class=> [
UpdateFinished::class => [
Version110::class,
Version200::class,
Version201::class,
Version202::class,
Version210::class,
Version300::class,
Version310::class,
],
Registered::class => [
SendEmailVerificationNotification::class,

View File

@ -38,7 +38,7 @@ class EnvironmentManager
'DB_PORT='.config('database.connections.'.config('database.default').'.port')."\n".
'DB_DATABASE='.config('database.connections.'.config('database.default').'.database')."\n".
'DB_USERNAME='.config('database.connections.'.config('database.default').'.username')."\n".
'DB_PASSWORD='.config('database.connections.'.config('database.default').'.password')."\n\n";
'DB_PASSWORD="'.config('database.connections.'.config('database.default').'.password')."\"\n\n";
$newDatabaseData =
'DB_CONNECTION='.$request->database_connection."\n".
@ -46,7 +46,7 @@ class EnvironmentManager
'DB_PORT='.$request->database_port."\n".
'DB_DATABASE='.$request->database_name."\n".
'DB_USERNAME='.$request->database_username."\n".
'DB_PASSWORD='.$request->database_password."\n\n";
'DB_PASSWORD="'.$request->database_password."\"\n\n";
try {
@ -61,7 +61,7 @@ class EnvironmentManager
} catch (Exception $e) {
return [
'error' => $e->getMessage()
'error_message' => $e->getMessage()
];
}

View File

@ -2,24 +2,44 @@
namespace Crater\Space;
use File;
use ZipArchive;
use Artisan;
use GuzzleHttp\Exception\RequestException;
use Crater\Space\SiteApi;
use Crater\Events\UpdateFinished;
use Crater\Setting;
use Illuminate\Http\Request;
use ZipArchive;
class Updater
{
use SiteApi;
public static function update($installed, $version)
public static function checkForUpdate($installed_version)
{
$data = null;
if(env('APP_ENV') === 'development')
{
$url = 'https://craterapp.com/downloads/check/latest/'. $installed_version . '?type=update&is_dev=1';
} else {
$url = 'https://craterapp.com/downloads/check/latest/'. $installed_version . '?type=update';
}
$response = static::getRemote($url, ['timeout' => 100, 'track_redirects' => true]);
if ($response && ($response->getStatusCode() == 200)) {
$data = $response->getBody()->getContents();
}
return json_decode($data);
}
public static function download($new_version)
{
$data = null;
$path = null;
$url = 'https://craterapp.com/downloads/file/'.$version.'?type=update';
if (env('APP_ENV') === 'development') {
$url = 'https://craterapp.com/downloads/file/' . $new_version . '?type=update&is_dev=1';
} else {
$url = 'https://craterapp.com/downloads/file/' . $new_version . '?type=update';
}
$response = static::getRemote($url, ['timeout' => 100, 'track_redirects' => true]);
@ -39,66 +59,68 @@ class Updater
}
// Create temp directory
$path = 'temp-' . md5(mt_rand());
$path2 = 'temp2-' . md5(mt_rand());
$temp_path = storage_path('app') . '/' . $path;
$temp_path2 = storage_path('app') . '/' . $path2;
$temp_dir = storage_path('app/temp-' . md5(mt_rand()));
if (!File::isDirectory($temp_path)) {
File::makeDirectory($temp_path);
File::makeDirectory($temp_path2);
if (!File::isDirectory($temp_dir)) {
File::makeDirectory($temp_dir);
}
try {
$zip_file_path = $temp_dir . '/upload.zip';
$file = $temp_path . '/upload.zip';
// Add content to the Zip file
$uploaded = is_int(file_put_contents($zip_file_path, $data)) ? true : false;
// Add content to the Zip file
$uploaded = is_int(file_put_contents($file, $data)) ? true : false;
if (!$uploaded) {
return false;
}
// Unzip the file
$zip = new ZipArchive();
if ($zip->open($file)) {
$zip->extractTo($temp_path2);
}
$zip->close();
// Delete zip file
File::delete($file);
if (!File::copyDirectory($temp_path2.'/Crater', base_path())) {
return false;
}
// Delete temp directory
File::deleteDirectory($temp_path);
File::deleteDirectory($temp_path2);
return [
'success' => true,
'error' => false,
'data' => []
];
} catch (\Exception $e) {
if (File::isDirectory($temp_path)) {
// Delete temp directory
File::deleteDirectory($temp_path);
File::deleteDirectory($temp_path2);
}
return [
'success' => false,
'error' => 'Update error',
'data' => []
];
if (!$uploaded) {
return false;
}
return $zip_file_path;
}
public static function unzip($zip_file_path)
{
if(!file_exists($zip_file_path)) {
throw new \Exception('Zip file not found');
}
$temp_extract_dir = storage_path('app/temp2-' . md5(mt_rand()));
if (!File::isDirectory($temp_extract_dir)) {
File::makeDirectory($temp_extract_dir);
}
// Unzip the file
$zip = new ZipArchive();
if ($zip->open($zip_file_path)) {
$zip->extractTo($temp_extract_dir);
}
$zip->close();
// Delete zip file
File::delete($zip_file_path);
return $temp_extract_dir;
}
public static function copyFiles($temp_extract_dir)
{
if (!File::copyDirectory($temp_extract_dir . '/Crater', base_path())) {
return false;
}
// Delete temp directory
File::deleteDirectory($temp_extract_dir);
return true;
}
public static function migrateUpdate()
{
Artisan::call('migrate --force');
return true;
}
public static function finishUpdate($installed, $version)
@ -112,17 +134,4 @@ class Updater
];
}
public static function checkForUpdate()
{
$data = null;
$url = 'https://craterapp.com/downloads/check/latest/'. Setting::getSetting('version') . '?type=update';
$response = static::getRemote($url, ['timeout' => 100, 'track_redirects' => true]);
if ($response && ($response->getStatusCode() == 200)) {
$data = $response->getBody()->getContents();
}
return json_decode($data);
}
}

View File

@ -41,10 +41,14 @@ function clean_slug($string)
* @param $money
* @return formated_money
*/
function format_money_pdf($money)
function format_money_pdf($money, $currency = null)
{
$money = $money / 100;
$currency = Currency::findOrFail(CompanySetting::getSetting('currency', 1));
if (!$currency) {
$currency = Currency::findOrFail(CompanySetting::getSetting('currency', 1));
}
$format_money = number_format(
$money,
$currency->precision,

26
app/Unit.php Normal file
View File

@ -0,0 +1,26 @@
<?php
namespace Crater;
use Illuminate\Database\Eloquent\Model;
use Crater\Item;
class Unit extends Model
{
protected $fillable = ['name', 'company_id'];
public function items()
{
return $this->hasMany(Item::class);
}
public function company()
{
return $this->belongsTo(Company::class);
}
public function scopeWhereCompany($query, $company_id)
{
$query->where('company_id', $company_id);
}
}

View File

@ -9,6 +9,7 @@ use carbon\carbon;
use Crater\MemberLoan;
use Crater\Address;
use Crater\Payment;
use Crater\Expense;
use Crater\Company;
use Crater\Notifications\MailResetPasswordNotification;
use Spatie\MediaLibrary\HasMedia\HasMedia;
@ -105,6 +106,11 @@ class User extends Authenticatable implements HasMedia
return $this->hasMany(Address::class);
}
public function expenses()
{
return $this->hasMany(Expense::class);
}
public function billingAddress()
{
return $this->hasOne(Address::class)->where('type', Address::BILLING_TYPE);

View File

@ -1,6 +1,6 @@
{
"name": "laravel/laravel",
"description": "The Laravel Framework.",
"name": "bytefury/crater",
"description": "Free & Open Source Invoice App for Freelancers & Small Businesses. https://craterapp.com",
"keywords": [
"framework",
"laravel"
@ -9,6 +9,7 @@
"type": "project",
"require": {
"php": "^7.2",
"aws/aws-sdk-php": "^3.137",
"barryvdh/laravel-dompdf": "^0.8.1",
"doctrine/dbal": "^2.10",
"fideloper/proxy": "^4.0",
@ -53,11 +54,20 @@
"minimum-stability": "dev",
"prefer-stable": true,
"scripts": {
"initial-setup": [
"test -f .env || (cp .env.example .env; php artisan key:generate 2>/dev/null; exit 0)"
],
"pre-install-cmd": [
"@initial-setup"
],
"pre-update-cmd": [
"@initial-setup"
],
"post-root-package-install": [
"php -r \"file_exists('.env') || copy('.env.example', '.env');\""
"@initial-setup"
],
"post-create-project-cmd": [
"php artisan key:generate --ansi"
"@initial-setup"
],
"post-autoload-dump": [
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",

1719
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,4 @@
<?php
<?php
return [

View File

@ -9,6 +9,6 @@ return [
|
*/
'version' => '2.0.2',
'version' => '3.1.0',
];

244
config/dompdf.php Normal file
View File

@ -0,0 +1,244 @@
<?php
return array(
/*
|--------------------------------------------------------------------------
| Settings
|--------------------------------------------------------------------------
|
| Set some default values. It is possible to add all defines that can be set
| in dompdf_config.inc.php. You can also override the entire config file.
|
*/
'show_warnings' => false, // Throw an Exception on warnings from dompdf
'orientation' => 'portrait',
'defines' => array(
/**
* The location of the DOMPDF font directory
*
* The location of the directory where DOMPDF will store fonts and font metrics
* Note: This directory must exist and be writable by the webserver process.
* *Please note the trailing slash.*
*
* Notes regarding fonts:
* Additional .afm font metrics can be added by executing load_font.php from command line.
*
* Only the original "Base 14 fonts" are present on all pdf viewers. Additional fonts must
* be embedded in the pdf file or the PDF may not display correctly. This can significantly
* increase file size unless font subsetting is enabled. Before embedding a font please
* review your rights under the font license.
*
* Any font specification in the source HTML is translated to the closest font available
* in the font directory.
*
* The pdf standard "Base 14 fonts" are:
* Courier, Courier-Bold, Courier-BoldOblique, Courier-Oblique,
* Helvetica, Helvetica-Bold, Helvetica-BoldOblique, Helvetica-Oblique,
* Times-Roman, Times-Bold, Times-BoldItalic, Times-Italic,
* Symbol, ZapfDingbats.
*/
"font_dir" => storage_path('fonts/'), // advised by dompdf (https://github.com/dompdf/dompdf/pull/782)
/**
* The location of the DOMPDF font cache directory
*
* This directory contains the cached font metrics for the fonts used by DOMPDF.
* This directory can be the same as DOMPDF_FONT_DIR
*
* Note: This directory must exist and be writable by the webserver process.
*/
"font_cache" => storage_path('fonts/'),
/**
* The location of a temporary directory.
*
* The directory specified must be writeable by the webserver process.
* The temporary directory is required to download remote images and when
* using the PFDLib back end.
*/
"temp_dir" => sys_get_temp_dir(),
/**
* ==== IMPORTANT ====
*
* dompdf's "chroot": Prevents dompdf from accessing system files or other
* files on the webserver. All local files opened by dompdf must be in a
* subdirectory of this directory. DO NOT set it to '/' since this could
* allow an attacker to use dompdf to read any files on the server. This
* should be an absolute path.
* This is only checked on command line call by dompdf.php, but not by
* direct class use like:
* $dompdf = new DOMPDF(); $dompdf->load_html($htmldata); $dompdf->render(); $pdfdata = $dompdf->output();
*/
"chroot" => realpath(base_path()),
/**
* Whether to enable font subsetting or not.
*/
"enable_font_subsetting" => false,
/**
* The PDF rendering backend to use
*
* Valid settings are 'PDFLib', 'CPDF' (the bundled R&OS PDF class), 'GD' and
* 'auto'. 'auto' will look for PDFLib and use it if found, or if not it will
* fall back on CPDF. 'GD' renders PDFs to graphic files. {@link
* Canvas_Factory} ultimately determines which rendering class to instantiate
* based on this setting.
*
* Both PDFLib & CPDF rendering backends provide sufficient rendering
* capabilities for dompdf, however additional features (e.g. object,
* image and font support, etc.) differ between backends. Please see
* {@link PDFLib_Adapter} for more information on the PDFLib backend
* and {@link CPDF_Adapter} and lib/class.pdf.php for more information
* on CPDF. Also see the documentation for each backend at the links
* below.
*
* The GD rendering backend is a little different than PDFLib and
* CPDF. Several features of CPDF and PDFLib are not supported or do
* not make any sense when creating image files. For example,
* multiple pages are not supported, nor are PDF 'objects'. Have a
* look at {@link GD_Adapter} for more information. GD support is
* experimental, so use it at your own risk.
*
* @link http://www.pdflib.com
* @link http://www.ros.co.nz/pdf
* @link http://www.php.net/image
*/
"pdf_backend" => "CPDF",
/**
* PDFlib license key
*
* If you are using a licensed, commercial version of PDFlib, specify
* your license key here. If you are using PDFlib-Lite or are evaluating
* the commercial version of PDFlib, comment out this setting.
*
* @link http://www.pdflib.com
*
* If pdflib present in web server and auto or selected explicitely above,
* a real license code must exist!
*/
//"DOMPDF_PDFLIB_LICENSE" => "your license key here",
/**
* html target media view which should be rendered into pdf.
* List of types and parsing rules for future extensions:
* http://www.w3.org/TR/REC-html40/types.html
* screen, tty, tv, projection, handheld, print, braille, aural, all
* Note: aural is deprecated in CSS 2.1 because it is replaced by speech in CSS 3.
* Note, even though the generated pdf file is intended for print output,
* the desired content might be different (e.g. screen or projection view of html file).
* Therefore allow specification of content here.
*/
"default_media_type" => "screen",
/**
* The default paper size.
*
* North America standard is "letter"; other countries generally "a4"
*
* @see CPDF_Adapter::PAPER_SIZES for valid sizes ('letter', 'legal', 'A4', etc.)
*/
"default_paper_size" => "a4",
/**
* The default font family
*
* Used if no suitable fonts can be found. This must exist in the font folder.
* @var string
*/
"default_font" => "DejaVu Sans",
/**
* Image DPI setting
*
* This setting determines the default DPI setting for images and fonts. The
* DPI may be overridden for inline images by explictly setting the
* image's width & height style attributes (i.e. if the image's native
* width is 600 pixels and you specify the image's width as 72 points,
* the image will have a DPI of 600 in the rendered PDF. The DPI of
* background images can not be overridden and is controlled entirely
* via this parameter.
*
* For the purposes of DOMPDF, pixels per inch (PPI) = dots per inch (DPI).
* If a size in html is given as px (or without unit as image size),
* this tells the corresponding size in pt.
* This adjusts the relative sizes to be similar to the rendering of the
* html page in a reference browser.
*
* In pdf, always 1 pt = 1/72 inch
*
* Rendering resolution of various browsers in px per inch:
* Windows Firefox and Internet Explorer:
* SystemControl->Display properties->FontResolution: Default:96, largefonts:120, custom:?
* Linux Firefox:
* about:config *resolution: Default:96
* (xorg screen dimension in mm and Desktop font dpi settings are ignored)
*
* Take care about extra font/image zoom factor of browser.
*
* In images, <img> size in pixel attribute, img css style, are overriding
* the real image dimension in px for rendering.
*
* @var int
*/
"dpi" => 96,
/**
* Enable inline PHP
*
* If this setting is set to true then DOMPDF will automatically evaluate
* inline PHP contained within <script type="text/php"> ... </script> tags.
*
* Enabling this for documents you do not trust (e.g. arbitrary remote html
* pages) is a security risk. Set this option to false if you wish to process
* untrusted documents.
*
* @var bool
*/
"enable_php" => false,
/**
* Enable inline Javascript
*
* If this setting is set to true then DOMPDF will automatically insert
* JavaScript code contained within <script type="text/javascript"> ... </script> tags.
*
* @var bool
*/
"enable_javascript" => true,
/**
* Enable remote file access
*
* If this setting is set to true, DOMPDF will access remote sites for
* images and CSS files as required.
* This is required for part of test case www/test/image_variants.html through www/examples.php
*
* Attention!
* This can be a security risk, in particular in combination with DOMPDF_ENABLE_PHP and
* allowing remote access to dompdf.php or on allowing remote html code to be passed to
* $dompdf = new DOMPDF(, $dompdf->load_html(...,
* This allows anonymous users to download legally doubtful internet content which on
* tracing back appears to being downloaded by your server, or allows malicious php code
* in remote html pages to be executed by your server with your account privileges.
*
* @var bool
*/
"enable_remote" => true,
/**
* A ratio applied to the fonts height to be more like browsers' line height
*/
"font_height_ratio" => 1.1,
/**
* Use the more-than-experimental HTML5 Lib parser
*/
"enable_html5_parser" => true,
),
);

View File

@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateUnitsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
if (!Schema::hasTable('units')) {
Schema::create('units', function (Blueprint $table) {
$table->increments('id');
$table->string('name');
$table->integer('company_id')->unsigned()->nullable();
$table->foreign('company_id')->references('id')->on('companies')->onDelete('cascade');
$table->timestamps();
});
}
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('units');
}
}

View File

@ -21,6 +21,8 @@ class CreateItemsTable extends Migration
$table->unsignedBigInteger('price');
$table->integer('company_id')->unsigned()->nullable();
$table->foreign('company_id')->references('id')->on('companies')->onDelete('cascade');
$table->integer('unit_id')->unsigned()->nullable();
$table->foreign('unit_id')->references('id')->on('units')->onDelete('cascade');
$table->timestamps();
});
}

View File

@ -30,6 +30,6 @@ class CreateExpenseCategoriesTable extends Migration
*/
public function down()
{
Schema::dropIfExists('expenses_categories');
Schema::dropIfExists('expense_categories');
}
}

View File

@ -39,6 +39,6 @@ class CreateAddressesTable extends Migration
*/
public function down()
{
Schema::dropIfExists('address');
Schema::dropIfExists('addresses');
}
}

View File

@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreatePaymentMethodsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
if (!Schema::hasTable('payment_methods')) {
Schema::create('payment_methods', function (Blueprint $table) {
$table->increments('id');
$table->string('name');
$table->integer('company_id')->unsigned()->nullable();
$table->foreign('company_id')->references('id')->on('companies')->onDelete('cascade');
$table->timestamps();
});
}
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('payment_methods');
}
}

View File

@ -16,16 +16,18 @@ class CreatePaymentsTable extends Migration
Schema::create('payments', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('payment_number');
$table->string('payment_mode')->nullable();
$table->date('payment_date');
$table->text('notes')->nullable();
$table->unsignedBigInteger('amount');
$table->string('unique_hash')->nullable();
$table->integer('user_id')->unsigned();
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
$table->integer('invoice_id')->unsigned()->nullable();
$table->foreign('invoice_id')->references('id')->on('invoices')->onDelete('cascade');
$table->integer('company_id')->unsigned()->nullable();
$table->foreign('company_id')->references('id')->on('companies')->onDelete('cascade');
$table->integer('payment_method_id')->unsigned()->nullable();
$table->foreign('payment_method_id')->references('id')->on('payment_methods')->onDelete('cascade');
$table->timestamps();
});
}

View File

@ -20,9 +20,9 @@ class CreateMediaTable extends Migration
$table->string('mime_type')->nullable();
$table->string('disk');
$table->unsignedInteger('size');
$table->json('manipulations');
$table->json('custom_properties');
$table->json('responsive_images');
$table->text('manipulations');
$table->text('custom_properties');
$table->text('responsive_images');
$table->unsignedInteger('order_column')->nullable();
$table->nullableTimestamps();
});

View File

@ -0,0 +1,33 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddUserIdToExpensesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('expenses', function (Blueprint $table) {
$table->integer('user_id')->unsigned()->nullable();
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('expenses', function (Blueprint $table) {
$table->dropColumn('paid');
});
}
}

View File

@ -524,6 +524,22 @@ class CurrenciesTableSeeder extends Seeder
'thousand_separator' => '.',
'decimal_separator' => ','
],
[
'name' => 'Serbian Dinar',
'code' => 'RSD',
'symbol' => 'RSD',
'precision' => '2',
'thousand_separator' => '.',
'decimal_separator' => ','
],
[
'name' => 'Kyrgyzstani som',
'code' => 'KGS',
'symbol' => 'С̲ ',
'precision' => '2',
'thousand_separator' => '.',
'decimal_separator' => ','
],
];
foreach ($currencies as $currency) {

View File

@ -0,0 +1,20 @@
<?php
use Illuminate\Database\Seeder;
use Crater\PaymentMethod;
class PaymentMethodSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
PaymentMethod::create(['name' => 'Cash', 'company_id' => 1]);
PaymentMethod::create(['name' => 'Check', 'company_id' => 1]);
PaymentMethod::create(['name' => 'Credit Card', 'company_id' => 1]);
PaymentMethod::create(['name' => 'Bank Transfer', 'company_id' => 1]);
}
}

View File

@ -0,0 +1,27 @@
<?php
use Illuminate\Database\Seeder;
use Crater\Unit;
class UnitSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
Unit::create(['name' => 'box', 'company_id' => 1]);
Unit::create(['name' => 'cm', 'company_id' => 1]);
Unit::create(['name' => 'dz', 'company_id' => 1]);
Unit::create(['name' => 'ft', 'company_id' => 1]);
Unit::create(['name' => 'g', 'company_id' => 1]);
Unit::create(['name' => 'in', 'company_id' => 1]);
Unit::create(['name' => 'kg', 'company_id' => 1]);
Unit::create(['name' => 'km', 'company_id' => 1]);
Unit::create(['name' => 'lb', 'company_id' => 1]);
Unit::create(['name' => 'mg', 'company_id' => 1]);
Unit::create(['name' => 'pc', 'company_id' => 1]);
}
}

View File

@ -0,0 +1,40 @@
version: '3.1'
services:
web:
image: nginx
depends_on:
- php
ports:
- 8080:80
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- app:/app
restart: always
php:
build: .
depends_on:
- db
expose:
- 9000
volumes:
- app:/app
restart: always
db:
image: mariadb
restart: always
volumes:
- db:/var/lib/mysql
environment:
MYSQL_USER: crater
MYSQL_PASSWORD: crater
MYSQL_DATABASE: crater
MYSQL_ROOT_PASSWORD: crater
volumes:
app:
db:

53
nginx.conf Normal file
View File

@ -0,0 +1,53 @@
worker_processes 8;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 4096;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
keepalive_timeout 65;
server {
listen 80 default_server;
root /app/public;
index index.php;
charset utf-8;
access_log off;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location = /favicon.ico { access_log off; log_not_found off; }
location = /robots.txt { access_log off; log_not_found off; }
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
add_header X-Robots-Tag none;
add_header Content-Security-Policy "frame-ancestors 'self'";
location ~ \.php$ {
fastcgi_pass php:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include /etc/nginx/fastcgi_params;
}
}
}

5020
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -8,19 +8,16 @@
"production": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js"
},
"devDependencies": {
"babel-eslint": "^8.2.3",
"browser-sync": "^2.26.7",
"browser-sync-webpack-plugin": "^2.0.1",
"babel-eslint": "^8.2.6",
"cross-env": "^5.1",
"css-loader": "^0.28.8",
"eslint": "^4.14.0",
"eslint-config-standard": "^11.0.0-beta.0",
"eslint-plugin-import": "^2.11.0",
"eslint-plugin-node": "^5.2.1",
"eslint-plugin-promise": "^3.6.0",
"eslint-plugin-standard": "^3.0.1",
"eslint-plugin-vue": "^4.0.1",
"eslint": "^4.19.1",
"eslint-config-prettier": "^6.10.1",
"eslint-loader": "^3.0.3",
"eslint-plugin-prettier": "^3.1.2",
"eslint-plugin-vue": "^4.7.1",
"laravel-mix": "^5.0.0",
"prettier": "^2.0.2",
"resolve-url-loader": "3.1.0",
"sass": "^1.22.9",
"sass-loader": "7.*",
@ -35,19 +32,14 @@
"axios": "^0.19",
"bootstrap": "^4.1.0",
"chart.js": "^2.7.3",
"cross-env": "^5.1.4",
"easy-pie-chart": "^2.1.7",
"fs": "0.0.1-security",
"guid": "0.0.12",
"lodash": "^4.17.13",
"moment": "^2.18.1",
"npm": "^6.4.1",
"popper.js": "^1.12.9",
"sweet-modal-vue": "^2.0.0",
"sweetalert": "^2.1.2",
"toastr": "^2.1.4",
"upgrade": "^1.1.0",
"v-money": "^0.8.1",
"v-tooltip": "^2.0.2",
"vue": "^2.5.17",
"vue-avatar-cropper": "^1.0.5",
"vue-i18n": "^8.14.0",

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,4 +1,4 @@
{
"/assets/js/app.js": "/assets/js/app.js?id=36ab3529ebffd4f0624b",
"/assets/css/crater.css": "/assets/css/crater.css?id=108e3a8d009e7d38018c"
"/assets/js/app.js": "/assets/js/app.js?id=2521d0dcc4cb4e4975a5",
"/assets/css/crater.css": "/assets/css/crater.css?id=84a4eeb53b0e6a937e44"
}

View File

@ -29,7 +29,7 @@ Web Application is made using Laravel & VueJS while the Mobile Apps are built us
## Mobile Apps
- [Android](https://play.google.com/store/apps/details?id=com.craterapp.app)
- IOS - Coming Soon
- [IOS](https://apps.apple.com/app/id1489169767)
- [Source](https://github.com/bytefury/crater-mobile)
## Discord
@ -60,8 +60,14 @@ Join the Crater discord server to discuss:
## Credits
Crater is a product of [Bytefury](https://bytefury.com)
**Special thanks to:**
* [Birkhoff Lee](https://github.com/BirkhoffLee)
* [Hassan A. Ba Abdullah](https://github.com/hsnapps)
## Translate
Help us translate on [Transifex](https://www.transifex.com/bytefury/crater-invoice)
Help us translate or suggest changes to existing languages if you find any mistakes by creating a new PR.
Here's the [english-version](https://github.com/bytefury/crater/blob/master/resources/assets/js/plugins/en.json) json file which you can use as a reference.
## License
Crater is released under the Attribution Assurance License.

View File

@ -12,6 +12,7 @@ import CustomerModal from './components/base/modal/CustomerModal.vue'
import TaxTypeModal from './components/base/modal/TaxTypeModal.vue'
import CategoryModal from './components/base/modal/CategoryModal.vue'
import money from 'v-money'
import VTooltip from 'v-tooltip'
/**
* Global css plugins
@ -87,17 +88,27 @@ window.axios.interceptors.request.use(function (config) {
global.axios.interceptors.response.use(undefined, function (err) {
// Do something with request error
return new Promise((resolve, reject) => {
if (!err.response) {
window.toastr['error']('Network error: Please check your internet connection or wait until servers are back online')
console.log('Network error: Please check your internet connection.')
} else {
console.log(err.response)
if (err.response.data.error === 'invalid_credentials') {
window.toastr['error']('Invalid Credentials')
}
if (err.response.data && (err.response.statusText === 'Unauthorized' || err.response.data === ' Unauthorized.')) {
// Unauthorized and log out
window.toastr['error']((err.response.data.message) ? err.response.data.message : 'Unauthorized')
store.dispatch('auth/logout', true)
} else if (err.response.data.errors) {
// Show a notification per error
const errors = JSON.parse(JSON.stringify(err.response.data.errors))
for (const i in errors) {
window.toastr['error'](errors[i])
}
} else {
throw err
// Unknown error
window.toastr['error']((err.response.data.message) ? err.response.data.message : 'Unknown error occurred')
}
})
}
return Promise.reject(err)
})
/**
@ -107,6 +118,7 @@ window.toastr = require('toastr')
Vue.use(VueRouter)
Vue.use(Vuex)
Vue.use(VTooltip)
// register directive v-money and component <money>
Vue.use(money, {precision: 2})

View File

@ -0,0 +1,71 @@
<template>
<div class="base-prefix-input" @click="focusInput">
<font-awesome-icon v-if="icon" :icon="icon" class="icon" />
<p class="prefix-label"><span class="mr-1">{{ prefix }}</span>-</p>
<input
ref="basePrefixInput"
v-model="inputValue"
:type="type"
class="prefix-input-field"
@input="handleInput"
@change="handleChange"
@keyup="handleKeyupEnter"
@keydown="handleKeyDownEnter"
@blur="handleFocusOut"
>
</div>
</template>
<script>
export default {
props: {
prefix: {
type: String,
default: null,
required: true
},
icon: {
type: String,
default: null
},
value: {
type: [String, Number, File],
default: ''
},
type: {
type: String,
default: 'text'
}
},
data () {
return {
inputValue: this.value
}
},
watch: {
'value' () {
this.inputValue = this.value
}
},
methods: {
focusInput () {
this.$refs.basePrefixInput.focus()
},
handleInput (e) {
this.$emit('input', this.inputValue)
},
handleChange (e) {
this.$emit('change', this.inputValue)
},
handleKeyupEnter (e) {
this.$emit('keyup', this.inputValue)
},
handleKeyDownEnter (e) {
this.$emit('keydown', e, this.inputValue)
},
handleFocusOut (e) {
this.$emit('blur', this.inputValue)
}
}
}
</script>

View File

@ -8,6 +8,7 @@ import BaseTextArea from './BaseTextArea.vue'
import BaseSelect from './base-select/BaseSelect.vue'
import BaseLoader from './BaseLoader.vue'
import BaseCustomerSelect from './BaseCustomerSelect.vue'
import BasePrefixInput from './BasePrefixInput.vue'
import BasePopup from './popup/BasePopup.vue'
import CustomerSelectPopup from './popup/CustomerSelectPopup.vue'
@ -23,6 +24,7 @@ Vue.component('base-input', BaseInput)
Vue.component('base-switch', BaseSwitch)
Vue.component('base-text-area', BaseTextArea)
Vue.component('base-loader', BaseLoader)
Vue.component('base-prefix-input', BasePrefixInput)
Vue.component('table-component', TableComponent)
Vue.component('table-column', TableColumn)

View File

@ -21,6 +21,9 @@ import EstimateTemplate from './EstimateTemplate'
import InvoiceTemplate from './InvoiceTemplate'
import CustomerModal from './CustomerModal'
import CategoryModal from './CategoryModal'
import PaymentMode from './PaymentModeModal'
import ItemUnit from './ItemUnitModal'
import MailTestModal from './MailTestModal'
export default {
components: {
@ -29,7 +32,10 @@ export default {
EstimateTemplate,
InvoiceTemplate,
CustomerModal,
CategoryModal
CategoryModal,
PaymentMode,
ItemUnit,
MailTestModal
},
data () {
return {

View File

@ -55,6 +55,7 @@
v-model="currency"
:options="currencies"
:searchable="true"
:allow-empty="false"
:show-labels="false"
:placeholder="$t('customers.select_currency')"
label="name"
@ -340,6 +341,7 @@ export default {
mixins: [validationMixin],
data () {
return {
isEdit: false,
isLoading: false,
countryList: [],
billingCountry: null,
@ -398,9 +400,22 @@ export default {
...mapGetters('currency', [
'defaultCurrency',
'currencies'
]),
...mapGetters('modal', [
'modalDataID',
'modalData',
'modalActive'
])
},
watch: {
'modalDataID' (val) {
if (val) {
this.isEdit = true
this.setData()
} else {
this.isEdit = false
}
},
billingCountry () {
if (this.billingCountry) {
this.billing.country_id = this.billingCountry.id
@ -418,6 +433,9 @@ export default {
this.$refs.name.focus = true
this.currency = this.defaultCurrency
this.fetchCountry()
if (this.modalDataID) {
this.setData()
}
},
methods: {
...mapActions('invoice', {
@ -492,6 +510,24 @@ export default {
this.formData.addresses = [{...this.shipping, type: 'shipping'}]
return true
},
async setData () {
this.formData.id = this.modalData.id
this.formData.name = this.modalData.name
this.formData.email = this.modalData.email
this.formData.contact_name = this.modalData.contact_name
this.formData.phone = this.modalData.phone
this.formData.website = this.modalData.website
this.currency = this.modalData.currency
if (this.modalData.billing_address) {
this.billing = this.modalData.billing_address
this.billingCountry = this.modalData.billing_address.country
}
if (this.modalData.shipping_address) {
this.shipping = this.modalData.shipping_address
this.shippingCountry = this.modalData.shipping_address.country
}
},
async submitCustomerData () {
this.$v.formData.$touch()
@ -509,14 +545,23 @@ export default {
this.formData.currency_id = this.defaultCurrency.id
}
try {
let response = await this.addCustomer(this.formData)
let response = null
if (this.modalDataID) {
response = await this.updateCustomer(this.formData)
} else {
response = await this.addCustomer(this.formData)
}
if (response.data) {
window.toastr['success'](this.$tc('customers.created_message'))
if (this.modalDataID) {
window.toastr['success'](this.$tc('customers.updated_message'))
} else {
window.toastr['success'](this.$tc('customers.created_message'))
}
this.isLoading = false
if (this.$route.name === 'invoices.create') {
if (this.$route.name === 'invoices.create' || this.$route.name === 'invoices.edit') {
this.setInvoiceCustomer(response.data.customer.id)
}
if (this.$route.name === 'estimates.create') {
if (this.$route.name === 'estimates.create' || this.$route.name === 'estimates.edit') {
this.setEstimateCustomer(response.data.customer.id)
}
this.resetData()

View File

@ -45,14 +45,34 @@
<div class="col-sm-7">
<base-select
v-model="formData.unit"
:options="units"
:options="itemUnits"
:searchable="true"
:show-labels="false"
label="name"
>
<div slot="afterList">
<button type="button" class="list-add-button" @click="addItemUnit">
<font-awesome-icon class="icon" icon="cart-plus" />
<label>{{ $t('settings.customization.items.add_item_unit') }}</label>
</button>
</div>
</base-select>
</div>
</div>
<div v-if="isTexPerItem" class="form-group row">
<label class="col-sm-4 col-form-label input-label">{{ $t('items.taxes') }}</label>
<div class="col-sm-7">
<base-select
v-model="formData.taxes"
:options="getTaxTypes"
:searchable="true"
:show-labels="false"
:allow-empty="true"
:multiple="true"
label="tax_name"
/>
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label input-label">{{ $t('items.description') }}</label>
<div class="col-sm-7">
@ -124,11 +144,13 @@ export default {
{ name: 'mg', value: 'mg' },
{ name: 'pc', value: 'pc' }
],
taxes: [],
formData: {
name: null,
price: null,
description: null,
unit: null
unit: null,
taxes: []
}
}
},
@ -161,12 +183,28 @@ export default {
this.formData.price = newValue * 100
}
},
// itemUnits () {
// return this.units
// },
...mapGetters('modal', [
'modalDataID'
'modalDataID',
'modalData'
]),
...mapGetters('item', [
'getItemById'
])
'getItemById',
'itemUnits'
]),
...mapGetters('taxType', [
'taxTypes'
]),
isTexPerItem () {
return this.modalData.taxPerItem === 'YES'
},
getTaxTypes () {
return this.taxTypes.map(tax => {
return {...tax, tax_name: tax.name + ' (' + tax.percent + '%)'}
})
}
},
watch: {
modalDataID () {
@ -179,12 +217,17 @@ export default {
this.isEdit = true
this.fetchEditData()
}
if (this.isEdit) {
this.loadEditData()
}
},
mounted () {
this.$refs.name.focus = true
},
methods: {
...mapActions('modal', [
'openModal',
'closeModal',
'resetModalData'
]),
@ -203,7 +246,6 @@ export default {
unit: null,
id: null
}
this.$v.$reset()
},
fetchEditData () {
@ -230,9 +272,20 @@ export default {
if (this.isEdit) {
response = await this.updateItem(this.formData)
} else {
response = await this.addItem(this.formData)
let data = {
...this.formData,
taxes: this.formData.taxes.map(tax => {
return {
tax_type_id: tax.id,
amount: ((this.formData.price * tax.percent) / 100),
percent: tax.percent,
name: tax.name,
collective_tax: 0
}
})
}
response = await this.addItem(data)
}
if (response.data) {
window.toastr['success'](this.$tc('items.created_message'))
this.setItem(response.data.item)
@ -245,6 +298,12 @@ export default {
}
window.toastr['error'](response.data.error)
},
async addItemUnit () {
this.openModal({
'title': 'Add Item Unit',
'componentName': 'ItemUnit'
})
},
closeItemModal () {
this.resetFormData()
this.closeModal()

View File

@ -0,0 +1,148 @@
<template>
<div class="item-unit-modal">
<form action="" @submit.prevent="submitItemUnit">
<div class="card-body">
<div class="form-group row">
<label class="col-sm-4 col-form-label input-label">{{ $t('settings.customization.items.unit_name') }} <span class="required"> *</span></label>
<div class="col-sm-7">
<base-input
ref="name"
:invalid="$v.formData.name.$error"
v-model="formData.name"
type="text"
@input="$v.formData.name.$touch()"
/>
<div v-if="$v.formData.name.$error">
<span v-if="!$v.formData.name.required" class="form-group__message text-danger">{{ $tc('validation.required') }}</span>
</div>
</div>
</div>
</div>
<div class="card-footer">
<base-button
:outline="true"
class="mr-3"
color="theme"
type="button"
@click="closePaymentModeModal"
>
{{ $t('general.cancel') }}
</base-button>
<base-button
:loading="isLoading"
color="theme"
icon="save"
type="submit"
>
{{ !isEdit ? $t('general.save') : $t('general.update') }}
</base-button>
</div>
</form>
</div>
</template>
<script>
import { mapActions, mapGetters } from 'vuex'
import { validationMixin } from 'vuelidate'
const { required, minLength } = require('vuelidate/lib/validators')
export default {
mixins: [validationMixin],
data () {
return {
isEdit: false,
isLoading: false,
formData: {
id: null,
name: null
}
}
},
computed: {
...mapGetters('modal', [
'modalDataID',
'modalData',
'modalActive'
])
},
validations: {
formData: {
name: {
required,
minLength: minLength(2)
}
}
},
async mounted () {
this.$refs.name.focus = true
if (this.modalDataID) {
this.isEdit = true
this.setData()
}
},
methods: {
...mapActions('modal', [
'closeModal',
'resetModalData'
]),
...mapActions('item', [
'addItemUnit',
'updateItemUnit',
'fatchItemUnit'
]),
resetFormData () {
this.formData = {
id: null,
name: null
}
this.$v.formData.$reset()
},
async submitItemUnit () {
this.$v.formData.$touch()
if (this.$v.$invalid) {
return true
}
this.isLoading = true
let response
if (this.isEdit) {
response = await this.updateItemUnit(this.formData)
if (response.data) {
window.toastr['success'](this.$t('settings.customization.items.item_unit_updated'))
this.closePaymentModeModal()
return true
}
window.toastr['error'](response.data.error)
} else {
try {
response = await this.addItemUnit(this.formData)
if (response.data) {
this.isLoading = false
window.toastr['success'](this.$t('settings.customization.items.item_unit_added'))
this.closePaymentModeModal()
return true
} window.toastr['error'](response.data.error)
} catch (err) {
if (err.response.data.errors.name) {
this.isLoading = true
window.toastr['error'](this.$t('validation.item_unit_already_taken'))
}
}
}
},
async setData () {
this.formData = {
id: this.modalData.id,
name: this.modalData.name
}
},
closePaymentModeModal () {
this.resetModalData()
this.resetFormData()
this.closeModal()
}
}
}
</script>

View File

@ -0,0 +1,166 @@
<template>
<div class="mail-test-modal">
<form action="" @submit.prevent="onTestMailSend">
<div class="card-body">
<div class="form-group row">
<label class="col-sm-4 col-form-label input-label">{{ $t('general.to') }} <span class="required"> *</span></label>
<div class="col-sm-7">
<base-input
ref="to"
:invalid="$v.formData.to.$error"
v-model="formData.to"
type="text"
@input="$v.formData.to.$touch()"
/>
<div v-if="$v.formData.to.$error">
<span v-if="!$v.formData.to.required" class="form-group__message text-danger">{{ $tc('validation.required') }}</span>
<span v-if="!$v.formData.to.email" class="form-group__message text-danger"> {{ $t('validation.email_incorrect') }} </span>
</div>
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label input-label">{{ $t('general.subject') }} <span class="required"> *</span></label>
<div class="col-sm-7">
<div class="base-input">
<base-input
:invalid="$v.formData.subject.$error"
v-model="formData.subject"
type="text"
@input="$v.formData.subject.$touch()"
/>
</div>
<div v-if="$v.formData.subject.$error">
<span v-if="!$v.formData.subject.required" class="text-danger">{{ $t('validation.required') }}</span>
<span v-if="!$v.formData.subject.maxLength" class="text-danger">{{ $t('validation.subject_maxlength') }}</span>
</div>
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label input-label">{{ $t('general.message') }}<span class="required"> *</span></label>
<div class="col-sm-7">
<base-text-area
v-model="formData.message"
:invalid="$v.formData.message.$error"
rows="4"
cols="50"
@input="$v.formData.message.$touch()"
/>
<div v-if="$v.formData.message.$error">
<span v-if="!$v.formData.message.required" class="text-danger">{{ $t('validation.required') }}</span>
<span v-if="!$v.formData.message.maxLength" class="text-danger">{{ $t('validation.message_maxlength') }}</span>
</div>
</div>
</div>
</div>
<div class="card-footer">
<base-button
:outline="true"
class="mr-3"
color="theme"
type="button"
@click="closeTaxModal"
>
{{ $t('general.cancel') }}
</base-button>
<base-button
:loading="isLoading"
color="theme"
icon="save"
type="submit"
>
{{ !isEdit ? $t('general.save') : $t('general.update') }}
</base-button>
</div>
</form>
</div>
</template>
<script>
import { mapActions, mapGetters } from 'vuex'
import { validationMixin } from 'vuelidate'
const { required, minLength, email, maxLength } = require('vuelidate/lib/validators')
export default {
mixins: [validationMixin],
data () {
return {
isEdit: false,
isLoading: false,
formData: {
to: null,
subject: null,
message: null
}
}
},
computed: {
...mapGetters('modal', [
'modalDataID',
'modalData',
'modalActive'
])
},
validations: {
formData: {
to: {
required,
email
},
subject: {
required,
maxLength: maxLength(100)
},
message: {
required,
maxLength: maxLength(255)
}
}
},
async mounted () {
this.$refs.to.focus = true
},
methods: {
...mapActions('modal', [
'closeModal',
'resetModalData'
]),
resetFormData () {
this.formData = {
to: null,
subject: null,
message: null
}
this.$v.formData.$reset()
},
async onTestMailSend () {
this.$v.formData.$touch()
if (this.$v.$invalid) {
return true
}
this.isLoading = true
let response = await axios.post('/api/settings/test/mail', this.formData)
if (response.data) {
if (response.data.success) {
window.toastr['success'](this.$tc('general.send_mail_successfully'))
this.closeTaxModal()
this.isLoading = false
return true
}
window.toastr['error'](this.$tc('validation.something_went_wrong'))
this.closeTaxModal()
this.isLoading = false
return true
}
window.toastr['error'](response.data.error)
},
closeTaxModal () {
this.resetModalData()
this.resetFormData()
this.closeModal()
}
}
}
</script>

View File

@ -0,0 +1,143 @@
<template>
<div class="payment-modes-modal">
<form action="" @submit.prevent="submitPaymentMode">
<div class="card-body">
<div class="form-group row">
<label class="col-sm-4 col-form-label input-label">{{ $t('settings.customization.payments.mode_name') }} <span class="required"> *</span></label>
<div class="col-sm-7">
<base-input
ref="name"
:invalid="$v.formData.name.$error"
v-model="formData.name"
type="text"
@input="$v.formData.name.$touch()"
/>
<div v-if="$v.formData.name.$error">
<span v-if="!$v.formData.name.required" class="form-group__message text-danger">{{ $tc('validation.required') }}</span>
</div>
</div>
</div>
</div>
<div class="card-footer">
<base-button
:outline="true"
class="mr-3"
color="theme"
type="button"
@click="closePaymentModeModal"
>
{{ $t('general.cancel') }}
</base-button>
<base-button
:loading="isLoading"
color="theme"
icon="save"
type="submit"
>
{{ !isEdit ? $t('general.save') : $t('general.update') }}
</base-button>
</div>
</form>
</div>
</template>
<script>
import { mapActions, mapGetters } from 'vuex'
import { validationMixin } from 'vuelidate'
const { required, minLength } = require('vuelidate/lib/validators')
export default {
mixins: [validationMixin],
data () {
return {
isEdit: false,
isLoading: false,
formData: {
id: null,
name: null
}
}
},
computed: {
...mapGetters('modal', [
'modalDataID',
'modalData',
'modalActive'
])
},
validations: {
formData: {
name: {
required,
minLength: minLength(2)
}
}
},
async mounted () {
this.$refs.name.focus = true
if (this.modalDataID) {
this.isEdit = true
this.setData()
}
},
methods: {
...mapActions('modal', [
'closeModal',
'resetModalData'
]),
...mapActions('payment', [
'addPaymentMode',
'updatePaymentMode'
]),
resetFormData () {
this.formData = {
id: null,
name: null
}
this.$v.formData.$reset()
},
async submitPaymentMode () {
this.$v.formData.$touch()
if (this.$v.$invalid) {
return true
}
this.isLoading = true
let response
if (this.isEdit) {
response = await this.updatePaymentMode(this.formData)
if (response.data) {
window.toastr['success'](this.$t('settings.customization.payments.payment_mode_updated'))
this.closePaymentModeModal()
return true
} window.toastr['error'](response.data.error)
} else {
try {
response = await this.addPaymentMode(this.formData)
if (response.data) {
this.isLoading = false
window.toastr['success'](this.$t('settings.customization.payments.payment_mode_added'))
this.closePaymentModeModal()
return true
} window.toastr['error'](response.data.error)
} catch (err) {
if (err.response.data.errors.name) {
this.isLoading = true
window.toastr['error'](this.$t('validation.payment_mode_already_taken'))
}
}
}
},
async setData () {
this.formData = {
id: this.modalData.id,
name: this.modalData.name
}
},
closePaymentModeModal () {
this.resetModalData()
this.resetFormData()
this.closeModal()
}
}
}
</script>

View File

@ -124,7 +124,7 @@ export default {
},
percent: {
required,
between: between(0.10, 100)
between: between(0, 100)
},
description: {
maxLength: maxLength(255)

View File

@ -1,69 +0,0 @@
<template>
<div class="graph-container">
<canvas
id="graph"
ref="graph"
/>
</div>
</template>
<script>
import Chart from 'chart.js'
export default {
props: {
labels: {
type: Array,
require: true,
default: Array
},
values: {
type: Array,
require: true,
default: Array
}
},
mounted () {
let context = this.$refs.graph.getContext('2d')
let options = {
responsive: true,
maintainAspectRatio: false,
legend: {
display: false
}
}
let data = {
labels: this.labels,
datasets: [
{
label: 'My First dataset',
backgroundColor: 'rgba(79, 196, 127,0.2)',
borderColor: 'rgba(79, 196, 127,1)',
borderWidth: 1,
hoverBackgroundColor: 'rgba(79, 196, 127,0.4)',
hoverBorderColor: 'rgba(79, 196, 127,1)',
data: this.values
}
]
}
this.myBarChart = new Chart(context, {
type: 'bar',
data: data,
options: options
})
},
beforeDestroy () {
this.myBarChart.destroy()
}
}
</script>
<style scoped>
.graph-container {
height: 300px;
}
</style>

View File

@ -1,71 +0,0 @@
<template>
<div class="graph-container">
<canvas
id="graph"
ref="graph"/>
</div>
</template>
<script>
import Chart from 'chart.js'
export default {
props: {
labels: {
type: Array,
require: true,
default: Array
},
values: {
type: Array,
require: true,
default: Array
},
bgColors: {
type: Array,
require: true,
default: Array
},
hoverBgColors: {
type: Array,
require: true,
default: Array
}
},
mounted () {
let context = this.$refs.graph.getContext('2d')
let options = {
responsive: true,
maintainAspectRatio: false
}
let data = {
labels: this.labels,
datasets: [
{
data: this.values,
backgroundColor: this.bgColors,
hoverBackgroundColor: this.hoverBgColors
}
]
}
this.myDoughnutChart = new Chart(context, {
type: 'doughnut',
data: data,
options: options
})
},
beforeDestroy () {
this.myDoughnutChart.destroy()
}
}
</script>
<style scoped>
.graph-container {
height: 300px;
}
</style>

View File

@ -1,8 +1,6 @@
<template>
<div class="graph-container">
<canvas
id="graph"
ref="graph" />
<canvas id="graph" ref="graph" />
</div>
</template>
@ -16,58 +14,56 @@ export default {
labels: {
type: Array,
require: true,
default: Array
default: Array,
},
values: {
type: Array,
require: true,
default: Array
default: Array,
},
invoices: {
type: Array,
require: true,
default: Array
default: Array,
},
expenses: {
type: Array,
require: true,
default: Array
default: Array,
},
receipts: {
type: Array,
require: true,
default: Array
default: Array,
},
income: {
type: Array,
require: true,
default: Array
default: Array,
},
formatMoney: {
type: Function,
require: false,
default: Function
default: Function,
},
FormatGraphMoney: {
type: Function,
require: false,
default: Function
}
default: Function,
},
},
computed: {
...mapGetters('currency', [
'defaultCurrency'
])
...mapGetters('currency', ['defaultCurrency']),
},
watch: {
labels (val) {
labels(val) {
this.update()
}
},
},
mounted () {
mounted() {
let self = this
let context = this.$refs.graph.getContext('2d')
let options = {
@ -77,13 +73,16 @@ export default {
enabled: true,
callbacks: {
label: function (tooltipItem, data) {
return self.FormatGraphMoney(tooltipItem.value, self.defaultCurrency)
}
}
return self.FormatGraphMoney(
tooltipItem.value * 100,
self.defaultCurrency
)
},
},
},
legend: {
display: false
}
display: false,
},
}
let data = {
labels: this.labels,
@ -107,7 +106,7 @@ export default {
pointHoverBorderWidth: 2,
pointRadius: 4,
pointHitRadius: 10,
data: this.invoices
data: this.invoices.map((invoice) => invoice / 100),
},
{
label: 'Receipts',
@ -128,7 +127,7 @@ export default {
pointHoverBorderWidth: 2,
pointRadius: 4,
pointHitRadius: 10,
data: this.receipts
data: this.receipts.map((receipt) => receipt / 100),
},
{
label: 'Expenses',
@ -149,7 +148,7 @@ export default {
pointHoverBorderWidth: 2,
pointRadius: 4,
pointHitRadius: 10,
data: this.expenses
data: this.expenses.map((expense) => expense / 100),
},
{
label: 'Net Income',
@ -170,34 +169,40 @@ export default {
pointHoverBorderWidth: 2,
pointRadius: 4,
pointHitRadius: 10,
data: this.income
}
]
data: this.income.map((_i) => _i / 100),
},
],
}
this.myLineChart = new Chart(context, {
type: 'line',
data: data,
options: options
options: options,
})
},
methods: {
update () {
update() {
this.myLineChart.data.labels = this.labels
this.myLineChart.data.datasets[0].data = this.invoices
this.myLineChart.data.datasets[1].data = this.receipts
this.myLineChart.data.datasets[2].data = this.expenses
this.myLineChart.data.datasets[3].data = this.income
this.myLineChart.data.datasets[0].data = this.invoices.map(
(invoice) => invoice / 100
)
this.myLineChart.data.datasets[1].data = this.receipts.map(
(receipt) => receipt / 100
)
this.myLineChart.data.datasets[2].data = this.expenses.map(
(expense) => expense / 100
)
this.myLineChart.data.datasets[3].data = this.income.map((_i) => _i / 100)
this.myLineChart.update({
lazy: true
lazy: true,
})
},
beforeDestroy () {
beforeDestroy() {
this.myLineChart.destroy()
}
}
},
},
}
</script>

View File

@ -1,72 +0,0 @@
<template>
<div class="graph-container">
<canvas
id="graph"
ref="graph" />
</div>
</template>
<script>
import Chart from 'chart.js'
export default {
props: {
labels: {
type: Array,
require: true,
default: Array
},
values: {
type: Array,
require: true,
default: Array
},
bgColors: {
type: Array,
require: true,
default: Array
},
hoverBgColors: {
type: Array,
require: true,
default: Array
}
},
mounted () {
let context = this.$refs.graph.getContext('2d')
let options = {
responsive: true,
maintainAspectRatio: false
}
let data = {
labels: this.labels,
datasets: [
{
data: this.values,
backgroundColor: this.bgColors,
hoverBackgroundColor: this.hoverBgColors
}
]
}
this.pieChart = new Chart(context, {
type: 'pie',
data: data,
options: options
})
},
beforeDestroy () {
this.pieChart.destroy()
}
}
</script>
<style scoped>
.graph-container {
height: 300px;
}
</style>

View File

@ -1,95 +0,0 @@
<template>
<div class="graph-container easy-pie-chart">
<svg width="100%" height="100%" viewBox="0 0 34 34" class="donut">
<circle :stroke-width="strokeWidth" class="donut-segment" cx="17" cy="17" r="15.91549430918954" fill="transparent" :stroke="strokeColor" stroke-dasharray="100 0" />
<circle :stroke-width="strokeWidth" :stroke="color" :stroke-dasharray="successProgress" class="donut-segment" cx="17" cy="17" r="15.91549430918954" fill="transparent" />
<!-- <g class="chart-text">
<text :style="'fill:' + color" x="48%" y="50%" class="chart-number" >
{{ progress }}
</text>
<text :style="'fill:' + color" x="73%" y="50%" class="chart-label" >
%
</text>
</g> -->
</svg>
</div>
</template>
<script>
export default {
props: {
values: {
type: Number,
require: true,
default: 100
},
strokeWidth: {
type: Number,
require: false,
default: 1.2
},
strokeColor: {
type: String,
require: true,
default: '#eeeeee'
},
color: {
type: String,
require: true,
default: '#007dcc'
}
},
data () {
return {
progress: 0
}
},
watch: {
values (newvalue, oldvalue) {
if (newvalue !== oldvalue) {
this.setProgress()
}
}
},
computed: {
successProgress () {
return this.progress + ' ' + (100 - this.progress)
},
remainProgress () {
return 100 - this.progress + ' ' + this.progress
},
},
mounted () {
this.setProgress()
},
methods: {
setProgress () {
let self = this
for (let i = 0; i < this.values; i++) {
setTimeout(function () {
++self.progress
}, 15 * i)
}
}
}
}
</script>
<style scoped>
.chart-text {
font: 6px "Montserrat", Arial, sans-serif;
fill: #000;
-moz-transform: translateY(0.25em);
-ms-transform: translateY(0.25em);
-webkit-transform: translateY(0.25em);
transform: translateY(0.5em);
}
.chart-number {
font-size: 8px;
line-height: 1;
text-anchor: middle;
}
.chart-label {
font-size: 5px;
text-transform: uppercase;
text-anchor: middle;
}
</style>

View File

@ -1,16 +1,16 @@
export default {
toggleSidebar () {
toggleSidebar() {
let icon = document.getElementsByClassName('hamburger')[0]
document.body.classList.toggle('sidebar-open')
icon.classList.toggle('is-active')
},
addClass (el, className) {
addClass(el, className) {
if (el.classList) el.classList.add(className)
else el.className += ' ' + className
},
hasClass (el, className) {
hasClass(el, className) {
const hasClass = el.classList
? el.classList.contains(className)
: new RegExp('(^| )' + className + '( |$)', 'gi').test(el.className)
@ -18,33 +18,38 @@ export default {
return hasClass
},
reset (prefix) {
reset(prefix) {
let regx = new RegExp('\\b' + prefix + '(.*)?\\b', 'g')
document.body.className = document.body.className.replace(regx, '')
},
setLayout (layoutName) {
setLayout(layoutName) {
this.reset('layout-')
document.body.classList.add('layout-' + layoutName)
},
setSkin (skinName) {
setSkin(skinName) {
this.reset('skin-')
document.body.classList.add('skin-' + skinName)
},
setLogo (logoSrc) {
setLogo(logoSrc) {
document.getElementById('logo-desk').src = logoSrc
},
formatMoney (amount, currency = 0) {
formatMoney(amount, currency = 0) {
if (!currency) {
currency = {precision: 2, thousand_separator: ',', decimal_separator: '.', symbol: '$'}
currency = {
precision: 2,
thousand_separator: ',',
decimal_separator: '.',
symbol: '$',
}
}
amount = amount / 100
let {precision, decimal_separator, thousand_separator, symbol} = currency
let { precision, decimal_separator, thousand_separator, symbol } = currency
try {
precision = Math.abs(precision)
@ -52,25 +57,44 @@ export default {
const negativeSign = amount < 0 ? '-' : ''
let i = parseInt(amount = Math.abs(Number(amount) || 0).toFixed(precision)).toString()
let j = (i.length > 3) ? i.length % 3 : 0
let i = parseInt(
(amount = Math.abs(Number(amount) || 0).toFixed(precision))
).toString()
let j = i.length > 3 ? i.length % 3 : 0
let moneySymbol = `<span style="font-family: sans-serif">${symbol}</span>`
return moneySymbol + ' ' + negativeSign + (j ? i.substr(0, j) + thousand_separator : '') + i.substr(j).replace(/(\d{3})(?=\d)/g, '$1' + thousand_separator) + (precision ? decimal_separator + Math.abs(amount - i).toFixed(precision).slice(2) : '')
return (
moneySymbol +
' ' +
negativeSign +
(j ? i.substr(0, j) + thousand_separator : '') +
i.substr(j).replace(/(\d{3})(?=\d)/g, '$1' + thousand_separator) +
(precision
? decimal_separator +
Math.abs(amount - i)
.toFixed(precision)
.slice(2)
: '')
)
} catch (e) {
console.log(e)
}
},
formatGraphMoney (amount, currency = 0) {
formatGraphMoney(amount, currency = 0) {
if (!currency) {
currency = {precision: 2, thousand_separator: ',', decimal_separator: '.', symbol: '$'}
currency = {
precision: 2,
thousand_separator: ',',
decimal_separator: '.',
symbol: '$',
}
}
amount = amount / 100
let {precision, decimal_separator, thousand_separator, symbol} = currency
let { precision, decimal_separator, thousand_separator, symbol } = currency
try {
precision = Math.abs(precision)
@ -78,25 +102,76 @@ export default {
const negativeSign = amount < 0 ? '-' : ''
let i = parseInt(amount = Math.abs(Number(amount) || 0).toFixed(precision)).toString()
let j = (i.length > 3) ? i.length % 3 : 0
let i = parseInt(
(amount = Math.abs(Number(amount) || 0).toFixed(precision))
).toString()
let j = i.length > 3 ? i.length % 3 : 0
let moneySymbol = `${symbol}`
return moneySymbol + ' ' + negativeSign + (j ? i.substr(0, j) + thousand_separator : '') + i.substr(j).replace(/(\d{3})(?=\d)/g, '$1' + thousand_separator) + (precision ? decimal_separator + Math.abs(amount - i).toFixed(precision).slice(2) : '')
return (
moneySymbol +
' ' +
negativeSign +
(j ? i.substr(0, j) + thousand_separator : '') +
i.substr(j).replace(/(\d{3})(?=\d)/g, '$1' + thousand_separator) +
(precision
? decimal_separator +
Math.abs(amount - i)
.toFixed(precision)
.slice(2)
: '')
)
} catch (e) {
console.log(e)
}
},
checkValidUrl (url) {
let pattern = new RegExp('^(https?:\\/\\/)?' + // protocol
checkValidUrl(url) {
let pattern = new RegExp(
'^(https?:\\/\\/)?' + // protocol
'((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // domain name
'((\\d{1,3}\\.){3}\\d{1,3}))' + // OR ip (v4) address
'(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*' + // port and path
'(\\?[;&a-z\\d%_.~+=-]*)?' + // query string
'(\\#[-a-z\\d_]*)?$', 'i') // fragment locator
'(\\#[-a-z\\d_]*)?$',
'i'
) // fragment locator
return !!pattern.test(url)
}
},
fallbackCopyTextToClipboard(text) {
var textArea = document.createElement('textarea')
textArea.value = text
// Avoid scrolling to bottom
textArea.style.top = '0'
textArea.style.left = '0'
textArea.style.position = 'fixed'
document.body.appendChild(textArea)
textArea.focus()
textArea.select()
try {
var successful = document.execCommand('copy')
var msg = successful ? 'successful' : 'unsuccessful'
console.log('Fallback: Copying text command was ' + msg)
} catch (err) {
console.error('Fallback: Oops, unable to copy', err)
}
document.body.removeChild(textArea)
},
copyTextToClipboard(text) {
if (!navigator.clipboard) {
this.fallbackCopyTextToClipboard(text)
return
}
navigator.clipboard.writeText(text).then(
function () {
return true
},
function (err) {
return false
}
)
},
}

Some files were not shown because too many files have changed in this diff Show More