├── .gitignore ├── .htaccess ├── LICENSE ├── README.md ├── bubblegum ├── app.html ├── css │ ├── bootstrap4-bubblegum.4.3.1.css │ ├── bootstrap4-bubblegum.4.3.1.min.css │ └── fontawesome-all.5.6.3.css ├── form.html ├── index.html ├── js │ ├── bootstrap.4.3.1.min.js │ ├── jquery-3.3.1.slim.min.js │ └── popper.1.14.7.min.js. └── webfonts │ ├── fa-brands-400.eot │ ├── fa-brands-400.svg │ ├── fa-brands-400.ttf │ ├── fa-brands-400.woff │ ├── fa-brands-400.woff2 │ ├── fa-regular-400.eot │ ├── fa-regular-400.svg │ ├── fa-regular-400.ttf │ ├── fa-regular-400.woff │ ├── fa-regular-400.woff2 │ ├── fa-solid-900.eot │ ├── fa-solid-900.svg │ ├── fa-solid-900.ttf │ ├── fa-solid-900.woff │ └── fa-solid-900.woff2 ├── changelog.txt ├── cron.php ├── db_mysql.sql ├── db_postgresql.sql ├── favicon ├── favicon.ico ├── manifest.json ├── tabby-16.png ├── tabby-180.png ├── tabby-192.png ├── tabby-32.png ├── tabby-512.png └── tabby-monochrome.svg ├── index.php ├── resources ├── activities.php ├── init.php ├── install.php ├── people.php ├── recurring.php ├── transactions.php ├── uiblocks.php └── users.php ├── screenshots ├── screenshot_1_landing.png ├── screenshot_2_install.png ├── screenshot_3_people.png ├── screenshot_4_activities.png └── screenshot_5_reminder.png ├── tabby-monochrome.svg ├── tabby.png ├── tabby.svg ├── templates ├── adminconfirm.php ├── box_config.php ├── button_merge.html ├── buttons.html ├── confirm_delete.php ├── emptynav.html ├── error.php ├── footer.php ├── form_activity.php ├── form_install.php ├── form_people.php ├── form_people_edit.php ├── form_profile.php ├── form_recurring.php ├── form_recurring_edit.php ├── form_remind.php ├── header.php ├── index.html ├── login.html ├── nav.html ├── register.html ├── success.php ├── table_merge.php ├── table_people.php ├── table_recurring.php └── tokennav.php └── upgrade.php /.gitignore: -------------------------------------------------------------------------------- 1 | .settings/ 2 | .settings/* 3 | .project 4 | .buildpath 5 | -------------------------------------------------------------------------------- /.htaccess: -------------------------------------------------------------------------------- 1 | 2 | RewriteEngine On 3 | RewriteCond %{REQUEST_FILENAME} !-f 4 | RewriteCond %{REQUEST_FILENAME} !-d 5 | RewriteRule ^ index.php [L] 6 | 7 | RewriteCond %{REQUEST_URI} changelog.txt$ 8 | RewriteRule ^ index.php [L] 9 | 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tabby - A friendly tool to manage debt 2 | 3 | Tabby is a tool I made for myself due to lack of practical and unannoying tools to manage debt and remind people about it. Since existing solutions require accounts for people to even see how much you owe, or have other annoying requirements or missing features, I created Tabby with the idea that it would serve all of my needs and be as enjoyable for my debtors as possible (I was mostly just tired of reminding people to pay back their meals). 4 | 5 | While initially developing Tabby as mostly a quick little script, I somehow managed to develop it in full. So since it works surprisingly well and has a bunch of features, I ended up FOSSing it. It's something I very much believe in ideologically, and I think it might be useful to others. From time to time, I still add new features to Tabby to make it more convenient for me or if a friend (or a user on GitHub) requests a specific feature that also makes sense to me. 6 | 7 | ## Table of Contents 8 | * [Features](#features) 9 | * [Screenshots](#screenshots) 10 | * [Landing page](#landing-page) 11 | * [Installation form](#installation-form) 12 | * [Overview of debt (or credit) of all your contacts](#overview-of-debt-or-credit-of-all-your-contacts) 13 | * [Overview of debt by activity](#overview-of-debt-by-activity) 14 | * [Reminder page](#reminder-page) 15 | * [Requirements](#requirements) 16 | * [Installation](#installation) 17 | * [Released version](#released-version) 18 | * [Git version](#git-version) 19 | * [Upgrading](#upgrading) 20 | * [Changelog](#changelog) 21 | * [License](#license) 22 | * [Acknowledgements](#acknowledgements) 23 | 24 | ## Features 25 | 26 | * An installation is a private instance owned by an admin 27 | * The admin can approve or deny account registrations 28 | * Accounts are only required to register debt, debtors don't require any form of account 29 | * While an interface is available for debtors, they can also get all the information required to repay someone through email 30 | * Track debt based on activities 31 | * Track credit separate from whether a specific debt was repaid or not (so people with open credit are just fine) 32 | * Reminds users to check their bank account and then ask Tabby to send reminders 33 | * In spirit of the GDPR, as little information is required as possible. 34 | * Adorable logo 35 | * ... and more (I'm probably forgetting to mention some neat stuff, so check out the screenshots below for sure!) 36 | 37 | ## Screenshots 38 | 39 | ### Landing page 40 | This page is displayed when a user is not logged in. It features some basic information about Tabby, as well as a login form, link to a registration form, and an easy way to request a token link to check your debt without an account. 41 | ![screenshot landing page](/screenshots/screenshot_1_landing.png?raw=true) 42 | 43 | ### Installation form 44 | Since version 1.1, Tabby has a simple installation form where you enter your database credentials, email preferences and application settings. The database tables as well as the configuration file are created automatically. 45 | ![screenshot installation form](/screenshots/screenshot_2_install.png?raw=true) 46 | 47 | ### Overview of debt (or credit) of all your contacts 48 | This is the page you see after logging in to Tabby. It gives an overview of what each of your contacts owes you and from what. Tabby displays which debts are (fully or partially) unpaid, as well as credits. A total is displayed at the bottom of each contact's box. You can easily enter wire transfers or cash you reveived through the small forms. Buttons are available for most actions you may need to perform. 49 | ![screenshot overview of debt by people](/screenshots/screenshot_3_people.png?raw=true) 50 | 51 | ### Overview of debt by activity 52 | This page gives an overview of each activity you've added to Tabby. For those who haven't fully repaid their debt for that specific activity, the number is marked with a colour. You can also easily add extra contacts for a specific activity or change the numbers if you've made a mistake. 53 | ![screenshot overview of debt by people](/screenshots/screenshot_4_activities.png?raw=true) 54 | 55 | ### Reminder page 56 | It's super easy to send a reminder with Tabby. You can pick whether you want to email everyone with debt or just a specific person. You can also add an optional message to make the reminder a bit more personal. 57 | ![screenshot reminder page](/screenshots/screenshot_5_reminder.png?raw=true) 58 | 59 | ## Requirements 60 | 61 | * PHP 7.2 or up, mostly works fine with PHP 5.5.9 except for email functionality 62 | * MySQL or PostgreSQL 63 | * Working mail setup on the webserver 64 | * Cron is advised but webcron fallback is available 65 | 66 | ## Installation 67 | 68 | ### Released version 69 | 70 | * Download the [latest release](https://github.com/bertvandepoel/tabby/releases/latest) from GitHub releases. 71 | * Unpack and upload the file to your server or hosting space. 72 | * Visit the corresponding URL, Tabby will automatically display the installation form. 73 | * Enter the database credentials (create them if you don't have them yet). 74 | * Enter email and application settings. 75 | * After confirming installation, the configuration will be written to a file. 76 | * If no write permissions are available, the contents of the configuration file are displayed. Create config.php locally with those contents and upload it to the correct folder. 77 | * When you're not using webcron, correctly install a cronjob using the displayed example as basis. 78 | * If not using Apache or if mod_rewrite and/or .htaccess aren't available, you may need to configure correct mapping to index.php and redirecting of the changelog. 79 | * You can now start using your Tabby installation. Log in with your account, then add people to register debt from activities for them. 80 | 81 | ### Git version 82 | 83 | Keep in mind that code may be committed to git that isn't ready for a full release. 84 | 85 | * Clone this repo to the right location or copy/transfer it there. 86 | * Visit the corresponding URL, Tabby will automatically display the installation form. 87 | * Enter the database credentials (create them if you don't have them yet). 88 | * Enter email and application settings. 89 | * After confirming installation, the configuration will be written to a file. 90 | * If no write permissions are available, the contents of the configuration file are displayed. Create config.php locally with those contents and upload it to the correct folder. 91 | * When you're not using webcron, correctly install a cronjob using the displayed example as basis. 92 | * If not using Apache or if mod_rewrite and/or .htaccess aren't available, you may need to configure correct mapping to index.php and redirecting of the changelog. 93 | * You can now start using your Tabby installation. Log in with your account, then add people to register debt from activities for them. 94 | 95 | ## Upgrading 96 | 97 | If you are using git, pull the latest version and then checkout the tag of the version you're upgrading to. If you are using releases, simply download the right files and overwrite your current directory (or move over config.php). When all the files are in place, visit upgrade.php or run it from the command line to perform database schema upgrade (if required). Follow any supplementary instructions upgrade.php displays. 98 | 99 | ## Changelog 100 | 101 | A simplified changelog is available in the [changelog.txt](changelog.txt) file. 102 | 103 | ## License 104 | 105 | This project is licensed under the AGPL license - see the [LICENSE](LICENSE) file for details. 106 | 107 | ## Acknowledgements 108 | 109 | Tabby uses the bubblegum bootstrap theme by hackerthemes.com, licensed under the MIT license and based on Bootstrap 4. This theme includes Font Awesome, which contains files under the CC BY 4.0, SIL OFL and MIT License. 110 | -------------------------------------------------------------------------------- /bubblegum/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Bubblegum - Bootstrap Theme 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 37 | 38 | 39 | 40 |
41 |

Sample application

42 | 43 |
44 |
45 |
46 |
47 | 48 |
49 | 50 |
51 |
52 |
53 | 54 | 65 |
66 |
67 | 68 |
69 |
70 |
71 |
72 |

Card title

73 |

74 | Some quick example text to build on the card title 75 | and make up the bulk of the card's content. 76 |

77 | 78 |
79 |
80 |
81 |
82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 100 | 108 | 109 | 110 | 111 | 112 | 117 | 125 | 126 | 127 | 128 | 129 | 134 | 142 | 143 | 144 | 145 | 146 | 151 | 159 | 160 | 161 | 162 | 163 | 168 | 176 | 177 | 178 | 179 | 180 | 185 | 193 | 194 | 195 | 196 | 197 | 202 | 210 | 211 | 212 | 213 | 214 | 219 | 227 | 228 | 229 | 230 | 231 | 236 | 244 | 245 | 246 | 247 |
#ItemActions
1 96 | 97 | Some item on your list 98 | 99 | 101 | 102 | 103 | Edit 104 | 105 | 106 | Delete 107 |
2 113 | 114 | Some item on your list 115 | 116 | 118 | 119 | 120 | Edit 121 | 122 | 123 | Delete 124 |
3 130 | 131 | Some item on your list 132 | 133 | 135 | 136 | 137 | Edit 138 | 139 | 140 | Delete 141 |
4 147 | 148 | Some item on your list 149 | 150 | 152 | 153 | 154 | Edit 155 | 156 | 157 | Delete 158 |
5 164 | 165 | Some item on your list 166 | 167 | 169 | 170 | 171 | Edit 172 | 173 | 174 | Delete 175 |
6 181 | 182 | Some item on your list 183 | 184 | 186 | 187 | 188 | Edit 189 | 190 | 191 | Delete 192 |
7 198 | 199 | Some item on your list 200 | 201 | 203 | 204 | 205 | Edit 206 | 207 | 208 | Delete 209 |
8 215 | 216 | Some item on your list 217 | 218 | 220 | 221 | 222 | Edit 223 | 224 | 225 | Delete 226 |
9 232 | 233 | Some item on your list 234 | 235 | 237 | 238 | 239 | Edit 240 | 241 | 242 | Delete 243 |
248 | 249 |
250 |
251 | 252 |
253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | -------------------------------------------------------------------------------- /bubblegum/form.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Bubblegum - Bootstrap Theme 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 37 | 38 | 39 | 40 |
41 | 42 |

Sample form

43 | 44 |
45 |
46 |

47 | Your cart 48 | 3 49 |

50 |
    51 |
  • 52 |
    53 |
    Product name
    54 | Brief description 55 |
    56 | $12 57 |
  • 58 |
  • 59 |
    60 |
    Second product
    61 | Brief description 62 |
    63 | $8 64 |
  • 65 |
  • 66 |
    67 |
    Third item
    68 | Brief description 69 |
    70 | $5 71 |
  • 72 |
  • 73 | Total (USD) 74 | $20 75 |
  • 76 |
77 | 78 |
79 |
80 | 81 |
82 | 83 |
84 |
85 |
86 |
87 |
88 |

Billing address

89 |
90 |
91 |
92 | 93 | 94 |
95 | Valid first name is required. 96 |
97 |
98 |
99 | 100 | 101 |
102 | Valid last name is required. 103 |
104 |
105 |
106 | 107 |
108 | 109 |
110 |
111 | @ 112 |
113 | 114 |
115 | Your username is required. 116 |
117 |
118 |
119 | 120 |
121 | 122 | 123 |
124 | Please enter a valid email address for shipping updates. 125 |
126 |
127 | 128 |
129 | 130 | 131 |
132 | Please enter your shipping address. 133 |
134 |
135 | 136 |
137 | 138 | 139 |
140 | 141 |
142 |
143 | 144 | 148 |
149 | Please select a valid country. 150 |
151 |
152 |
153 | 154 | 158 |
159 | Please provide a valid state. 160 |
161 |
162 |
163 | 164 | 165 |
166 | Zip code required. 167 |
168 |
169 |
170 |
171 |
172 | 173 | 174 |
175 |
176 | 177 | 178 |
179 |
180 | 181 |

Payment

182 | 183 |
184 |
185 | 186 | 187 |
188 |
189 | 190 | 191 |
192 |
193 | 194 | 195 |
196 |
197 |
198 |
199 | 200 | 201 | Full name as displayed on card 202 |
203 | Name on card is required 204 |
205 |
206 |
207 | 208 | 209 |
210 | Credit card number is required 211 |
212 |
213 |
214 |
215 |
216 | 217 | 218 |
219 | Expiration date required 220 |
221 |
222 |
223 | 224 | 225 |
226 | Security code required 227 |
228 |
229 |
230 |
231 | 232 |
233 |
234 |
235 |
236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | -------------------------------------------------------------------------------- /bubblegum/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Bubblegum - Bootstrap Theme 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 37 | 38 | 39 | 40 | 41 |
42 |
43 |

Bubblegum

44 | This is a starter template with a jumbotron 45 | 46 | 58 |
59 |
60 | 61 | 62 |
63 |

Thanks

64 | 65 |

Thank you for downloading this theme. If you have trouble or find a bug, please open an issue on GitHub:
66 | https://github.com/HackerThemes/theme-machine

67 | 68 | 69 |
70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /bubblegum/js/popper.1.14.7.min.js.: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) Federico Zivolo 2019 3 | Distributed under the MIT License (license terms are at http://opensource.org/licenses/MIT). 4 | */(function(e,t){'object'==typeof exports&&'undefined'!=typeof module?module.exports=t():'function'==typeof define&&define.amd?define(t):e.Popper=t()})(this,function(){'use strict';function e(e){return e&&'[object Function]'==={}.toString.call(e)}function t(e,t){if(1!==e.nodeType)return[];var o=e.ownerDocument.defaultView,n=o.getComputedStyle(e,null);return t?n[t]:n}function o(e){return'HTML'===e.nodeName?e:e.parentNode||e.host}function n(e){if(!e)return document.body;switch(e.nodeName){case'HTML':case'BODY':return e.ownerDocument.body;case'#document':return e.body;}var i=t(e),r=i.overflow,p=i.overflowX,s=i.overflowY;return /(auto|scroll|overlay)/.test(r+s+p)?e:n(o(e))}function r(e){return 11===e?pe:10===e?se:pe||se}function p(e){if(!e)return document.documentElement;for(var o=r(10)?document.body:null,n=e.offsetParent||null;n===o&&e.nextElementSibling;)n=(e=e.nextElementSibling).offsetParent;var i=n&&n.nodeName;return i&&'BODY'!==i&&'HTML'!==i?-1!==['TH','TD','TABLE'].indexOf(n.nodeName)&&'static'===t(n,'position')?p(n):n:e?e.ownerDocument.documentElement:document.documentElement}function s(e){var t=e.nodeName;return'BODY'!==t&&('HTML'===t||p(e.firstElementChild)===e)}function d(e){return null===e.parentNode?e:d(e.parentNode)}function a(e,t){if(!e||!e.nodeType||!t||!t.nodeType)return document.documentElement;var o=e.compareDocumentPosition(t)&Node.DOCUMENT_POSITION_FOLLOWING,n=o?e:t,i=o?t:e,r=document.createRange();r.setStart(n,0),r.setEnd(i,0);var l=r.commonAncestorContainer;if(e!==l&&t!==l||n.contains(i))return s(l)?l:p(l);var f=d(e);return f.host?a(f.host,t):a(e,d(t).host)}function l(e){var t=1=o.clientWidth&&n>=o.clientHeight}),l=0a[e]&&!t.escapeWithReference&&(n=Q(f[o],a[e]-('right'===e?f.width:f.height))),le({},o,n)}};return l.forEach(function(e){var t=-1===['left','top'].indexOf(e)?'secondary':'primary';f=fe({},f,m[t](e))}),e.offsets.popper=f,e},priority:['left','right','top','bottom'],padding:5,boundariesElement:'scrollParent'},keepTogether:{order:400,enabled:!0,fn:function(e){var t=e.offsets,o=t.popper,n=t.reference,i=e.placement.split('-')[0],r=Z,p=-1!==['top','bottom'].indexOf(i),s=p?'right':'bottom',d=p?'left':'top',a=p?'width':'height';return o[s]r(n[s])&&(e.offsets.popper[d]=r(n[s])),e}},arrow:{order:500,enabled:!0,fn:function(e,o){var n;if(!K(e.instance.modifiers,'arrow','keepTogether'))return e;var i=o.element;if('string'==typeof i){if(i=e.instance.popper.querySelector(i),!i)return e;}else if(!e.instance.popper.contains(i))return console.warn('WARNING: `arrow.element` must be child of its popper element!'),e;var r=e.placement.split('-')[0],p=e.offsets,s=p.popper,d=p.reference,a=-1!==['left','right'].indexOf(r),l=a?'height':'width',f=a?'Top':'Left',m=f.toLowerCase(),h=a?'left':'top',c=a?'bottom':'right',u=S(i)[l];d[c]-us[c]&&(e.offsets.popper[m]+=d[m]+u-s[c]),e.offsets.popper=g(e.offsets.popper);var b=d[m]+d[l]/2-u/2,w=t(e.instance.popper),y=parseFloat(w['margin'+f],10),E=parseFloat(w['border'+f+'Width'],10),v=b-e.offsets.popper[m]-y-E;return v=ee(Q(s[l]-u,v),0),e.arrowElement=i,e.offsets.arrow=(n={},le(n,m,$(v)),le(n,h,''),n),e},element:'[x-arrow]'},flip:{order:600,enabled:!0,fn:function(e,t){if(W(e.instance.modifiers,'inner'))return e;if(e.flipped&&e.placement===e.originalPlacement)return e;var o=v(e.instance.popper,e.instance.reference,t.padding,t.boundariesElement,e.positionFixed),n=e.placement.split('-')[0],i=T(n),r=e.placement.split('-')[1]||'',p=[];switch(t.behavior){case ge.FLIP:p=[n,i];break;case ge.CLOCKWISE:p=G(n);break;case ge.COUNTERCLOCKWISE:p=G(n,!0);break;default:p=t.behavior;}return p.forEach(function(s,d){if(n!==s||p.length===d+1)return e;n=e.placement.split('-')[0],i=T(n);var a=e.offsets.popper,l=e.offsets.reference,f=Z,m='left'===n&&f(a.right)>f(l.left)||'right'===n&&f(a.left)f(l.top)||'bottom'===n&&f(a.top)f(o.right),g=f(a.top)f(o.bottom),b='left'===n&&h||'right'===n&&c||'top'===n&&g||'bottom'===n&&u,w=-1!==['top','bottom'].indexOf(n),y=!!t.flipVariations&&(w&&'start'===r&&h||w&&'end'===r&&c||!w&&'start'===r&&g||!w&&'end'===r&&u);(m||b||y)&&(e.flipped=!0,(m||b)&&(n=p[d+1]),y&&(r=z(r)),e.placement=n+(r?'-'+r:''),e.offsets.popper=fe({},e.offsets.popper,D(e.instance.popper,e.offsets.reference,e.placement)),e=P(e.instance.modifiers,e,'flip'))}),e},behavior:'flip',padding:5,boundariesElement:'viewport'},inner:{order:700,enabled:!1,fn:function(e){var t=e.placement,o=t.split('-')[0],n=e.offsets,i=n.popper,r=n.reference,p=-1!==['left','right'].indexOf(o),s=-1===['top','left'].indexOf(o);return i[p?'left':'top']=r[o]-(s?i[p?'width':'height']:0),e.placement=T(t),e.offsets.popper=g(i),e}},hide:{order:800,enabled:!0,fn:function(e){if(!K(e.instance.modifiers,'hide','preventOverflow'))return e;var t=e.offsets.reference,o=C(e.instance.modifiers,function(e){return'preventOverflow'===e.name}).boundaries;if(t.bottomo.right||t.top>o.bottom||t.rightwindow.devicePixelRatio||!me),c='bottom'===o?'top':'bottom',g='right'===n?'left':'right',b=H('transform');if(d='bottom'==c?'HTML'===l.nodeName?-l.clientHeight+h.bottom:-f.height+h.bottom:h.top,s='right'==g?'HTML'===l.nodeName?-l.clientWidth+h.right:-f.width+h.right:h.left,a&&b)m[b]='translate3d('+s+'px, '+d+'px, 0)',m[c]=0,m[g]=0,m.willChange='transform';else{var w='bottom'==c?-1:1,y='right'==g?-1:1;m[c]=d*w,m[g]=s*y,m.willChange=c+', '+g}var E={"x-placement":e.placement};return e.attributes=fe({},E,e.attributes),e.styles=fe({},m,e.styles),e.arrowStyles=fe({},e.offsets.arrow,e.arrowStyles),e},gpuAcceleration:!0,x:'bottom',y:'right'},applyStyle:{order:900,enabled:!0,fn:function(e){return j(e.instance.popper,e.styles),V(e.instance.popper,e.attributes),e.arrowElement&&Object.keys(e.arrowStyles).length&&j(e.arrowElement,e.arrowStyles),e},onLoad:function(e,t,o,n,i){var r=L(i,t,e,o.positionFixed),p=O(o.placement,r,t,e,o.modifiers.flip.boundariesElement,o.modifiers.flip.padding);return t.setAttribute('x-placement',p),j(t,{position:o.positionFixed?'fixed':'absolute'}),o},gpuAcceleration:void 0}}},ue}); 5 | //# sourceMappingURL=popper.min.js.map 6 | -------------------------------------------------------------------------------- /bubblegum/webfonts/fa-brands-400.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bertvandepoel/tabby/3c429cf99b995b99bc1e88f88535d573516c7e67/bubblegum/webfonts/fa-brands-400.eot -------------------------------------------------------------------------------- /bubblegum/webfonts/fa-brands-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bertvandepoel/tabby/3c429cf99b995b99bc1e88f88535d573516c7e67/bubblegum/webfonts/fa-brands-400.ttf -------------------------------------------------------------------------------- /bubblegum/webfonts/fa-brands-400.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bertvandepoel/tabby/3c429cf99b995b99bc1e88f88535d573516c7e67/bubblegum/webfonts/fa-brands-400.woff -------------------------------------------------------------------------------- /bubblegum/webfonts/fa-brands-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bertvandepoel/tabby/3c429cf99b995b99bc1e88f88535d573516c7e67/bubblegum/webfonts/fa-brands-400.woff2 -------------------------------------------------------------------------------- /bubblegum/webfonts/fa-regular-400.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bertvandepoel/tabby/3c429cf99b995b99bc1e88f88535d573516c7e67/bubblegum/webfonts/fa-regular-400.eot -------------------------------------------------------------------------------- /bubblegum/webfonts/fa-regular-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bertvandepoel/tabby/3c429cf99b995b99bc1e88f88535d573516c7e67/bubblegum/webfonts/fa-regular-400.ttf -------------------------------------------------------------------------------- /bubblegum/webfonts/fa-regular-400.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bertvandepoel/tabby/3c429cf99b995b99bc1e88f88535d573516c7e67/bubblegum/webfonts/fa-regular-400.woff -------------------------------------------------------------------------------- /bubblegum/webfonts/fa-regular-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bertvandepoel/tabby/3c429cf99b995b99bc1e88f88535d573516c7e67/bubblegum/webfonts/fa-regular-400.woff2 -------------------------------------------------------------------------------- /bubblegum/webfonts/fa-solid-900.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bertvandepoel/tabby/3c429cf99b995b99bc1e88f88535d573516c7e67/bubblegum/webfonts/fa-solid-900.eot -------------------------------------------------------------------------------- /bubblegum/webfonts/fa-solid-900.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bertvandepoel/tabby/3c429cf99b995b99bc1e88f88535d573516c7e67/bubblegum/webfonts/fa-solid-900.ttf -------------------------------------------------------------------------------- /bubblegum/webfonts/fa-solid-900.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bertvandepoel/tabby/3c429cf99b995b99bc1e88f88535d573516c7e67/bubblegum/webfonts/fa-solid-900.woff -------------------------------------------------------------------------------- /bubblegum/webfonts/fa-solid-900.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bertvandepoel/tabby/3c429cf99b995b99bc1e88f88535d573516c7e67/bubblegum/webfonts/fa-solid-900.woff2 -------------------------------------------------------------------------------- /changelog.txt: -------------------------------------------------------------------------------- 1 | version 1.2.2 - commit 3eb1c7b26fd2196b68674f36d7302004bf3666b7 2 | 3 | Small fix: add R for South African Rand as a currency during installation 4 | 5 | version 1.2.1 - commit 3c0ec99ebcd3ca59506f2bf5370364e9de4f4ee9 6 | 7 | Small fixes: 8 | - Better error message when not supplying a comment on an activity debt line 9 | - Add more IDE files and folders to gitignore (development improvement) 10 | - Fix edge case where on specific server configurations thesession starting after output is already sent to the buffer causes errors 11 | 12 | version 1.2 - commit 5add4225155daf537ff2664418001fb68e6f415e 13 | 14 | Recurring expenses 15 | Account email address aliases 16 | Currency symbols are now configurable 17 | Broader favicon support 18 | Some small quality of life fixes: 19 | - consistent use of "my" 20 | - display account email address on My profile 21 | 22 | 23 | version 1.1.2 - commit f358375d5d47e0502bd13eabfb4d28751b59a304 24 | 25 | Fix problems with amount larger than or equal to 1000 euro not being displayed properly 26 | Improve new activity email notifications 27 | 28 | version 1.1.1 - commit eef6723c2e891162584689158235362e693b586b 29 | 30 | Add merge functionality to merge mutual debt (when both the logged in user and another user have debt with each other) 31 | Small fix: improve sorting when debt and credit are on the same date 32 | 33 | version 1.1 - commit 7964df787e6e3cc55f9869d74fa9d9811893a136 34 | 35 | Introduces a procedure for installation and upgrades (includes changes to the database structure, configuration file and .htaccess). 36 | Support for PostgreSQL (includes changes to the database structure). 37 | Support for webcron. 38 | Some small quality of life fixes: 39 | - Sort by the most recent transaction when debt is identical 40 | - Better introduction text on landing page 41 | - Clear up text describing the licenses of Bootstrap and Font Awesome 42 | - Improve URL handling by merging reminderurl and base_url configuration 43 | - Favicon ico file fallback 44 | - More semantic link for home icon 45 | 46 | version 1.0.1 - commit 75ab9c8d0f69938240146989b6832d4c20a5ee65 47 | 48 | Some small quality of life fixes: 49 | - When logged in, redirect any token link to "My debt" 50 | - Add a "Today" button to enter the current date in forms 51 | - Fix sorting issue in detailed overview of a user 52 | - Mention total debt in new activity emails, not just the newly added debt 53 | - Favicons 54 | - Hide unnecessary layout elements on the chrome/chromium date picker 55 | 56 | version 1.0 - commit 67b554a08bbed216423b8d968c67ddfe8169df2a 57 | 58 | Very first version of Tabby. 59 | Includes all core features of Tabby as well as the striking and user-friendly interface. 60 | -------------------------------------------------------------------------------- /cron.php: -------------------------------------------------------------------------------- 1 | prepare('SELECT value FROM config WHERE id=?'); 13 | $check->execute(array('cron')); 14 | $result = $check->fetch(PDO::FETCH_ASSOC); 15 | 16 | if(date('Y-m-d') !== date('Y-m-d', $result['value'])){ 17 | $users = get_users_by_reminddif($days); 18 | 19 | foreach($users as $user) { 20 | if(user_have_debtors_in_debt($user['email'])) { 21 | if(is_null($user['reminddate'])) { 22 | $message = "Hi " . $user['name'] . ",\r\n\r\nIt seems you've never sent any reminders on Tabby. Since some people still have an open debt with you, it's probably best if you check your bank account, update any information on Tabby and then sent out new reminders if required.\r\n\r\nYou can get started straight away at " . $base_url . ".\r\n\r\nTabby will remind you every " . $days . " days as long as there is open debt.\r\n\r\nHave a nice day!\r\n\r\nTabby"; 23 | } 24 | else { 25 | $message = "Hi " . $user['name'] . ",\r\n\r\nIt seems you haven't sent any new reminders on Tabby since " . date('d M Y', strtotime($user['reminddate'])) . ". Since some people still have an open debt with you, it's probably best if you check your bank account, update any information on Tabby and then sent out new reminders if required.\r\n\r\nYou can get started straight away at " . $base_url . ".\r\n\r\nTabby will remind you every " . $days . " days as long as there is open debt.\r\n\r\nHave a nice day!\r\n\r\nTabby"; 26 | } 27 | 28 | $headers = 'From: ' . $application_email; 29 | mail($user['email'], 'Tabby: time to send reminders', $message, $headers); 30 | } 31 | } 32 | 33 | $recurring_expenses = get_all_recurring(); 34 | 35 | foreach($recurring_expenses as $recurring_expense) { 36 | if(strtotime(get_nextrun($recurring_expense['start'], $recurring_expense['frequency'], $recurring_expense['lastrun'])) <= strtotime(date('Y-m-d'))) { 37 | execute_recurring($recurring_expense['id']); 38 | } 39 | } 40 | 41 | $update = $db->prepare('UPDATE config SET value=? WHERE id=?'); 42 | $update->execute(array(strtotime('now'), 'cron')); 43 | } 44 | -------------------------------------------------------------------------------- /db_mysql.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `users` ( 2 | `email` varchar(50) NOT NULL, 3 | `name` varchar(50) NOT NULL, 4 | `password` varchar(255) NOT NULL, 5 | `iban` varchar(34) NOT NULL, 6 | `reminddate` date NULL, 7 | PRIMARY KEY (`email`) 8 | ); 9 | 10 | CREATE TABLE `tokens` ( 11 | `email` varchar(50) NOT NULL, 12 | `token` varchar(25) NOT NULL, 13 | PRIMARY KEY (`email`), 14 | UNIQUE KEY (`token`) 15 | ); 16 | 17 | CREATE TABLE `activities` ( 18 | `id` int NOT NULL AUTO_INCREMENT, 19 | `name` varchar(250) NOT NULL, 20 | `owner` varchar(50) NOT NULL, 21 | `date` date NOT NULL, 22 | PRIMARY KEY (`id`), 23 | KEY (`owner`), 24 | FOREIGN KEY (`owner`) REFERENCES users(email) 25 | ); 26 | 27 | CREATE TABLE `debtors` ( 28 | `id` int NOT NULL AUTO_INCREMENT, 29 | `name` varchar(50) NOT NULL, 30 | `email` varchar(50) NOT NULL, 31 | `owner` varchar(50) NOT NULL, 32 | PRIMARY KEY (`id`), 33 | KEY (`owner`), 34 | KEY (`email`), 35 | FOREIGN KEY (`owner`) REFERENCES users(email) 36 | ); 37 | 38 | CREATE TABLE `credits` ( 39 | `id` int NOT NULL AUTO_INCREMENT, 40 | `debtor` int NOT NULL, 41 | `comment` varchar(250) NOT NULL, 42 | `amount` int NOT NULL, 43 | `date` date NOT NULL, 44 | PRIMARY KEY (`id`), 45 | KEY (`debtor`), 46 | FOREIGN KEY (`debtor`) REFERENCES debtors(id) 47 | ); 48 | 49 | 50 | CREATE TABLE `debts` ( 51 | `id` int NOT NULL AUTO_INCREMENT, 52 | `activity` int NOT NULL, 53 | `debtor` int NOT NULL, 54 | `comment` varchar(250) DEFAULT NULL, 55 | `amount` int NOT NULL, 56 | PRIMARY KEY (`id`), 57 | KEY (`debtor`), 58 | KEY (`activity`), 59 | FOREIGN KEY (`debtor`) REFERENCES debtors(id), 60 | FOREIGN KEY (`activity`) REFERENCES activities(id) ON DELETE CASCADE 61 | ); 62 | 63 | 64 | CREATE TABLE `recurring` ( 65 | `id` int NOT NULL AUTO_INCREMENT, 66 | `name` varchar(250) NOT NULL, 67 | `owner` varchar(50) NOT NULL, 68 | `amount` int NOT NULL, 69 | `start` date NOT NULL, 70 | `frequency` varchar(5) NOT NULL, 71 | `lastrun` date DEFAULT NULL, 72 | PRIMARY KEY (`id`), 73 | KEY (`owner`), 74 | KEY (`lastrun`), 75 | FOREIGN KEY (`owner`) REFERENCES users(email) 76 | ); 77 | 78 | CREATE TABLE `recurring_debtors` ( 79 | `recurringid` int NOT NULL, 80 | `debtor` int NOT NULL, 81 | PRIMARY KEY (`recurringid`, `debtor`), 82 | FOREIGN KEY (`recurringid`) REFERENCES recurring(id) ON DELETE CASCADE, 83 | FOREIGN KEY (`debtor`) REFERENCES debtors(id) 84 | ); 85 | 86 | 87 | CREATE TABLE `pending_users` ( 88 | `email` varchar(50) NOT NULL, 89 | `name` varchar(50) NOT NULL, 90 | `password` varchar(255) NOT NULL, 91 | `iban` varchar(34) NOT NULL, 92 | `confirmation` varchar(25) NOT NULL, 93 | `datetime` datetime NOT NULL, 94 | PRIMARY KEY (`email`), 95 | UNIQUE KEY (`confirmation`) 96 | ); 97 | 98 | CREATE TABLE `aliases` ( 99 | `email` varchar(50) NOT NULL, 100 | `owner` varchar(50) NOT NULL, 101 | `unconfirmed` varchar(25) NULL, 102 | KEY (`owner`), 103 | FOREIGN KEY (`owner`) REFERENCES users(email), 104 | UNIQUE KEY (`unconfirmed`) 105 | ); 106 | 107 | CREATE TABLE `config` ( 108 | `id` varchar(50) NOT NULL, 109 | `value` TEXT NOT NULL, 110 | PRIMARY KEY (`id`) 111 | ); 112 | INSERT INTO `config` VALUES ('schema', '3'); 113 | INSERT INTO `config` VALUES ('cron', '0'); 114 | -------------------------------------------------------------------------------- /db_postgresql.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE "users" ( 2 | "email" varchar(50) NOT NULL, 3 | "name" varchar(50) NOT NULL, 4 | "password" varchar(255) NOT NULL, 5 | "iban" varchar(34) NOT NULL, 6 | "reminddate" date NULL, 7 | PRIMARY KEY ("email") 8 | ); 9 | 10 | 11 | CREATE TABLE "tokens" ( 12 | "email" varchar(50) NOT NULL, 13 | "token" varchar(25) NOT NULL, 14 | PRIMARY KEY ("email"), 15 | UNIQUE ("token") 16 | ); 17 | 18 | 19 | CREATE TABLE "activities" ( 20 | "id" serial, 21 | "name" varchar(250) NOT NULL, 22 | "owner" varchar(50) NOT NULL, 23 | "date" date NOT NULL, 24 | PRIMARY KEY ("id"), 25 | FOREIGN KEY ("owner") REFERENCES users(email) 26 | ); 27 | CREATE INDEX ON "activities" ("owner"); 28 | 29 | 30 | CREATE TABLE "debtors" ( 31 | "id" serial, 32 | "name" varchar(50) NOT NULL, 33 | "email" varchar(50) NOT NULL, 34 | "owner" varchar(50) NOT NULL, 35 | PRIMARY KEY ("id"), 36 | FOREIGN KEY ("owner") REFERENCES users(email) 37 | ); 38 | CREATE INDEX ON "debtors" ("owner"); 39 | CREATE INDEX ON "debtors" ("email"); 40 | 41 | 42 | CREATE TABLE "credits" ( 43 | "id" serial, 44 | "debtor" integer NOT NULL, 45 | "comment" varchar(250) NOT NULL, 46 | "amount" integer NOT NULL, 47 | "date" date NOT NULL, 48 | PRIMARY KEY ("id"), 49 | FOREIGN KEY ("debtor") REFERENCES debtors(id) 50 | ); 51 | CREATE INDEX ON "credits" ("debtor"); 52 | 53 | 54 | CREATE TABLE "debts" ( 55 | "id" serial, 56 | "activity" integer NOT NULL, 57 | "debtor" integer NOT NULL, 58 | "comment" varchar(250) DEFAULT NULL, 59 | "amount" integer NOT NULL, 60 | PRIMARY KEY ("id"), 61 | FOREIGN KEY ("debtor") REFERENCES debtors(id), 62 | FOREIGN KEY ("activity") REFERENCES activities(id) ON DELETE CASCADE 63 | ); 64 | CREATE INDEX ON "debts" ("debtor"); 65 | CREATE INDEX ON "debts" ("activity"); 66 | 67 | 68 | CREATE TABLE "recurring" ( 69 | "id" serial, 70 | "name" varchar(250) NOT NULL, 71 | "owner" varchar(50) NOT NULL, 72 | "amount" integer NOT NULL, 73 | "start" date NOT NULL, 74 | "frequency" varchar(5) NOT NULL, 75 | "lastrun" date DEFAULT NULL, 76 | PRIMARY KEY ("id"), 77 | FOREIGN KEY ("owner") REFERENCES users(email) 78 | ); 79 | CREATE INDEX ON "recurring" ("owner"); 80 | CREATE INDEX ON "recurring" ("lastrun"); 81 | 82 | CREATE TABLE "recurring_debtors" ( 83 | "recurringid" integer NOT NULL, 84 | "debtor" integer NOT NULL, 85 | PRIMARY KEY ("recurringid", "debtor"), 86 | FOREIGN KEY ("recurringid") REFERENCES recurring(id) ON DELETE CASCADE, 87 | FOREIGN KEY ("debtor") REFERENCES debtors(id) 88 | ); 89 | 90 | 91 | CREATE TABLE "pending_users" ( 92 | "email" varchar(50) NOT NULL, 93 | "name" varchar(50) NOT NULL, 94 | "password" varchar(255) NOT NULL, 95 | "iban" varchar(34) NOT NULL, 96 | "confirmation" varchar(25) NOT NULL, 97 | "datetime" timestamp NOT NULL, 98 | PRIMARY KEY ("email"), 99 | UNIQUE ("confirmation") 100 | ); 101 | 102 | CREATE TABLE "aliases" ( 103 | "email" varchar(50) NOT NULL, 104 | "owner" varchar(50) NOT NULL, 105 | "unconfirmed" varchar(25) NULL, 106 | FOREIGN KEY ("owner") REFERENCES users(email), 107 | UNIQUE ("unconfirmed") 108 | ); 109 | CREATE INDEX ON "aliases" ("owner"); 110 | 111 | CREATE TABLE "config" ( 112 | "id" varchar(50) NOT NULL, 113 | "value" TEXT NOT NULL, 114 | PRIMARY KEY ("id") 115 | ); 116 | INSERT INTO "config" VALUES ('schema', '3'); 117 | INSERT INTO "config" VALUES ('cron', '0'); 118 | -------------------------------------------------------------------------------- /favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bertvandepoel/tabby/3c429cf99b995b99bc1e88f88535d573516c7e67/favicon/favicon.ico -------------------------------------------------------------------------------- /favicon/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Tabby - Friendly tool to manage debt", 3 | "short_name": "Tabby", 4 | "icons": [ 5 | { 6 | "src": "tabby-192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "tabby-512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#f13e5a", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } -------------------------------------------------------------------------------- /favicon/tabby-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bertvandepoel/tabby/3c429cf99b995b99bc1e88f88535d573516c7e67/favicon/tabby-16.png -------------------------------------------------------------------------------- /favicon/tabby-180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bertvandepoel/tabby/3c429cf99b995b99bc1e88f88535d573516c7e67/favicon/tabby-180.png -------------------------------------------------------------------------------- /favicon/tabby-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bertvandepoel/tabby/3c429cf99b995b99bc1e88f88535d573516c7e67/favicon/tabby-192.png -------------------------------------------------------------------------------- /favicon/tabby-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bertvandepoel/tabby/3c429cf99b995b99bc1e88f88535d573516c7e67/favicon/tabby-32.png -------------------------------------------------------------------------------- /favicon/tabby-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bertvandepoel/tabby/3c429cf99b995b99bc1e88f88535d573516c7e67/favicon/tabby-512.png -------------------------------------------------------------------------------- /favicon/tabby-monochrome.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 15 | 17 | 19 | 20 | 22 | image/svg+xml 23 | 25 | 26 | 27 | 28 | 32 | 36 | 43 | 44 | 48 | 53 | 58 | 63 | 68 | 72 | 77 | 81 | 85 | 89 | 93 | 97 | 102 | 107 | 113 | 119 | 125 | 131 | 135 | 139 | 143 | 147 | 148 | 149 | -------------------------------------------------------------------------------- /resources/activities.php: -------------------------------------------------------------------------------- 1 | prepare('INSERT INTO activities (name, owner, date) VALUES (?,?,?)'); 9 | $insert->execute(array($name, $owner, $date)); 10 | return $db->lastInsertId(); 11 | } 12 | 13 | function add_debt($actid, $debtor, $comment, $amount) { 14 | global $db; 15 | $insert = $db->prepare('INSERT INTO debts (activity, debtor, comment, amount) VALUES (?,?,?,?)'); 16 | $insert->execute(array($actid, $debtor, $comment, $amount)); 17 | } 18 | 19 | function email_new_debt($debtor, $user, $actname, $actdate, $comment, $amount, $total, $token) { 20 | global $base_url; 21 | global $application_email; 22 | 23 | if($total <= -$amount) { // No credit or outstanding debt 24 | $message = "Hi there,\r\n\r\nThis is a notification that " . $user['name'] . " has added \"" . $actname . "\" in Tabby as a new activity that took place on " . $actdate . ". You owe them " . human_friendly_amount($amount, FALSE, TRUE) . " for this activity, they mentioned the following details for you: \"" . $comment . "\". This brings your total debt to " . human_friendly_amount(-$total, FALSE, TRUE) . ". You can transfer the money to the following bank account: " . $user['iban'] . "\r\n\r\nYou can see an overview of all of your debt by visiting " . $base_url . "token/" . $token . "\r\n\r\nHave a nice day!\r\n\r\nTabby"; 25 | } 26 | elseif($total > 0) { // More than enough credit to cover the cost 27 | $message = "Hi there,\r\n\r\nThis is a notification that " . $user['name'] . " has added \"" . $actname . "\" in Tabby as a new activity that took place on " . $actdate . ". Your cost is " . human_friendly_amount($amount, FALSE, TRUE) . " for this activity, they mentioned the following details for you: \"" . $comment . "\". Your account had enough outstanding credit to offset this cost. You currently have " . human_friendly_amount($total, FALSE, TRUE) . " of credit left.\r\n\r\nYou can see an overview of all costs and credits by visiting " . $base_url . "token/" . $token . "\r\n\r\nHave a nice day!\r\n\r\nTabby"; 28 | } 29 | elseif($total == 0) { // Just enough credit to cover the debt 30 | $message = "Hi there,\r\n\r\nThis is a notification that " . $user['name'] . " has added \"" . $actname . "\" in Tabby as a new activity that took place on " . $actdate . ". Your cost is " . human_friendly_amount($amount, FALSE, TRUE) . " for this activity, they mentioned the following details for you: \"" . $comment . "\". Your account had just enough outstanding credit to offset this cost, so you now have " . human_friendly_amount(0, FALSE, TRUE) . " of credit left.\r\n\r\nYou can see an overview of all costs and credits by visiting " . $base_url . "token/" . $token . "\r\n\r\nHave a nice day!\r\n\r\nTabby"; 31 | } 32 | else { // Some credit, but not enough to fully pay the debt 33 | $message = "Hi there,\r\n\r\nThis is a notification that " . $user['name'] . " has added \"" . $actname . "\" in Tabby as a new activity that took place on " . $actdate . ". Your cost is " . human_friendly_amount($amount, FALSE, TRUE) . " for this activity, they mentioned the following details for you: \"" . $comment . "\". Your account had some outstanding credit but not enough to fully offset the cost. This leaves a remaining debt of " . human_friendly_amount(-$total, FALSE, TRUE) . ". You can transfer the money to the following bank account: " . $user['iban'] . "\r\n\r\nYou can see an overview of all costs and credits by visiting " . $base_url . "token/" . $token . "\r\n\r\nHave a nice day!\r\n\r\nTabby"; 34 | } 35 | 36 | $headers = array( 37 | 'From' => $application_email, 38 | 'Reply-To' => $user['email'], 39 | 'Content-Type' => 'text/plain; charset=UTF-8' 40 | ); 41 | mail($debtor['email'], 'Tabby: ' . $user['name'] . ' added ' . $actname, $message, $headers); 42 | } 43 | 44 | function del_activity($id) { 45 | global $db; 46 | $del = $db->prepare('DELETE FROM activities WHERE id=? AND owner=?'); 47 | $del->execute(array($id, $_SESSION['tabby_loggedin'])); 48 | if($del->rowCount() > 0) { 49 | return TRUE; 50 | } 51 | return FALSE; 52 | } 53 | 54 | function get_activity($actid) { 55 | global $db; 56 | $get = $db->prepare('SELECT id, name, date FROM activities WHERE id=? AND owner=?'); 57 | $get->execute(array($actid, $_SESSION['tabby_loggedin'])); 58 | $result = $get->fetch(PDO::FETCH_ASSOC); 59 | if(!$result) { 60 | return FALSE; 61 | } 62 | return $result; 63 | } 64 | -------------------------------------------------------------------------------- /resources/init.php: -------------------------------------------------------------------------------- 1 | 'SET NAMES utf8', PDO::MYSQL_ATTR_FOUND_ROWS => TRUE)); 8 | } 9 | else { 10 | $db = new PDO($dsn, $db_username, $db_password); 11 | } 12 | } catch (PDOException $e) { 13 | include('templates/header.php'); 14 | $error = 'Something went wrong while connecting to the database.'; 15 | include('templates/error.php'); 16 | include('templates/footer.php'); 17 | exit; 18 | } 19 | 20 | session_start(); 21 | -------------------------------------------------------------------------------- /resources/install.php: -------------------------------------------------------------------------------- 1 | query('ALTER DATABASE `' . $_POST['db_name'] . '` DEFAULT CHARSET=utf8mb4;'); 10 | $sql = file_get_contents('db_mysql.sql'); 11 | } 12 | $result = $db->exec($sql); 13 | if($result === FALSE) { 14 | return FALSE; 15 | } 16 | return TRUE; 17 | } 18 | 19 | function create_first_user($email, $name, $password, $iban) { 20 | global $db; 21 | $insert = $db->prepare('INSERT INTO users VALUES (?,?,?,?,?)'); 22 | $insert->execute(array($email, $name, password_hash($password, PASSWORD_DEFAULT), $iban, NULL)); 23 | if($insert->rowCount() === 0) { 24 | return FALSE; 25 | } 26 | return TRUE; 27 | } 28 | 29 | function create_config($dsn, $db_username, $db_password, $app_email, $admin_email, $base_url, $currency, $days, $cron_type) { 30 | $config = 'prepare('SELECT id, name, email FROM debtors WHERE owner=? ORDER BY name'); 6 | $get->execute(array($_SESSION['tabby_loggedin'])); 7 | return $get->fetchAll(PDO::FETCH_ASSOC); 8 | } 9 | 10 | function check_debtor($email) { 11 | global $db; 12 | $get = $db->prepare('SELECT count(*) FROM debtors WHERE email=? AND owner=?'); 13 | $get->execute(array($email, $_SESSION['tabby_loggedin'])); 14 | if($get->fetchColumn() == 0) { 15 | return TRUE; 16 | } 17 | return FALSE; 18 | } 19 | 20 | function check_any_debtors($emails) { 21 | global $db; 22 | $get = $db->prepare('SELECT count(*) FROM debtors WHERE email=? AND owner=?'); 23 | foreach($emails as $email) { 24 | $get->execute(array($email, $_SESSION['tabby_loggedin'])); 25 | if($get->fetchColumn() > 0) { 26 | return FALSE; 27 | } 28 | } 29 | return T; 30 | } 31 | 32 | function add_debtor($name, $email) { 33 | global $db; 34 | $insert = $db->prepare('INSERT INTO debtors (name, email, owner) VALUES (?,?,?)'); 35 | $insert->execute(array($name, $email, $_SESSION['tabby_loggedin'])); 36 | } 37 | 38 | function update_debtor($oldemail, $newname, $newemail) { 39 | global $db; 40 | $update = $db->prepare('UPDATE debtors SET email=?, name=? WHERE email=? AND owner=?'); 41 | $update->execute(array($newemail, $newname, $oldemail, $_SESSION['tabby_loggedin'])); 42 | } 43 | 44 | function delete_debtor($email) { 45 | global $db; 46 | $delete = $db->prepare('DELETE FROM debtors WHERE email=? AND owner=?'); 47 | $delete->execute(array($email, $_SESSION['tabby_loggedin'])); 48 | if($delete->errorCode() === '00000') { 49 | return TRUE; 50 | } 51 | else { 52 | return FALSE; 53 | } 54 | } 55 | 56 | function change_debtor_email($debtor, $email) { 57 | global $db; 58 | $update = $db->prepare('UPDATE debtors SET email=? WHERE email=? AND owner=?'); 59 | $update->execute(array($email, $debtor, $_SESSION['tabby_loggedin'])); 60 | } 61 | 62 | function get_debtor_details($email, $owner = NULL) { 63 | global $db; 64 | if(is_null($owner)) { 65 | $owner = $_SESSION['tabby_loggedin']; 66 | } 67 | $get = $db->prepare('SELECT * FROM debtors WHERE email=? AND owner=?'); 68 | $get->execute(array($email, $owner)); 69 | $result = $get->fetch(PDO::FETCH_ASSOC); 70 | return $result; 71 | } 72 | 73 | function get_debtor_token($email) { 74 | global $db; 75 | $get = $db->prepare('SELECT token FROM tokens WHERE email=?'); 76 | $get->execute(array($email)); 77 | $result = $get->fetch(PDO::FETCH_ASSOC); 78 | if(!$result) { 79 | return FALSE; 80 | } 81 | return $result['token']; 82 | } 83 | 84 | function create_debtor_token($email) { 85 | global $db; 86 | $insert = $db->prepare('INSERT INTO tokens VALUES (?,?)'); 87 | $token = str_rand(25); 88 | $insert->execute(array($email, $token)); 89 | return $token; 90 | } 91 | 92 | function get_token_email($token) { 93 | global $db; 94 | $get = $db->prepare('SELECT email FROM tokens WHERE token=?'); 95 | $get->execute(array($token)); 96 | $result = $get->fetch(PDO::FETCH_ASSOC); 97 | if(empty($result)) { 98 | return FALSE; 99 | } 100 | return $result['email']; 101 | } 102 | 103 | function reset_token($token) { 104 | global $db; 105 | $update = $db->prepare('UPDATE tokens SET token=? WHERE token=?'); 106 | $newtoken = str_rand(25); 107 | $update->execute(array($newtoken, $token)); 108 | return $newtoken; 109 | } 110 | 111 | function mail_token($email, $token) { 112 | global $base_url; 113 | global $application_email; 114 | $message = "Hi there,\r\n\r\nYou requested an overview of your debt and credit. You can find that on " . $base_url . "token/" . $token . "\r\n\r\nHave a nice day!\r\n\r\nTabby"; 115 | $headers = 'From: ' . $application_email; 116 | mail($email, 'Tabby: you requested an overview', $message, $headers); 117 | } 118 | 119 | function email_reminder($email, $total, $comment, $token, $user) { 120 | global $base_url; 121 | global $application_email; 122 | 123 | update_reminddate(); 124 | 125 | if(is_null($comment)) { 126 | $message = "Hi there,\r\n\r\nThis is a reminder from " . $user['name'] . ". You owe them " . human_friendly_amount(-$total, FALSE, TRUE) . ".\r\n\r\nYou can transfer the money to their bank account: " . $user['iban'] . "\r\n\r\nYou can see an overview of all of your debt by visiting " . $base_url . "token/" . $token . "\r\n\r\nHave a nice day!\r\n\r\nTabby"; 127 | } 128 | else { 129 | $message = "Hi there,\r\n\r\nThis is a reminder from " . $user['name'] . ". You owe them " . human_friendly_amount(-$total, FALSE, TRUE) . ".\r\n\r\nThey added the following message for you \"" . $comment . "\".\r\n\r\nYou can transfer the money to their bank account: " . $user['iban'] . "\r\n\r\nYou can see an overview of all of your debt by visiting " . $base_url . "token/" . $token . "\r\n\r\nHave a nice day!\r\n\r\nTabby"; 130 | } 131 | 132 | $headers = array( 133 | 'From' => $application_email, 134 | 'Reply-To' => $user['email'], 135 | 'Content-Type' => 'text/plain; charset=UTF-8' 136 | ); 137 | mail($email, 'Tabby: ' . $user['name'] . ' is reminding you', $message, $headers); 138 | } 139 | -------------------------------------------------------------------------------- /resources/recurring.php: -------------------------------------------------------------------------------- 1 | prepare('SELECT id, name, amount, start, frequency, lastrun FROM recurring WHERE owner=? ORDER BY name'); 6 | $get->execute(array($_SESSION['tabby_loggedin'])); 7 | $result = array(); 8 | while($row = $get->fetch(PDO::FETCH_ASSOC)) { 9 | $row['debtors'] = _get_recurring_debtors($row['id'], $_SESSION['tabby_loggedin']); 10 | $result[] = $row; 11 | } 12 | return $result; 13 | } 14 | 15 | function get_all_recurring() { 16 | global $db; 17 | $get = $db->prepare('SELECT id, name, owner, amount, start, frequency, lastrun FROM recurring'); 18 | $get->execute(array()); 19 | $result = array(); 20 | while($row = $get->fetch(PDO::FETCH_ASSOC)) { 21 | $row['debtors'] = _get_recurring_debtors($row['id'], $row['owner']); 22 | $result[] = $row; 23 | } 24 | return $result; 25 | } 26 | 27 | function get_recurring($id) { 28 | global $db; 29 | $get = $db->prepare('SELECT id, name, amount, start, frequency, lastrun FROM recurring WHERE id=? AND owner=? ORDER BY name'); 30 | $get->execute(array($id, $_SESSION['tabby_loggedin'])); 31 | $result = $get->fetch(PDO::FETCH_ASSOC); 32 | $result['debtors'] = _get_recurring_debtors($result['id'], $result['owner'], $_SESSION['tabby_loggedin']); 33 | return $result; 34 | } 35 | 36 | function _get_recurring_debtors($recurringid, $owner) { 37 | global $db; 38 | $get = $db->prepare('SELECT d.id AS id, d.name AS name FROM recurring_debtors as r, debtors as d WHERE r.recurringid=? AND owner=? AND r.debtor=d.id ORDER BY name'); 39 | $get->execute(array($recurringid, $owner)); 40 | $results = $get->fetchAll(PDO::FETCH_ASSOC); 41 | $return['id'] = array_column($results, 'id'); 42 | $return['name'] = array_column($results, 'name'); 43 | return $return; 44 | } 45 | 46 | function add_recurring($name, $amount, $start, $frequency, $debtors) { 47 | global $db; 48 | $insert = $db->prepare('INSERT INTO recurring (name, owner, amount, start, frequency) VALUES (?,?,?,?,?)'); 49 | $insert->execute(array($name, $_SESSION['tabby_loggedin'], $amount, $start, $frequency)); 50 | $recurringid = $db->lastInsertId(); 51 | $insert = $db->prepare('INSERT INTO recurring_debtors VALUES (?,?)'); 52 | foreach($debtors as $debtor) { 53 | $insert->execute(array($recurringid, $debtor)); 54 | } 55 | return $recurringid; 56 | } 57 | 58 | function update_recurring($id, $name, $amount, $debtors) { 59 | global $db; 60 | $update = $db->prepare('UPDATE recurring SET name=?, amount=? WHERE id=? AND owner=?'); 61 | $update->execute(array($name, $amount, $id, $_SESSION['tabby_loggedin'])); 62 | if($update->rowCount() === 1) { 63 | $delete = $db->prepare('DELETE FROM recurring_debtors WHERE recurringid=?'); 64 | $delete->execute(array($id)); 65 | $insert = $db->prepare('INSERT INTO recurring_debtors VALUES (?,?)'); 66 | foreach($debtors as $debtor) { 67 | $insert->execute(array($id, $debtor)); 68 | } 69 | } 70 | } 71 | 72 | function del_recurring($id) { 73 | global $db; 74 | $del = $db->prepare('DELETE FROM recurring WHERE id=? AND owner=?'); 75 | $del->execute(array($id, $_SESSION['tabby_loggedin'])); 76 | if($del->rowCount() > 0) { 77 | return TRUE; 78 | } 79 | return FALSE; 80 | } 81 | 82 | function execute_recurring($id) { 83 | global $db; 84 | $get = $db->prepare('SELECT name, owner, amount FROM recurring WHERE id=?'); 85 | $get->execute(array($id)); 86 | $recurring = $get->fetch(PDO::FETCH_ASSOC); 87 | $actid = add_activity('Recurring expense', date('Y-m-d'), $recurring['owner']); 88 | 89 | $get = $db->prepare('SELECT d.id AS id, d.name AS name, d.email AS email FROM recurring_debtors as r, debtors as d WHERE r.recurringid=? AND r.debtor=d.id ORDER BY name'); 90 | $get->execute(array($id)); 91 | while($debtor = $get->fetch(PDO::FETCH_ASSOC)) { 92 | add_debt($actid, $debtor['id'], $recurring['name'], $recurring['amount']); 93 | $user = get_user_details($recurring['owner']); 94 | $finstate = get_debtor_financial_state($debtor['email'], $recurring['owner']); 95 | $token = get_debtor_token($debtor['email']); 96 | if($token === FALSE) { 97 | $token = create_debtor_token($debtor['email']); 98 | } 99 | email_new_debt($debtor, $user, 'Recurring expense', date('d M Y'), $recurring['name'], $recurring['amount'], $finstate['total'], $token); 100 | } 101 | 102 | $update = $db->prepare('UPDATE recurring SET lastrun=? WHERE id=?'); 103 | $update->execute(array(date('Y-m-d'), $id)); 104 | } 105 | 106 | function frequency_to_dateintervalstring($frequency, $days = 0) { 107 | if($frequency == 'yearly') { 108 | return 'P1Y'; 109 | } 110 | elseif($frequency == 'monthly') { 111 | return 'P1M'; 112 | } 113 | elseif($frequency == 'weekly') { 114 | return 'P1W'; 115 | } 116 | elseif($frequency == 'days' AND $days > 0) { 117 | return 'P' . $days . 'D'; 118 | } 119 | else { 120 | return FALSE; 121 | } 122 | } 123 | 124 | function dateintervalstring_to_frequency($interval) { 125 | if($interval == 'P1Y') { 126 | return 'yearly'; 127 | } 128 | elseif($interval == 'P1M') { 129 | return 'monthly'; 130 | } 131 | elseif($interval == 'P1W' OR $interval == 'P7D') { 132 | return 'weekly'; 133 | } 134 | else { 135 | return substr($interval, 1, -1) . ' days'; 136 | } 137 | } 138 | 139 | function get_nextrun($start, $frequency, $lastrun) { 140 | if(is_null($lastrun)) { 141 | return $start; 142 | } 143 | $nextrun = new DateTime($lastrun); 144 | $nextrun->add(new DateInterval($frequency)); 145 | return $nextrun->format('Y-m-d'); 146 | } 147 | -------------------------------------------------------------------------------- /resources/transactions.php: -------------------------------------------------------------------------------- 1 | prepare('SELECT a.id AS activity_id, a.name AS activity_name, a.date AS activity_date, debts.comment AS comment, debts.amount AS amount, debtors.id AS debtorid, debtors.name AS name FROM activities AS a, debts, debtors WHERE a.owner=? AND debtors.owner=? AND a.id=debts.activity AND debts.debtor=debtors.id ORDER BY activity_date DESC, activity_id DESC'); 7 | $get->execute(array($_SESSION['tabby_loggedin'], $_SESSION['tabby_loggedin'])); 8 | $finstate = get_all_debtor_financial_state(); 9 | while($row = $get->fetch(PDO::FETCH_ASSOC)) { 10 | if(in_array($row['activity_id'], $finstate[$row['debtorid']]['red'])) { 11 | $row['color'] = 'red'; 12 | } 13 | elseif(in_array($row['activity_id'], $finstate[$row['debtorid']]['orange'])) { 14 | $row['color'] = 'orange'; 15 | } 16 | else { 17 | $row['color'] = 'neutral'; 18 | } 19 | if(!isset($activities[$row['activity_id']])) { 20 | $activities[$row['activity_id']] = array('id' => $row['activity_id'], 'name' => $row['activity_name'], 'date' => $row['activity_date'], 'data' => array()); 21 | } 22 | $activities[$row['activity_id']]['data'][] = array('name' => $row['name'], 'comment' => $row['comment'], 'amount' => $row['amount'], 'color' => $row['color']); 23 | } 24 | return $activities; 25 | } 26 | 27 | function get_activity_transactions($actid) { 28 | global $db; 29 | $activity = array(); 30 | $get = $db->prepare('SELECT a.id AS activity_id, a.name AS activity_name, a.date AS activity_date, debts.id AS debtid, debts.comment AS comment, debts.amount AS amount, debtors.id AS debtorid, debtors.name AS name FROM activities AS a, debts, debtors WHERE a.id=? AND a.owner=? AND debtors.owner=? AND a.id=debts.activity AND debts.debtor=debtors.id ORDER BY activity_date DESC, activity_id DESC'); 31 | $get->execute(array($actid, $_SESSION['tabby_loggedin'], $_SESSION['tabby_loggedin'])); 32 | $finstate = get_all_debtor_financial_state(); 33 | while($row = $get->fetch(PDO::FETCH_ASSOC)) { 34 | if(in_array($row['activity_id'], $finstate[$row['debtorid']]['red'])) { 35 | $row['color'] = 'red'; 36 | } 37 | elseif(in_array($row['activity_id'], $finstate[$row['debtorid']]['orange'])) { 38 | $row['color'] = 'orange'; 39 | } 40 | else { 41 | $row['color'] = 'neutral'; 42 | } 43 | if(empty($activity)) { 44 | $activity = array('id' => $row['activity_id'], 'name' => $row['activity_name'], 'date' => $row['activity_date'], 'data' => array()); 45 | } 46 | $activity['data'][] = array('id' => $row['debtid'],'name' => $row['name'], 'comment' => $row['comment'], 'amount' => $row['amount'], 'color' => $row['color']); 47 | } 48 | return $activity; 49 | } 50 | 51 | function get_transactions_per_debtor() { 52 | global $db; 53 | $debtors = array(); 54 | $get_debt = $db->prepare('SELECT a.id AS activity_id, a.name AS activity_name, a.date AS activity_date, debts.comment AS comment, (debts.amount * -1) AS amount, debtors.id AS debtorid, debtors.name AS name, debtors.email AS email FROM activities AS a, debts, debtors WHERE a.owner=? AND debtors.owner=? AND a.id=debts.activity AND debts.debtor=debtors.id ORDER BY activity_date DESC, activity_id ASC'); 55 | $get_credit = $db->prepare('SELECT debtors.id AS debtorid, credits.amount AS amount, credits.comment AS comment, credits.date AS date, credits.id AS creditid FROM debtors, credits WHERE debtors.owner=? AND debtors.id=credits.debtor ORDER BY date DESC, creditid DESC'); 56 | 57 | $get_debt->execute(array($_SESSION['tabby_loggedin'], $_SESSION['tabby_loggedin'])); 58 | $get_credit->execute(array($_SESSION['tabby_loggedin'])); 59 | $finstate = get_all_debtor_financial_state(); 60 | 61 | while($row = $get_debt->fetch(PDO::FETCH_ASSOC)) { 62 | if(in_array($row['activity_id'], $finstate[$row['debtorid']]['red'])) { 63 | $row['color'] = 'red'; 64 | } 65 | elseif(in_array($row['activity_id'], $finstate[$row['debtorid']]['orange'])) { 66 | $row['color'] = 'orange'; 67 | } 68 | else { 69 | $row['color'] = 'neutral'; 70 | } 71 | if(isset($debtors[$row['debtorid']])) { 72 | if(count($debtors[$row['debtorid']]['data']) == 4) { // we won't display more than 4 records but just give a link for the details 73 | $debtors[$row['debtorid']]['more'] = TRUE; 74 | continue; 75 | } 76 | $debtors[$row['debtorid']]['data'][] = array('date' => $row['activity_date'], 'sort' => $row['activity_date'] . '-1-' . $row['activity_id'] . $row['comment'], 'description' => $row['activity_name'] . ' - ' . $row['comment'], 'amount' => $row['amount'], 'color' => $row['color']); 77 | } 78 | else { 79 | $debtors[$row['debtorid']] = array('name' => $row['name'], 'email' => $row['email'], 'total' => $finstate[$row['debtorid']]['total'], 'more' => FALSE, 'data' => array()); 80 | $debtors[$row['debtorid']]['data'][] = array('date' => $row['activity_date'], 'sort' => $row['activity_date'] . '-1-' . $row['activity_id'] . $row['comment'], 'description' => $row['activity_name'] . ' - ' . $row['comment'], 'amount' => $row['amount'], 'color' => $row['color']); 81 | } 82 | } 83 | while($row = $get_credit->fetch(PDO::FETCH_ASSOC)) { 84 | if(count($debtors[$row['debtorid']]['data']) == 4) { // we won't display more than 4 records but just give a link for the details 85 | $debtors[$row['debtorid']]['more'] = TRUE; 86 | } 87 | $debtors[$row['debtorid']]['data'][] = array('date' => $row['date'], 'sort' => $row['date'] . '-2-' . $row['creditid'], 'description' => $row['comment'], 'amount' => $row['amount'], 'color' => 'green'); 88 | } 89 | foreach($debtors as $key => $value) { 90 | usort($debtors[$key]['data'], function ($a, $b) { 91 | return (-1 * strcmp($a['sort'], $b['sort'])); 92 | }); 93 | $debtors[$key]['data'] = array_reverse(array_slice($debtors[$key]['data'], 0, 4, TRUE), true); 94 | } 95 | usort($debtors, function ($a, $b) { 96 | if($a['total'] < $b['total']) { 97 | return -1; 98 | } 99 | elseif($a['total'] > $b['total']) { 100 | return 1; 101 | } 102 | else { 103 | // total debt is identical, for example 0 104 | // if we don't apply further sorting here, it won't make sense to the user 105 | return ($a['data'][0]['date'] > $b['data'][0]['date']) ? -1 : 1; 106 | } 107 | }); 108 | return $debtors; 109 | } 110 | 111 | function get_debtor_transactions($debtormail) { 112 | global $db; 113 | $debtors = array(); 114 | $get_debt = $db->prepare('SELECT a.id AS activity_id, a.name AS activity_name, a.date AS activity_date, debts.id AS debtid, debts.comment AS comment, (debts.amount * -1) AS amount, debtors.id AS debtorid, debtors.name AS name, debtors.email AS email FROM activities AS a, debts, debtors WHERE a.owner=? AND debtors.owner=? AND debtors.email=? AND a.id=debts.activity AND debts.debtor=debtors.id ORDER BY activity_date DESC, activity_id ASC'); 115 | $get_credit = $db->prepare('SELECT debtors.id AS debtorid, credits.amount AS amount, credits.comment AS comment, credits.date AS date, credits.id AS creditid FROM debtors, credits WHERE debtors.owner=? AND debtors.email=? AND debtors.id=credits.debtor ORDER BY date DESC, creditid DESC'); 116 | 117 | $get_debt->execute(array($_SESSION['tabby_loggedin'], $_SESSION['tabby_loggedin'], $debtormail)); 118 | $get_credit->execute(array($_SESSION['tabby_loggedin'], $debtormail)); 119 | $finstate = get_debtor_financial_state($debtormail); 120 | $result = array(); 121 | 122 | while($row = $get_debt->fetch(PDO::FETCH_ASSOC)) { 123 | if(in_array($row['activity_id'], $finstate['red'])) { 124 | $row['color'] = 'red'; 125 | } 126 | elseif(in_array($row['activity_id'], $finstate['orange'])) { 127 | $row['color'] = 'orange'; 128 | } 129 | else { 130 | $row['color'] = 'neutral'; 131 | } 132 | if(!empty($result)) { 133 | $result['data'][] = array('id' => 'd' . $row['debtid'], 'date' => $row['activity_date'], 'sort' => $row['activity_date'] . '-1-' . $row['activity_id'] . $row['comment'], 'description' => $row['activity_name'] . ' - ' . $row['comment'], 'amount' => $row['amount'], 'color' => $row['color']); 134 | } 135 | else { 136 | $result = array('name' => $row['name'], 'email' => $row['email'], 'total' => $finstate['total'], 'more' => FALSE, 'data' => array()); 137 | $result['data'][] = array('id' => 'd' . $row['debtid'], 'date' => $row['activity_date'], 'sort' => $row['activity_date'] . '-1-' . $row['activity_id'] . $row['comment'], 'description' => $row['activity_name'] . ' - ' . $row['comment'], 'amount' => $row['amount'], 'color' => $row['color']); 138 | } 139 | } 140 | while($row = $get_credit->fetch(PDO::FETCH_ASSOC)) { 141 | $result['data'][] = array('id' => 'c' . $row['creditid'], 'date' => $row['date'], 'sort' => $row['date'] . '-2-' . $row['creditid'], 'description' => $row['comment'], 'amount' => $row['amount'], 'color' => 'green'); 142 | } 143 | usort($result['data'], function ($a, $b) { 144 | return (-1 * strcmp($a['sort'], $b['sort'])); 145 | }); 146 | //$result['data'] = array_reverse($result['data'], TRUE); 147 | return $result; 148 | } 149 | 150 | function get_transactions_per_user($debtormail) { 151 | global $db; 152 | $users = array(); 153 | $get_debt = $db->prepare('SELECT a.id AS activity_id, a.name AS activity_name, a.date AS activity_date, debts.comment AS comment, (debts.amount * -1) AS amount, debtors.owner AS owner, users.name AS name, users.email AS email, users.iban AS iban FROM activities AS a, debts, debtors, users WHERE debtors.email=? AND a.id=debts.activity AND debts.debtor=debtors.id AND debtors.owner=users.email ORDER BY activity_date DESC, activity_id ASC'); 154 | $get_credit = $db->prepare('SELECT credits.amount AS amount, credits.comment AS comment, credits.date AS date, credits.id AS creditid, debtors.owner AS owner FROM debtors, credits WHERE debtors.email=? AND debtors.id=credits.debtor ORDER BY date DESC, creditid DESC'); 155 | 156 | $get_debt->execute(array($debtormail)); 157 | $get_credit->execute(array($debtormail)); 158 | 159 | while($row = $get_debt->fetch(PDO::FETCH_ASSOC)) { 160 | if(isset($users[$row['owner']])) { 161 | if(count($users[$row['owner']]['data']) == 4) { // we won't display more than 4 records but just give a link for the details 162 | $users[$row['owner']]['more'] = TRUE; 163 | $users[$row['owner']]['total'] += $row['amount']; 164 | continue; 165 | } 166 | $users[$row['owner']]['data'][] = array('date' => $row['activity_date'], 'sort' => $row['activity_date'] . '-1-' . $row['activity_id'] . $row['comment'], 'description' => $row['activity_name'] . ' - ' . $row['comment'], 'amount' => $row['amount']); 167 | $users[$row['owner']]['total'] += $row['amount']; 168 | } 169 | else { 170 | $users[$row['owner']] = array('user' => $row['owner'], 'name' => $row['name'], 'email' => $row['email'], 'iban' => $row['iban'], 'total' => $row['amount'], 'more' => FALSE, 'data' => array()); 171 | $users[$row['owner']]['data'][] = array('date' => $row['activity_date'], 'sort' => $row['activity_date'] . '-1-' . $row['activity_id'] . $row['comment'], 'description' => $row['activity_name'] . ' - ' . $row['comment'], 'amount' => $row['amount']); 172 | } 173 | } 174 | while($row = $get_credit->fetch(PDO::FETCH_ASSOC)) { 175 | if(count($users[$row['owner']]['data']) == 4) { // we won't display more than 4 records but just give a link for the details 176 | $users[$row['owner']]['more'] = TRUE; 177 | } 178 | $users[$row['owner']]['data'][] = array('date' => $row['date'], 'sort' => $row['date'] . '-2-' . $row['creditid'], 'description' => $row['comment'], 'amount' => $row['amount']); 179 | $users[$row['owner']]['total'] += $row['amount']; 180 | } 181 | foreach($users as $key => $value) { 182 | usort($users[$key]['data'], function ($a, $b) { 183 | return (-1 * strcmp($a['sort'], $b['sort'])); 184 | }); 185 | $users[$key]['data'] = array_reverse(array_slice($users[$key]['data'], 0, 4, TRUE), true); 186 | } 187 | return $users; 188 | } 189 | 190 | function get_user_transactions_for_debtor($usermail, $debtormail) { 191 | global $db; 192 | $user = array(); 193 | $get_debt = $db->prepare('SELECT a.id AS activity_id, a.name AS activity_name, a.date AS activity_date, debts.comment AS comment, (debts.amount * -1) AS amount, debtors.owner AS owner, users.name AS name, users.email AS email, users.iban AS iban FROM activities AS a, debts, debtors, users WHERE users.email=? AND debtors.email=? AND a.id=debts.activity AND debts.debtor=debtors.id AND debtors.owner=users.email ORDER BY activity_date DESC, activity_id ASC'); 194 | $get_credit = $db->prepare('SELECT credits.amount AS amount, credits.comment AS comment, credits.date AS date, credits.id AS creditid, debtors.owner AS owner FROM debtors, credits WHERE debtors.owner=? AND debtors.email=? AND debtors.id=credits.debtor ORDER BY date DESC, creditid DESC'); 195 | 196 | $get_debt->execute(array($usermail, $debtormail)); 197 | $get_credit->execute(array($usermail, $debtormail)); 198 | 199 | while($row = $get_debt->fetch(PDO::FETCH_ASSOC)) { 200 | if(!empty($user)) { 201 | $user['data'][] = array('date' => $row['activity_date'], 'sort' => $row['activity_date'] . '-1-' . $row['activity_id'] . $row['comment'], 'description' => $row['activity_name'] . ' - ' . $row['comment'], 'amount' => $row['amount']); 202 | $user['total'] += $row['amount']; 203 | } 204 | else { 205 | $user = array('user' => $row['owner'], 'name' => $row['name'], 'email' => $row['email'], 'iban' => $row['iban'], 'total' => $row['amount'], 'data' => array()); 206 | $user['data'][] = array('date' => $row['activity_date'], 'sort' => $row['activity_date'] . '-1-' . $row['activity_id'] . $row['comment'], 'description' => $row['activity_name'] . ' - ' . $row['comment'], 'amount' => $row['amount']); 207 | } 208 | } 209 | while($row = $get_credit->fetch(PDO::FETCH_ASSOC)) { 210 | $user['data'][] = array('date' => $row['date'], 'sort' => $row['date'] . '-2-' . $row['creditid'], 'description' => $row['comment'], 'amount' => $row['amount']); 211 | $user['total'] += $row['amount']; 212 | } 213 | usort($user['data'], function ($a, $b) { 214 | return (-1 * strcmp($a['sort'], $b['sort'])); 215 | }); 216 | return $user; 217 | } 218 | 219 | function get_all_debtor_financial_state() { 220 | global $db; 221 | $get_debt = $db->prepare('SELECT debtors.id AS debtorid, debtors.email AS email, SUM(debts.amount) AS debt FROM debtors, debts WHERE debtors.owner=? AND debtors.id=debts.debtor GROUP BY debtorid ORDER BY debtorid'); 222 | $get_credit = $db->prepare('SELECT debtors.id AS debtorid, SUM(credits.amount) AS credit FROM debtors, credits WHERE debtors.owner=? AND debtors.id=credits.debtor GROUP BY debtorid ORDER BY debtorid'); 223 | $get_activities = $db->prepare('SELECT debts.activity AS id, debts.amount AS amount, activities.date AS date FROM debts, activities WHERE debts.debtor=? AND debts.activity=activities.id ORDER BY date DESC'); 224 | 225 | $get_debt->execute(array($_SESSION['tabby_loggedin'])); 226 | $get_credit->execute(array($_SESSION['tabby_loggedin'])); 227 | $credits = $get_credit->fetchAll(PDO::FETCH_KEY_PAIR); 228 | $results = array(); 229 | while($row = $get_debt->fetch(PDO::FETCH_ASSOC)) { 230 | if(isset($credits[$row['debtorid']])) { 231 | $total = $credits[$row['debtorid']] - $row['debt']; 232 | } 233 | else { 234 | $total = - $row['debt']; 235 | } 236 | if($total < 0) { 237 | $get_activities->execute(array($row['debtorid'])); 238 | $missing = -$total; 239 | $red = array(); //unpaid 240 | $orange = array(); //partially paid 241 | while($act = $get_activities->fetch(PDO::FETCH_ASSOC)) { 242 | if($act['amount'] > $missing) { 243 | $orange[] = $act['id']; 244 | break; 245 | } 246 | elseif($act['amount'] == $missing) { 247 | $red[] = $act['id']; 248 | break; 249 | } 250 | else { 251 | $red[] = $act['id']; 252 | $missing -= $act['amount']; 253 | } 254 | } 255 | $results[$row['debtorid']] = array('email' => $row['email'], 'total' => $total, 'red' => $red, 'orange' => $orange); 256 | } 257 | else { 258 | $results[$row['debtorid']] = array('email' => $row['email'], 'total' => $total, 'red' => array(), 'orange' => array()); 259 | } 260 | } 261 | return $results; 262 | } 263 | 264 | function get_debtor_financial_state($debtormail, $owner = NULL) { 265 | global $db; 266 | if(is_null($owner)) { 267 | $owner = $_SESSION['tabby_loggedin']; 268 | } 269 | $get_debt = $db->prepare('SELECT debtors.id AS debtorid, SUM(debts.amount) AS debt FROM debtors, debts WHERE debtors.owner=? AND debtors.email=? AND debtors.id=debts.debtor GROUP BY debtorid ORDER BY debtorid'); 270 | $get_credit = $db->prepare('SELECT debtors.id AS debtorid, SUM(credits.amount) AS credit FROM debtors, credits WHERE debtors.owner=? AND debtors.email=? AND debtors.id=credits.debtor GROUP BY debtorid ORDER BY debtorid'); 271 | $get_activities = $db->prepare('SELECT debts.activity AS id, debts.amount AS amount, activities.date AS date FROM debts, activities WHERE debts.debtor=? AND debts.activity=activities.id ORDER BY date DESC'); 272 | 273 | $get_debt->execute(array($owner, $debtormail)); 274 | $get_credit->execute(array($owner, $debtormail)); 275 | $debt = $get_debt->fetch(PDO::FETCH_ASSOC); 276 | $credit = $get_credit->fetch(PDO::FETCH_ASSOC); 277 | $debtorid = $debt['debtorid']; 278 | $debt = $debt['debt']; 279 | if(isset($credit['credit'])) { 280 | $credit = $credit['credit']; 281 | } 282 | else { 283 | $credit = 0; 284 | } 285 | $total = $credit - $debt; 286 | $result = array(); 287 | if($total < 0) { 288 | $get_activities->execute(array($debtorid)); 289 | $missing = -$total; 290 | $red = array(); //unpaid 291 | $orange = array(); //partially paid 292 | while($act = $get_activities->fetch(PDO::FETCH_ASSOC)) { 293 | if($act['amount'] > $missing) { 294 | $orange[] = $act['id']; 295 | break; 296 | } 297 | elseif($act['amount'] == $missing) { 298 | $red[] = $act['id']; 299 | break; 300 | } 301 | else { 302 | $red[] = $act['id']; 303 | $missing -= $act['amount']; 304 | } 305 | } 306 | $result = array('total' => $total, 'red' => $red, 'orange' => $orange); 307 | } 308 | else { 309 | $result = array('total' => $total, 'red' => array(), 'orange' => array()); 310 | } 311 | return $result; 312 | } 313 | 314 | function user_have_debtors_in_debt($usermail) { 315 | global $db; 316 | $get_debt = $db->prepare('SELECT debtors.id AS debtorid, SUM(debts.amount) AS debt FROM debtors, debts WHERE debtors.owner=? AND debtors.id=debts.debtor GROUP BY debtorid ORDER BY debtorid'); 317 | $get_credit = $db->prepare('SELECT debtors.id AS debtorid, SUM(credits.amount) AS credit FROM debtors, credits WHERE debtors.owner=? AND debtors.id=credits.debtor GROUP BY debtorid ORDER BY debtorid'); 318 | 319 | $get_debt->execute(array($usermail)); 320 | $get_credit->execute(array($usermail)); 321 | $credits = $get_credit->fetchAll(PDO::FETCH_KEY_PAIR); 322 | $results = array(); 323 | while($row = $get_debt->fetch(PDO::FETCH_ASSOC)) { 324 | if(isset($credits[$row['debtorid']])) { 325 | $total = $credits[$row['debtorid']] - $row['debt']; 326 | } 327 | else { 328 | $total = - $row['debt']; 329 | } 330 | if($total < 0) { 331 | return TRUE; 332 | } 333 | } 334 | return FALSE; 335 | } 336 | 337 | function add_credit($debtor, $comment, $amount, $date) { 338 | global $db; 339 | $insert = $db->prepare('INSERT INTO credits (debtor, comment, amount, date) VALUES (?,?,?,?)'); 340 | $insert->execute(array($debtor, $comment, $amount, $date)); 341 | } 342 | 343 | function del_debt($id) { 344 | global $db; 345 | $check = $db->prepare('SELECT count(*) FROM debts, debtors WHERE debts.id=? AND debtors.owner=? AND debtors.id=debts.debtor'); 346 | $check->execute(array($id, $_SESSION['tabby_loggedin'])); 347 | if($check->fetchColumn() > 0) { 348 | $del = $db->prepare('DELETE FROM debts WHERE id=?'); 349 | $del->execute(array($id)); 350 | return TRUE; 351 | } 352 | return FALSE; 353 | } 354 | 355 | function del_credit($id) { 356 | global $db; 357 | $check = $db->prepare('SELECT count(*) FROM credits, debtors WHERE credits.id=? AND debtors.owner=? AND debtors.id=credits.debtor'); 358 | $check->execute(array($id, $_SESSION['tabby_loggedin'])); 359 | if($check->fetchColumn() > 0) { 360 | $del = $db->prepare('DELETE FROM credits WHERE id=?'); 361 | $del->execute(array($id)); 362 | return TRUE; 363 | } 364 | return FALSE; 365 | } 366 | 367 | -------------------------------------------------------------------------------- /resources/uiblocks.php: -------------------------------------------------------------------------------- 1 | 5 |
6 |
7 |
8 |

