├── .babelrc ├── .flowconfig ├── .gitignore ├── 404.html ├── LICENSE ├── README.md ├── app ├── actions │ └── index.js ├── components │ ├── App.js │ ├── common │ │ └── NavBar.js │ ├── dashboard │ │ ├── Dashboard.js │ │ ├── Invoice.js │ │ ├── ItemRow.js │ │ ├── Preview.js │ │ ├── SideNav.js │ │ ├── data.json │ │ └── item.js │ └── home │ │ └── HomePage.js ├── constants │ └── index.js ├── index.js ├── reducers │ ├── addInfoReducer.js │ ├── currencyReducer.js │ ├── dateFormatReducer.js │ ├── downloadStatusReducer.js │ ├── dueDateReducer.js │ ├── index.js │ ├── invoiceDetailsReducer.js │ ├── issueDateReducer.js │ ├── itemOrderReducer.js │ ├── itemsReducer.js │ ├── paidStatusReducer.js │ ├── statusReducer.js │ └── widthReducer.js └── store │ └── configureStore.js ├── assets ├── css │ ├── styles.css │ └── styles.css.map ├── images │ ├── banner.jpg │ ├── favicon.ico │ ├── icon-full.png │ ├── icon.png │ └── logo.png └── styles │ ├── partials │ ├── base │ │ ├── _base.scss │ │ └── _helper.scss │ ├── components │ │ ├── _buttons.scss │ │ ├── _dashboard.scss │ │ ├── _home.scss │ │ ├── _invoice.scss │ │ ├── _item.scss │ │ ├── _navbar.scss │ │ ├── _pdf-preview.scss │ │ ├── _react-select.scss │ │ ├── _react-toggle.scss │ │ └── _sidenav.scss │ ├── generic │ │ └── _bootstrap.scss │ ├── settings │ │ └── _global.scss │ └── tools │ │ └── _mixins.scss │ └── styles.scss ├── dist └── index.bundle.js ├── index.html ├── manifest.json ├── package.json ├── screenshots ├── Dashboard.png ├── Preview Screen.png └── home.png ├── server.js ├── service-worker.js ├── webpack.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react", "stage-1", "flow"] 3 | } -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | 3 | [include] 4 | 5 | [libs] 6 | 7 | [options] 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Dependency directories 15 | node_modules/ 16 | 17 | # Optional npm cache directory 18 | .npm 19 | 20 | # Optional eslint cache 21 | .eslintcache 22 | 23 | # Yarn Integrity file 24 | .yarn-integrity 25 | 26 | # SASS cache 27 | .sass-cache/ 28 | 29 | .DS_Store 30 | -------------------------------------------------------------------------------- /404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | QuickBill 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 67 | 68 | 69 |
70 | 71 | 72 | 73 | 74 | 92 | 93 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Punit Grover 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # QuickBill 2 | Create unlimited invoices for free and generate pdfs - Handy for freelancers and businesses. 3 | 4 | [Click here](https://punitgr.github.io/QuickBill/) to create an invoice. 5 | 6 | ## Features: 7 | 8 | * **No Signup required and FREE.** 9 | 10 | ![Home](https://github.com/PunitGr/QuickBill/blob/master/screenshots/home.png) 11 | 12 | * **Easy to use dashboard.** 13 | 14 | ![Dashboard](https://github.com/PunitGr/QuickBill/blob/master/screenshots/Dashboard.png) 15 | 16 | * **Generate pdfs at go and preview them.** 17 | 18 | ![Preview](https://github.com/PunitGr/QuickBill/blob/master/screenshots/Preview%20Screen.png) 19 | 20 | * **Mobile Friendly.** 21 | 22 | * **Offline Support.** 23 | 24 | * **Add it to your device homescreen.** 25 | 26 | ## License (MIT) 27 | 28 | Copyright (c) 2017 Punit Grover 29 | 30 | Permission is hereby granted, free of charge, to any person obtaining a copy 31 | of this software and associated documentation files (the "Software"), to deal 32 | in the Software without restriction, including without limitation the rights 33 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 34 | copies of the Software, and to permit persons to whom the Software is 35 | furnished to do so, subject to the following conditions: 36 |
37 | The above copyright notice and this permission notice shall be included in all 38 | copies or substantial portions of the Software. 39 |
40 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 41 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 42 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 43 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 44 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 45 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 46 | SOFTWARE. 47 | -------------------------------------------------------------------------------- /app/actions/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { 3 | ADD_ITEM, 4 | SORT_ITEMS, 5 | SET_ITEM, 6 | REMOVE_ITEM, 7 | SET_CURRENCY, 8 | SET_DATE_FORMAT, 9 | SET_PAID_STATUS, 10 | SET_ADDINFO, 11 | SET_INVOICE_DETAILS, 12 | SET_STATUS, 13 | SET_ISSUE_DATE, 14 | SET_DUE_DATE, 15 | SET_WIDTH, 16 | SET_DOWNLOAD_STATUS 17 | } from "../constants"; 18 | 19 | export type Action = { 20 | type: string, 21 | id?: number, 22 | value?: Object, 23 | order?: Array, 24 | discount?: string, 25 | tax?: string, 26 | amountPaid?: string, 27 | paidStatus?: boolean, 28 | currency?: Object, 29 | dateFormat?: Object, 30 | invoiceDetails?: Object, 31 | status?: Object, 32 | issueDate?: Date, 33 | width?: number, 34 | downloadStatus?: boolean, 35 | dueDate?: Date, 36 | name?: string, 37 | val?:string 38 | }; 39 | 40 | export function addItem(id: number, value: Object): Action { 41 | return { 42 | type: ADD_ITEM, 43 | id, 44 | value 45 | } 46 | } 47 | 48 | export function setItemsOrder(order: Array): Action { 49 | return { 50 | type: SORT_ITEMS, 51 | order 52 | } 53 | } 54 | 55 | export function setItem(id: number, value: Object): Action { 56 | return { 57 | type: SET_ITEM, 58 | id, 59 | value 60 | } 61 | } 62 | 63 | export function removeItem(id: number): Action { 64 | return { 65 | type: REMOVE_ITEM, 66 | id 67 | } 68 | } 69 | 70 | export function setAddInfo(discount: string, tax: string, amountPaid: string, vat: string): Action { 71 | return { 72 | type: SET_ADDINFO, 73 | discount, 74 | tax, 75 | amountPaid, 76 | vat 77 | } 78 | } 79 | 80 | export function setCurrency(currency: Object): Action { 81 | return { 82 | type: SET_CURRENCY, 83 | currency 84 | } 85 | } 86 | 87 | export function setDateFormat(dateFormat: Object): Action { 88 | return { 89 | type: SET_DATE_FORMAT, 90 | dateFormat 91 | } 92 | } 93 | 94 | export function setStatus(status: Object): Action { 95 | return { 96 | type: SET_STATUS, 97 | status 98 | } 99 | } 100 | 101 | export function setPaidStatus(paidStatus: boolean): Action { 102 | return { 103 | type: SET_PAID_STATUS, 104 | paidStatus 105 | } 106 | } 107 | 108 | export function setDownloadStatus(downloadStatus: boolean): Action { 109 | return { 110 | type: SET_DOWNLOAD_STATUS, 111 | downloadStatus 112 | } 113 | } 114 | 115 | export function setInvoiceDetails(name: string, val: string): Action { 116 | return { 117 | type: SET_INVOICE_DETAILS, 118 | name, 119 | val 120 | } 121 | } 122 | 123 | export function setIssueDate(issueDate: Date): Action { 124 | return { 125 | type: SET_ISSUE_DATE, 126 | issueDate 127 | } 128 | } 129 | 130 | export function setDueDate(dueDate: Date): Action { 131 | return { 132 | type: SET_DUE_DATE, 133 | dueDate 134 | } 135 | } 136 | 137 | export function setWidth(width: number): Action { 138 | return { 139 | type: SET_WIDTH, 140 | width 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /app/components/App.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { Component } from "react"; 3 | import { BrowserRouter as Router, Route} from "react-router-dom"; 4 | 5 | import NavBar from "./common/NavBar"; 6 | import HomePage from "./home/HomePage"; 7 | import Dashboard from "./dashboard/Dashboard"; 8 | import Preview from "./dashboard/Preview"; 9 | 10 | export default class App extends Component { 11 | render() { 12 | return ( 13 | 14 |
15 | 16 | 17 | 18 | 19 |
20 |
21 | ); 22 | } 23 | } -------------------------------------------------------------------------------- /app/components/common/NavBar.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { Link } from "react-router-dom"; 3 | 4 | type Props = {}; 5 | 6 | export default class NavBar extends Component { 7 | 8 | constructor(props: Props) { 9 | super(props); 10 | this.state = { 11 | pageOffset: window.pageYOffset 12 | } 13 | } 14 | componentDidMount() { 15 | window.addEventListener('scroll',() => { 16 | this.setState({ 17 | pageOffset: window.pageYOffset 18 | }); 19 | }); 20 | } 21 | 22 | render() { 23 | const style = { 24 | svg: { 25 | fill: "#555", 26 | color: "#fff", 27 | top: "0", 28 | border: "0", 29 | right: "0" 30 | }, 31 | octoArm: { 32 | transformOrigin: "130px 106px" 33 | }, 34 | githubCorner: { 35 | position: "fixed", 36 | right: "0", 37 | top: "0" 38 | }, 39 | logo: { 40 | color: "#555" 41 | } 42 | } 43 | return( 44 | 74 | ); 75 | } 76 | } -------------------------------------------------------------------------------- /app/components/dashboard/Dashboard.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { Component } from "react"; 3 | import SideNav from "./SideNav"; 4 | import Invoice from "./Invoice"; 5 | import { Link } from "react-router-dom"; 6 | import { connect } from "react-redux"; 7 | 8 | import { 9 | setDownloadStatus 10 | } from "../../actions"; 11 | 12 | type Props = { 13 | setDownloadStatus: Function, 14 | downloadStatus: ?boolean 15 | }; 16 | 17 | class Dashboard extends Component { 18 | 19 | componentDidMount() { 20 | const app = document.querySelector("#app"); 21 | if (app) { 22 | app.className = "fix-navbar"; 23 | } 24 | } 25 | 26 | componentWillUnmount() { 27 | const app = document.querySelector("#app"); 28 | if (app) { 29 | app.className = ""; 30 | } 31 | } 32 | 33 | render() { 34 | return ( 35 |
36 | 37 | 38 |
39 |
40 | Preview 41 | {this.props.setDownloadStatus(!this.props.downloadStatus)}} 45 | > 46 | Download 47 | 48 |
49 |
50 |
51 | ); 52 | } 53 | } 54 | 55 | function mapStateToProps(state, ownProps) { 56 | return { 57 | downloadStatus: state.downloadStatus 58 | } 59 | } 60 | 61 | function mapDispatchToProps(dispatch) { 62 | return { 63 | setDownloadStatus: (downloadStatus) => dispatch(setDownloadStatus(downloadStatus)) 64 | } 65 | } 66 | 67 | export default connect(mapStateToProps, mapDispatchToProps)(Dashboard); -------------------------------------------------------------------------------- /app/components/dashboard/Invoice.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { Component } from "react"; 3 | import { SingleDatePicker } from "react-dates"; 4 | import Select from "react-select"; 5 | import { connect } from "react-redux"; 6 | 7 | import SideNav from "./SideNav"; 8 | import Item from "./item"; 9 | import { 10 | setInvoiceDetails, 11 | setStatus, 12 | setIssueDate, 13 | setDueDate 14 | } from "../../actions"; 15 | 16 | type Props = { 17 | currency: Object, 18 | items: Object, 19 | addInfo: { 20 | discount: ?number, 21 | tax: ?number, 22 | amountPaid: ?number, 23 | vat: ?number 24 | }, 25 | invoiceDetails: { 26 | to: string, 27 | from: string, 28 | addressTo: string, 29 | addressFrom: string, 30 | phoneTo: string, 31 | phoneFrom: string, 32 | emailTo: string, 33 | emailFrom: string, 34 | invoiceNumber: string, 35 | job: string, 36 | invoiceType: string 37 | }, 38 | status: {value: ?string, label: ?string}, 39 | paidStatus: ?boolean, 40 | issueDate: ?Date, 41 | dueDate: ?Date, 42 | setInvoiceDetails: Function, 43 | setStatus: Function, 44 | setIssueDate: Function, 45 | setDueDate: Function, 46 | }; 47 | 48 | type State = { 49 | issueFocused: boolean, 50 | dueFocused: boolean, 51 | }; 52 | 53 | const options = [ 54 | { value: "paid", label: "Paid" }, 55 | { value: "due", label: "Due" }, 56 | { value: "overdue", label: "Overdue" }, 57 | { value: "onhold", label: "On Hold" }, 58 | ] 59 | 60 | class Invoice extends Component { 61 | state: State; 62 | 63 | constructor(props: Props) { 64 | super(props); 65 | this.state = { 66 | invoiceNumber: "001", 67 | job: "", 68 | issueFocused: false, 69 | dueFocused: false 70 | } 71 | } 72 | 73 | handleChange = (e: Event) => { 74 | if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) { 75 | const { name, value } = e.target; 76 | this.props.setInvoiceDetails(name, value); 77 | } 78 | } 79 | 80 | selectChange = (val: {value: ?string, label: ?string}) => { 81 | if (val) { 82 | this.props.setStatus(val); 83 | }else { 84 | this.props.setStatus({ value: "paid", label: "Paid"}); 85 | } 86 | } 87 | 88 | render() { 89 | const { items, addInfo, invoiceDetails, paidStatus } = this.props; 90 | let discountElement; 91 | let vatElement; 92 | let amount = 0; 93 | let subTotal = 0; 94 | let discount = 0; 95 | let vat = 0; 96 | let amountPaidElement; 97 | 98 | if (addInfo["discount"] && addInfo["discount"] > 0) { 99 | discountElement = ( 100 |
101 | Discount 102 |

{addInfo["discount"]} %

103 |
104 | ); 105 | } 106 | 107 | if(addInfo["vat"] && addInfo["vat"] > 0) { 108 | vatElement = ( 109 |
110 | VAT 111 |

{this.props.addInfo["vat"]} %

112 |
113 | ) 114 | } 115 | 116 | if (addInfo["amountPaid"] && addInfo["amountPaid"] > 0 && paidStatus) { 117 | amountPaidElement = ( 118 |
119 | Paid to Date 120 |

{this.props.currency["value"]} {addInfo["amountPaid"]}

121 |
122 | ); 123 | } 124 | 125 | for (let key in items) { 126 | if (items.hasOwnProperty(key)) { 127 | if (items[key] && parseFloat(items[key]["quantity"]) > 0 && parseFloat(items[key]["price"]) > 0) { 128 | subTotal = subTotal + (items[key]["quantity"] * items[key]["price"]); 129 | let tax = 0; 130 | if (addInfo["discount"] && addInfo["discount"] >= 0 ) { 131 | discount = (addInfo["discount"] / 100); 132 | } 133 | if (addInfo["tax"] && addInfo["tax"] >= 0) { 134 | tax = (addInfo["tax"] / 100); 135 | } 136 | if (addInfo["vat"] && addInfo["vat"] >= 0) { 137 | vat = (addInfo["vat"] / 100); 138 | } 139 | if (addInfo["amountPaid"] && addInfo["amountPaid"] > 0 && paidStatus) { 140 | amount = (subTotal - (subTotal * discount)) + (subTotal * tax) + (subTotal * vat) - parseFloat(addInfo["amountPaid"]); 141 | } 142 | else { 143 | amount = (subTotal - (subTotal * discount)) + (subTotal * tax) + (subTotal * vat); 144 | } 145 | } 146 | } 147 | } 148 | 149 | 150 | return ( 151 |
152 |
153 |
154 | 168 |
169 | 170 |
171 |
172 | 173 | this.props.setIssueDate(date)} 178 | onFocusChange={({focused}) => this.setState({ issueFocused: !this.state.issueFocused})} 179 | isOutsideRange={() => false} 180 | /> 181 |
182 | 183 |
184 | 185 | this.props.setDueDate(date)} 190 | onFocusChange={({focused}) => this.setState({ dueFocused: !this.state.dueFocused})} 191 | isOutsideRange={() => false} 192 | /> 193 |
194 | 195 |
196 | 197 | 204 |
205 | 206 |
207 | 208 | 215 |
216 |
217 | 218 |
219 |
220 | 221 | 229 |