9 |
10 |
11 |
12 | 17 |
18 |
19 |
20 |

21 |
22 |
23 |
24 | 29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | '; 41 | echo '
'; 42 | echo '
'; 43 | echo '
'; 44 | foreach($cards as $card) { 45 | if($type == 'people') { 46 | peoplecard($card); 47 | } 48 | elseif($type == 'user') { 49 | usercard($card, $extra); 50 | } 51 | else { //activity 52 | activitycard($card, $extra); 53 | } 54 | } 55 | echo '
'; 56 | echo '
'; 57 | echo '
'; 58 | echo ''; 59 | } 60 | 61 | function detailcard($card, $type) { 62 | echo '
'; 63 | echo '
'; 64 | echo '
'; 65 | if($type == 'people') { 66 | big_peoplecard($card); 67 | } 68 | elseif($type == 'user') { 69 | big_usercard($card); 70 | } 71 | else { 72 | big_activitycard($card); 73 | } 74 | echo '
'; 75 | echo '
'; 76 | echo '
'; 77 | } 78 | 79 | function activitycard($card, $debtors) { 80 | ?> 81 |
82 |
83 | 84 |
85 |
86 |
    87 | ' . human_friendly_amount($row['amount']) . '' . $row['name'] . ': ' . $row['comment'] . ''; 101 | } 102 | ?> 103 |
104 |
105 |
    106 |
  • 107 |
    108 |
    109 |
    110 | 118 |
    119 |
    120 | 121 |
    122 |
    123 | 124 |
    125 |
    126 |
    127 | 128 |
    129 |
    130 |
    131 | 132 |
    133 |
    134 |
    135 |
  • 136 |
137 | 145 |
146 | 151 |
152 |
153 | 154 |
155 |
156 |
    157 | Show all records'; 160 | } 161 | foreach($card['data'] as $row) { 162 | $badge = 'light'; 163 | if($row['color'] == 'red') { 164 | $badge = 'danger'; 165 | } 166 | elseif($row['color'] == 'orange') { 167 | $badge = 'warning'; 168 | } 169 | elseif($row['color'] == 'green') { 170 | $badge = 'success'; 171 | } 172 | 173 | echo '
  • ' . date('d M Y', strtotime($row['date'])) . ' ' . human_friendly_amount($row['amount']) . '' . $row['description'] . '
  • '; 174 | } 175 | ?> 176 |
177 |
178 |
    179 |
  • 180 |
    181 |
    182 |
    183 | 184 |
    185 |
    186 | 187 |
    188 |
    189 | 190 |
    191 |
    192 | 193 |
    194 |
    195 | 196 |
    197 |
    198 |
    199 |
  • 200 |
201 | 222 |
223 | 233 |
234 |
235 | 236 |
237 |
238 |
    239 | Show all records'; 242 | } 243 | foreach($card['data'] as $row) { 244 | echo '
  • ' . date('d M Y', strtotime($row['date'])) . ' ' . human_friendly_amount($row['amount']) . '' . $row['description'] . '
  • '; 245 | } 246 | ?> 247 |
248 |
249 | 270 |
271 | 276 |
277 |
278 | 279 |
280 |
281 |
    282 | ' . date('d M Y', strtotime($row['date'])) . ' ' . human_friendly_amount($row['amount']) . '' . $row['description'] . ''; 296 | } 297 | ?> 298 |
299 |
300 | 318 |
319 | 324 |
325 |
326 | 327 |
328 |
329 |
    330 | ' . human_friendly_amount($row['amount']) . '' . $row['name'] . ': ' . $row['comment'] . ''; 344 | } 345 | ?> 346 |
347 |
348 | 355 |
356 | 361 |
362 |
363 | 364 |
365 |
366 |
    367 | ' . date('d M Y', strtotime($row['date'])) . ' ' . human_friendly_amount($row['amount']) . '' . $row['description'] . ''; 370 | } 371 | ?> 372 |
373 |
374 | 395 |
396 | 0) { 403 | return '+' . $currency . strval($amount); 404 | } 405 | elseif($force_signed && $amount > 0) { 406 | return '+' . strval($amount); 407 | } 408 | elseif($with_currency && $amount < 0) { 409 | return '-' . $currency . strval(-$amount); 410 | } 411 | elseif($with_currency) { 412 | return $currency . strval($amount); 413 | } 414 | return strval($amount); 415 | } 416 | -------------------------------------------------------------------------------- /resources/users.php: -------------------------------------------------------------------------------- 1 | prepare('SELECT password FROM users WHERE email=?'); 6 | $get->execute(array($email)); 7 | $row = $get->fetch(); 8 | if(password_verify($password, $row['password'])) { 9 | if(password_needs_rehash($row['password'], PASSWORD_DEFAULT)) { 10 | change_password($email, $password); 11 | } 12 | return TRUE; 13 | } 14 | return FALSE; 15 | } 16 | 17 | function change_password($email, $newpassword) { 18 | global $db; 19 | $update = $db->prepare('UPDATE users SET password=? WHERE email=?'); 20 | $update->execute(array(password_hash($newpassword, PASSWORD_DEFAULT), $email)); 21 | } 22 | 23 | function update_user($email, $name, $iban) { 24 | global $db; 25 | $update = $db->prepare('UPDATE users SET name=?, iban=? WHERE email=?'); 26 | $update->execute(array($name, $iban, $email)); 27 | } 28 | 29 | function register_user($email, $name, $password, $iban) { 30 | global $db; 31 | global $base_url; 32 | global $application_email; 33 | 34 | $insert = $db->prepare('INSERT INTO pending_users VALUES (?,?,?,?,?,NOW())'); 35 | $confirm = str_rand(25); 36 | $insert->execute(array($email, $name, password_hash($password, PASSWORD_DEFAULT), $iban, $confirm)); 37 | 38 | $message = "Hi " . $name . ",\r\n\r\nYou have registered an account with a Tabby instance for debt management.\r\nPlease confirm your account by visiting " . $base_url . "confirm/" . $confirm . "\r\n\r\nHave a nice day!\r\n\r\nTabby"; 39 | $headers = 'From: ' . $application_email; 40 | mail($email, 'Tabby: please confirm your email address', $message, $headers); 41 | } 42 | 43 | function user_email_confirm($confirmation) { 44 | global $db; 45 | global $base_url; 46 | global $application_email; 47 | global $admin_email; 48 | 49 | $get = $db->prepare('SELECT count(*) FROM pending_users WHERE confirmation=?'); 50 | $get->execute(array($confirmation)); 51 | if($get->fetchColumn() > 0) { 52 | $newconfirm = str_rand(25); 53 | $update = $db->prepare('UPDATE pending_users SET confirmation=? WHERE confirmation=?'); 54 | $update->execute(array($newconfirm, $confirmation)); 55 | 56 | $message = "Hi there admin,\r\n\r\nAn account has been registered and confirmed for a new user.\r\nYou can confirm the account by visiting " . $base_url . "adminconfirm/" . $newconfirm . "\r\n\r\nHave a nice day!\r\n\r\nTabby"; 57 | $headers = 'From: ' . $application_email; 58 | mail($admin_email, 'Tabby: new confirmed user', $message, $headers); 59 | 60 | return TRUE; 61 | } 62 | } 63 | 64 | function user_admin_confirm($confirmation) { 65 | global $db; 66 | global $base_url; 67 | global $application_email; 68 | $get = $db->prepare('SELECT * FROM pending_users WHERE confirmation=?'); 69 | $get->execute(array($confirmation)); 70 | $pending = $get->fetch(PDO::FETCH_ASSOC); 71 | if(!empty($pending)) { 72 | $insert = $db->prepare('INSERT INTO users VALUES (?,?,?,?,?)'); 73 | $insert->execute(array($pending['email'], $pending['name'], $pending['password'], $pending['iban'], NULL)); 74 | $delete = $db->prepare('DELETE FROM pending_users WHERE confirmation=?'); 75 | $delete->execute(array($confirmation)); 76 | 77 | $message = "Hi " . $pending['name'] . ",\r\n\r\nYou registered a Tabby account on " . date('d M Y', strtotime($pending['datetime'])) . ". The admin of this instance has just confirmed it. So that means you can now get going.\r\n\r\nGo login at " . $base_url . " start tracking debt.\r\n\r\nHave a nice day!\r\n\r\nTabby"; 78 | $headers = 'From: ' . $application_email; 79 | mail($pending['email'], 'Tabby: the admin has confirmed your account', $message, $headers); 80 | 81 | return TRUE; 82 | } 83 | else { 84 | return FALSE; 85 | } 86 | } 87 | 88 | function user_exists($email) { 89 | global $db; 90 | $check_user = $db->prepare('SELECT count(*) FROM users WHERE email=?'); 91 | $check_pending = $db->prepare('SELECT count(*) FROM pending_users WHERE email=?'); 92 | $check_user->execute(array($email)); 93 | $check_pending->execute(array($email)); 94 | if($check_user->fetchColumn() > 0 OR $check_pending->fetchColumn() > 0) { 95 | return TRUE; 96 | } 97 | return FALSE; 98 | } 99 | 100 | function get_user_details($email = NULL) { 101 | global $db; 102 | if(is_null($email)) { 103 | $email = $_SESSION['tabby_loggedin']; 104 | } 105 | $get = $db->prepare('SELECT email, name, iban, reminddate FROM users WHERE email=?'); 106 | $get->execute(array($email)); 107 | return $get->fetch(PDO::FETCH_ASSOC); 108 | } 109 | 110 | function get_user_aliases() { 111 | global $db; 112 | $get = $db->prepare('SELECT email, unconfirmed FROM aliases WHERE owner=? ORDER BY email'); 113 | $get->execute(array($_SESSION['tabby_loggedin'])); 114 | return $get->fetchAll(PDO::FETCH_ASSOC); 115 | } 116 | 117 | function get_confirmed_aliases() { 118 | global $db; 119 | $get = $db->prepare('SELECT email FROm aliases WHERE owner=? AND unconfirmed IS NULL ORDER BY email'); 120 | $get->execute(array($_SESSION['tabby_loggedin'])); 121 | return $get->fetchAll(PDO::FETCH_COLUMN); 122 | } 123 | 124 | function add_user_alias($email) { 125 | global $db; 126 | $aliastoken = str_rand(25); 127 | $insert = $db->prepare('INSERT INTO aliases VALUES (?,?,?)'); 128 | $insert->execute(array($email, $_SESSION['tabby_loggedin'], $aliastoken)); 129 | email_alias_confirmation($email, $aliastoken); 130 | } 131 | 132 | function validate_user_alias($email) { 133 | global $db; 134 | global $application_email; 135 | global $admin_email; 136 | if($email == $application_email OR $email == $admin_email) { 137 | return FALSE; 138 | } 139 | $get = $db->prepare('SELECT count(*) AS count FROM aliases WHERE email=?'); 140 | $get->execute(array($email)); 141 | $result = $get->fetch(PDO::FETCH_ASSOC); 142 | if($result['count'] > 0) { 143 | return FALSE; 144 | } 145 | $get = $db->prepare('SELECT count(*) AS count FROM users WHERE email=?'); 146 | $get->execute(array($email)); 147 | $result = $get->fetch(PDO::FETCH_ASSOC); 148 | if($result['count'] > 0) { 149 | return FALSE; 150 | } 151 | $get = $db->prepare('SELECT count(*) AS count FROM pending_users WHERE email=?'); 152 | $get->execute(array($email)); 153 | $result = $get->fetch(PDO::FETCH_ASSOC); 154 | if($result['count'] > 0) { 155 | return FALSE; 156 | } 157 | return TRUE; 158 | } 159 | 160 | function confirm_user_alias($token) { 161 | global $db; 162 | $update = $db->prepare('UPDATE aliases SET unconfirmed=NULL WHERE unconfirmed=?'); 163 | $update->execute(array($token)); 164 | } 165 | 166 | function del_user_alias($email) { 167 | global $db; 168 | $del->prepare('DELETE FROM aliases WHERE email=? AND owner=?'); 169 | $del->execute(array($email, $_SESSION['tabby_loggedin'])); 170 | } 171 | 172 | function email_alias_confirmation($email, $aliastoken) { 173 | global $application_email; 174 | global $base_url; 175 | $message = "Hi there,\r\n\r\nThis email address has been added by " . $_SESSION['tabby_loggedin'] . " as an alias.\r\nIf that is correct, please confirm the alias by visiting " . $base_url . "aliasconfirm/" . $aliastoken . "\r\n\r\nHave a nice day!\r\n\r\nTabby"; 176 | $headers = 'From: ' . $application_email; 177 | mail($email, 'Tabby: please confirm your alias', $message, $headers); 178 | } 179 | 180 | function update_reminddate() { 181 | global $db; 182 | $update = $db->prepare('UPDATE users SET reminddate=NOW() WHERE email=?'); 183 | $update->execute(array($_SESSION['tabby_loggedin'])); 184 | } 185 | 186 | function get_pending_user_from_confirmation($confirmation) { 187 | global $db; 188 | $get = $db->prepare('SELECT * FROM pending_users WHERE confirmation=?'); 189 | $get->execute(array($confirmation)); 190 | return $get->fetch(PDO::FETCH_ASSOC); 191 | } 192 | 193 | function delete_pending_user($confirmation) { 194 | global $db; 195 | $delete = $db->prepare('DELETE FROM pending_users WHERE confirmation=?'); 196 | $delete->execute(array($confirmation)); 197 | } 198 | 199 | function get_users_by_reminddif($days) { 200 | global $db; 201 | $get = $db->prepare('SELECT email, name, reminddate FROM users WHERE reminddate IS NULL OR (DATEDIFF(reminddate, NOW()) % ? = 0 AND reminddate < CURDATE())'); 202 | $get->execute(array($days)); 203 | return $get->fetchAll(PDO::FETCH_ASSOC); 204 | } 205 | 206 | function str_rand($length) { 207 | $chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; 208 | $return = ''; 209 | for ($i = 0; $i < $length; $i++) { 210 | $return .= $chars[random_int(0, strlen($chars)-1)]; 211 | } 212 | return $return; 213 | } 214 | -------------------------------------------------------------------------------- /screenshots/screenshot_1_landing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bertvandepoel/tabby/3c429cf99b995b99bc1e88f88535d573516c7e67/screenshots/screenshot_1_landing.png -------------------------------------------------------------------------------- /screenshots/screenshot_2_install.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bertvandepoel/tabby/3c429cf99b995b99bc1e88f88535d573516c7e67/screenshots/screenshot_2_install.png -------------------------------------------------------------------------------- /screenshots/screenshot_3_people.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bertvandepoel/tabby/3c429cf99b995b99bc1e88f88535d573516c7e67/screenshots/screenshot_3_people.png -------------------------------------------------------------------------------- /screenshots/screenshot_4_activities.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bertvandepoel/tabby/3c429cf99b995b99bc1e88f88535d573516c7e67/screenshots/screenshot_4_activities.png -------------------------------------------------------------------------------- /screenshots/screenshot_5_reminder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bertvandepoel/tabby/3c429cf99b995b99bc1e88f88535d573516c7e67/screenshots/screenshot_5_reminder.png -------------------------------------------------------------------------------- /tabby-monochrome.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 22 | 24 | 49 | 51 | 52 | 54 | image/svg+xml 55 | 57 | 58 | 59 | 60 | 66 | 72 | 79 | 80 | 86 | 92 | 97 | 102 | 107 | 112 | 117 | 122 | 127 | 132 | 137 | 150 | 164 | 181 | 187 | 193 | 199 | 205 | 210 | 215 | 220 | 225 | 226 | 227 | -------------------------------------------------------------------------------- /tabby.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bertvandepoel/tabby/3c429cf99b995b99bc1e88f88535d573516c7e67/tabby.png -------------------------------------------------------------------------------- /tabby.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 22 | 24 | 49 | 51 | 52 | 54 | image/svg+xml 55 | 57 | 58 | 59 | 60 | 66 | 73 | 90 | 108 | 114 | 120 | 126 | 132 | 133 | 139 | 144 | 149 | 155 | 160 | 167 | 172 | 177 | 178 | 183 | 190 | 203 | 217 | 234 | 240 | 246 | 252 | 258 | 263 | 268 | 273 | 278 | 279 | 280 | -------------------------------------------------------------------------------- /templates/adminconfirm.php: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

Would you like to confirm this user?

5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
Name
Email
IBAN
Date
21 |
22 |
23 |
24 | 25 |
26 |
27 | 28 |
29 |
30 |
31 |
32 |
33 |
-------------------------------------------------------------------------------- /templates/box_config.php: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 | 7 |
8 |
9 | 10 |
11 |
12 |
13 |
14 |
-------------------------------------------------------------------------------- /templates/button_merge.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | Merge debt 5 |
6 |
7 |
-------------------------------------------------------------------------------- /templates/buttons.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 9 |
10 |

11 | Unpaid debtPartially paidCredit payment 12 |

13 |
14 |
15 |
-------------------------------------------------------------------------------- /templates/confirm_delete.php: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

Are you sure you want to delete this ?

5 |
6 |
7 |
8 |
9 | No, take me back 10 |
11 |
12 | 13 |
14 |
15 |
16 |
17 |
18 |
-------------------------------------------------------------------------------- /templates/emptynav.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /templates/error.php: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 7 |
8 |
9 |
-------------------------------------------------------------------------------- /templates/footer.php: -------------------------------------------------------------------------------- 1 | 2019) { 3 | $year = '2019 - ' . date('Y'); 4 | } 5 | else { 6 | $year = 2019; 7 | } 8 | ?> 9 |
10 |
11 |

Tabby © Bert Van de Poel, available as free software under the GNU AGPL License and available on GitHub.

12 |
13 |
14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /templates/form_activity.php: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

Add a new activity

5 |
6 |
7 | 8 |
9 | 10 |
11 |
12 |
13 | 14 |
15 | 16 |
17 |
18 | 19 |
20 |
21 |
22 |
23 |
24 | 25 | 28 |
29 |
30 |
31 |

Activity debt

32 |
33 |
34 |
35 | 43 |
44 | 45 |
46 | 47 |
48 |
49 |
50 |
51 |
52 | 53 |
54 |
55 |
56 |
57 | 58 |
59 |
60 |
61 | 62 |
63 |
64 |
65 |
66 | 67 |
68 |
69 |
70 | 78 |
79 | 80 |
81 | 82 |
83 |
84 |
85 |
86 |
87 | 88 |
89 |
90 |
91 | 92 | 93 | -------------------------------------------------------------------------------- /templates/form_install.php: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

Install Tabby

5 |

It seems there's currently no configuration file available, so Tabby was unable to connect to your database. Please enter your configuration details below so the correct file can be created for you or, if no write permissions are available, printed so you can install it yourself.

6 |
7 |

Database configuration

8 |
9 | Database software 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 |
39 |
40 | 41 |
42 | 43 |
44 |
45 |

Note: If you are using socket authentication based on the user running your PHP CGI or FPM, enter localhost as your host for MySQL or the location of the socket as host (e.g. /var/run/postgresql) for PostgreSQL, enter the username and leave password blank.

46 |

Email configuration

47 |
48 | 49 |
50 | 51 |
52 |
53 |
54 | 55 |
56 | 57 |
58 |
59 |

Please refer to the PHP documentation and adapt your php.ini or .user.ini if standard sendmail emailing isn't available.

60 |

Your account details

61 |
62 | 63 |
64 | 65 |
66 |
67 |
68 | 69 |
70 | 71 |
72 |
73 |
74 | 75 |
76 | 77 |
78 |
79 |
80 | 81 |
82 | 83 |
84 |
85 |

Note: It's only possible to approve or deny new users after logging in. This email address can however be different from the admin email address, e.g. when you work with a forwarder for several admins.

86 |

Other configuration

87 |
88 | 89 |
90 | 91 |
92 |
93 |
94 | 95 |
96 | 108 |
109 |
110 |
111 | 112 |
113 | 114 |
115 |
116 |
117 | Reminder cron 118 |
119 |
120 | > 121 | 122 |
123 |
124 | > 125 | 126 |
127 |
128 |
129 |

Keep in mind the days between reminders are to remind those who have an account to check their bank statements and then issue reminders through the interface. Tabby won't automatically remind people if the owner of the debt isn't checking their bank account to prevent spam.

130 |
131 |
132 | 133 |
134 |
135 |
136 |
137 |
138 |
139 | 140 | -------------------------------------------------------------------------------- /templates/form_people.php: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

Add a new contact to track debt for

5 |
6 |
7 | 8 |
9 | 10 |
11 |
12 |
13 | 14 |
15 | 16 |
17 |
18 | 19 |
20 |
21 |
22 |
-------------------------------------------------------------------------------- /templates/form_people_edit.php: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

Edit existing contact

5 |
6 |
7 | 8 |
9 | 10 |
11 |
12 |
13 | 14 |
15 | 16 |
17 |
18 | 19 |
20 |
21 |
22 |
-------------------------------------------------------------------------------- /templates/form_profile.php: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

Change your profile details

5 |
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 |

Alias management

31 |
32 |
33 | 34 |
35 | '; 38 | } 39 | else { 40 | foreach($filled['aliases'] as $alias) { 41 | echo '
  • ' . $alias['email']; 42 | if(!is_null($alias['unconfirmed'])) { 43 | echo ' '; 44 | } 45 | echo ' Delete
  • '; 46 | } 47 | } 48 | ?> 49 |
    50 |
    51 |
    52 |
    53 | 54 |
    55 |
    56 | 57 |
    58 |
    59 |
    60 |
    61 |
    62 |
    63 | 64 | -------------------------------------------------------------------------------- /templates/form_recurring.php: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 |

    Add a new recurring expense

    5 |
    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 | 39 | 40 |
    41 |
    42 |
    43 |
    44 | 45 |
    46 | 47 |
    48 |
    49 | 50 |
    51 |
    52 | 53 |
    54 | 55 |
    56 | 64 |
    65 |
    66 | 67 |
    68 |
    69 | 70 | 71 |
    72 |
    73 |
    74 |
    75 | 76 |
    77 |
    78 |
    79 | 87 |
    88 |
    89 |
    90 | 91 | 92 | 97 | -------------------------------------------------------------------------------- /templates/form_recurring_edit.php: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 |

    Edit recurring expense

    5 |
    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 | 45 |
    46 |
    47 | 48 |
    49 |
    50 | 51 | 52 |
    53 |
    54 | 67 |
    68 |
    69 | 70 | 71 | 72 |
    73 |
    74 |
    75 |
    76 | 77 |
    78 |
    79 |
    80 | 88 |
    89 |
    90 |
    91 | 92 | 93 | 98 | -------------------------------------------------------------------------------- /templates/form_remind.php: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 |

    Send reminder email

    5 |
    You've last sent a reminder to someone on .
    6 |
    7 |
    8 | 9 |
    10 | 18 |
    19 |
    20 |
    21 | 22 |
    23 | 24 |
    25 |
    26 | 27 |
    28 |
    29 |
    30 |
    -------------------------------------------------------------------------------- /templates/header.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Tabby - Friendly tool to manage debt 7 | 8 | '; } ?> 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 41 | 42 | -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |

    Tabby

    4 | Tabby is a tool to manage debt to friends. It automates splitting of bills and makes sure people pay back their meals. It also reminds whoever paid to check their bank account regularly. 5 |
    6 |
    7 | 8 | 9 |
    10 |
    11 |
    12 |
    13 |
    14 |

    Login

    15 |
    16 |
    17 |
    18 | 19 |
    20 | 21 |
    22 |
    23 |
    24 | 25 |
    26 | 27 |
    28 |
    29 | 30 |
    31 |
    32 |
    33 |
    34 |
    35 |
    36 |
    37 |
    38 | Register an account 39 |
    40 |
    41 |

    This is not a public service

    42 |

    This installation of Tabby is a private instance. You are free to register an account but the instance owner has to approve your account.

    43 | Register 44 |
    45 |
    46 |
    47 |
    48 |
    49 |
    50 |
    51 |

    Want to check whether you owe anyone? Request a link to see your debt or credit before reminder emails fill your mailbox.

    52 |
    53 |
    54 | 55 |
    56 | 57 |
    58 |
    59 |
    60 |
    61 |
    62 |
    63 |
    64 |
    65 |
    66 | -------------------------------------------------------------------------------- /templates/login.html: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 |
    5 |
    6 |

    Login

    7 |
    8 |
    9 |
    10 | 11 |
    12 | 13 |
    14 |
    15 |
    16 | 17 |
    18 | 19 |
    20 |
    21 | 22 |
    23 |
    24 |
    25 |
    26 |
    27 |
    28 |
    -------------------------------------------------------------------------------- /templates/nav.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /templates/register.html: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 |
    5 |
    6 |

    Register an account

    7 |

    This installation of Tabby is a private instance. You are free to register an account but the instance owner has to approve your account.

    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 |
    39 |
    40 |
    41 |
    -------------------------------------------------------------------------------- /templates/success.php: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 | 12 |
    13 |
    14 |
    15 | setTimeout("location.href = \'' . $redirect . '\';", 1000);'; 18 | } -------------------------------------------------------------------------------- /templates/table_merge.php: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 |

    Merge debt between users

    5 |

    If you and another user on this instance of Tabby both have debt with each other, it's possible to merge both debts. In that case, the largest debt will be topped off with the smallest debt, diminishing the smaller debt. This will be indicated by a credit line with the merge message.

    6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | $mergedetail) { 20 | echo ''; 21 | echo ''; 22 | echo ''; 23 | echo ''; 24 | echo ''; 25 | echo ''; 26 | echo ''; 27 | echo ''; 28 | echo ''; 29 | echo ''; 30 | } 31 | ?> 32 | 33 |
    ContactTheir debtYour debtWho has debt after merge?Merge messageMerge
    ' . $mergedetail['name'] . ' (' . $otheremail . ')' . human_friendly_amount($mergedetail['loggedin_debt']) . '' . human_friendly_amount($mergedetail['other_debt']) . '' . $mergedetail['debt_after_merge'] . '
    34 |
    35 |
    36 |
    -------------------------------------------------------------------------------- /templates/table_people.php: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 |

    List of all your contacts

    5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | '; 18 | echo ''; 19 | echo ''; 20 | echo ''; 21 | echo ''; 22 | echo ''; 23 | } 24 | ?> 25 | 26 |
    NameEmailEditDelete
    ' . $debtor['name'] . '' . $debtor['email'] . 'EditDelete
    27 | Add person 28 |
    29 |
    30 |
    -------------------------------------------------------------------------------- /templates/table_recurring.php: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 |

    Recurring expenses

    5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | '; 22 | echo ''; 23 | echo ''; 24 | echo ''; 25 | echo ''; 26 | echo ''; 27 | echo ''; 28 | echo ''; 29 | echo ''; 30 | echo ''; 31 | } 32 | ?> 33 | 34 |
    NameAmountFrequencyLast runNext runContactsEditDelete
    ' . $row['name'] . '' . human_friendly_amount($row['amount'], FALSE, TRUE) . '' . dateintervalstring_to_frequency($row['frequency']) . '' . ((is_null($row['lastrun'])) ? 'never' : $row['lastrun']) . '' . get_nextrun($row['start'], $row['frequency'], $row['lastrun']) . '' . implode(', ', $row['debtors']['name']) . 'EditDelete
    35 | Add new recurring expense 36 |
    37 |
    38 |
    -------------------------------------------------------------------------------- /templates/tokennav.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /upgrade.php: -------------------------------------------------------------------------------- 1 | prepare('SELECT value FROM config WHERE id=?'); 40 | $get_schema->execute(array('schema')); 41 | $result = $get_schema->fetch(PDO::FETCH_ASSOC); 42 | $schema = $result['value']; 43 | if(intval($schema) < 1) { 44 | $schema = 1; 45 | } 46 | 47 | switch ($schema) { 48 | case 1: 49 | echo "Upgrading database schema version 1 to version 2\n"; 50 | $db->query('ALTER TABLE activities CHANGE user owner VARCHAR(50);'); 51 | echo '.'; 52 | $db->query('ALTER TABLE activities MODIFY COLUMN id int;'); 53 | echo '.'; 54 | $db->query('ALTER TABLE debtors CHANGE user owner VARCHAR(50);'); 55 | echo '.'; 56 | $db->query('ALTER TABLE debtors MODIFY COLUMN id int;'); 57 | echo '.'; 58 | $db->query('ALTER TABLE credits MODIFY COLUMN id int;'); 59 | echo '.'; 60 | $db->query('ALTER TABLE credits MODIFY COLUMN debtor int;'); 61 | echo '.'; 62 | $db->query('ALTER TABLE credits MODIFY COLUMN amount int;'); 63 | echo '.'; 64 | $db->query('ALTER TABLE debts MODIFY COLUMN id int;'); 65 | echo '.'; 66 | $db->query('ALTER TABLE debts MODIFY COLUMN activity int;'); 67 | echo '.'; 68 | $db->query('ALTER TABLE debts MODIFY COLUMN debtor int;'); 69 | echo '.'; 70 | $db->query('ALTER TABLE debts MODIFY COLUMN amount int;'); 71 | echo '.'; 72 | $db->query('CREATE TABLE `config` ( `id` varchar(50) NOT NULL, `value` TEXT NOT NULL, PRIMARY KEY (`id`) );'); 73 | echo '.'; 74 | $db->query('INSERT INTO `config` VALUES (\'schema\', \'2\');'); 75 | echo '.'; 76 | $db->query('INSERT INTO `config` VALUES (\'cron\', \'0\');'); 77 | echo "\nAll queries have been executed. Database is now on schema version 2\n\n"; 78 | case 2: 79 | echo "Upgrading database schema version 2 to version 3\n"; 80 | if(strpos($dsn, 'mysql:') === 0) { 81 | $db->query('CREATE TABLE `recurring` ( `id` int NOT NULL AUTO_INCREMENT, `name` varchar(250) NOT NULL, `owner` varchar(50) NOT NULL, `amount` int NOT NULL, `start` date NOT NULL, `frequency` varchar(5) NOT NULL, `lastrun` date DEFAULT NULL, PRIMARY KEY (`id`), KEY (`owner`), KEY (`lastrun`), FOREIGN KEY (`owner`) REFERENCES users(email) );'); 82 | echo '.'; 83 | $db->query('CREATE TABLE `recurring_debtors` ( `recurringid` int NOT NULL, `debtor` int NOT NULL, PRIMARY KEY (`recurringid`, `debtor`), FOREIGN KEY (`recurringid`) REFERENCES recurring(id) ON DELETE CASCADE, FOREIGN KEY (`debtor`) REFERENCES debtors(id) );'); 84 | echo '.'; 85 | $db->query('CREATE TABLE `aliases` ( `email` varchar(50) NOT NULL, `owner` varchar(50) NOT NULL, `unconfirmed` varchar(25) NULL, KEY (`owner`), FOREIGN KEY (`owner`) REFERENCES users(email), UNIQUE KEY (`unconfirmed`) );'); 86 | echo '.'; 87 | } 88 | else { 89 | $db->query('CREATE TABLE "recurring" ( "id" serial, "name" varchar(250) NOT NULL, "owner" varchar(50) NOT NULL, "amount" integer NOT NULL, "start" date NOT NULL, "frequency" varchar(5) NOT NULL, "lastrun" date DEFAULT NULL, PRIMARY KEY ("id"), FOREIGN KEY ("owner") REFERENCES users(email) );'); 90 | echo '.'; 91 | $db->query('CREATE INDEX ON "recurring" ("owner");'); 92 | echo '.'; 93 | $db->query('CREATE INDEX ON "recurring" ("lastrun");'); 94 | echo '.'; 95 | $db->query('CREATE TABLE "recurring_debtors" ( "recurringid" integer NOT NULL, "debtor" integer NOT NULL, PRIMARY KEY ("recurringid", "debtor"), FOREIGN KEY ("recurringid") REFERENCES recurring(id) ON DELETE CASCADE, FOREIGN KEY ("debtor") REFERENCES debtors(id) );'); 96 | echo '.'; 97 | $db->query('CREATE TABLE "aliases" ( "email" varchar(50) NOT NULL, "owner" varchar(50) NOT NULL, "unconfirmed" varchar(25) NULL, FOREIGN KEY ("owner") REFERENCES users(email), UNIQUE ("unconfirmed") );'); 98 | echo '.'; 99 | $db->query('CREATE INDEX ON "aliases" ("owner");'); 100 | echo '.'; 101 | } 102 | $db->query('UPDATE config SET value=\'3\' WHERE id=\'schema\';'); 103 | echo '.'; 104 | echo "\nAll queries have been executed. Database is now on schema version 3\n\n"; 105 | } 106 | --------------------------------------------------------------------------------