├── .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 | 
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 | 
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 | 
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 | 
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 | 
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 |
21 |
22 |
23 |
24 |
25 |
26 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
Sample application
42 |
43 |
44 |
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 |
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 |
21 |
22 |
23 |
24 |
25 |
26 |
33 |
34 |
35 |
36 |
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 |
86 |
87 |
88 |
Billing address
89 |
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 |
21 |
22 |
23 |
24 |
25 |
26 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
Bubblegum
44 |
This is a starter template with a jumbotron
45 |
46 |
58 |
59 |
60 |
61 |
62 |
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 |
12 |
17 |
24 |
29 |
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 |
85 |
86 |
87 | ' . human_friendly_amount($row['amount']) . ' ' . $row['name'] . ' : ' . $row['comment'] . '';
101 | }
102 | ?>
103 |
104 |
105 |
137 |
145 |
146 |
151 |
152 |
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 |
201 |
222 |
223 |
233 |
234 |
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 |
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 |
328 |
329 |
330 | ' . human_friendly_amount($row['amount']) . ' ' . $row['name'] . ' : ' . $row['comment'] . '';
344 | }
345 | ?>
346 |
347 |
348 |
355 |
356 |
361 |
362 |
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 | Name
9 |
10 |
11 | Email
12 |
13 |
14 | IBAN
15 |
16 |
17 | Date
18 |
19 |
20 |
21 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/templates/box_config.php:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/templates/button_merge.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/templates/buttons.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
11 | Unpaid debt Partially paid Credit payment
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/templates/confirm_delete.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
Are you sure you want to delete this ?
5 |
6 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/templates/emptynav.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/templates/error.php:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/templates/footer.php:
--------------------------------------------------------------------------------
1 | 2019) {
3 | $year = '2019 - ' . date('Y');
4 | }
5 | else {
6 | $year = 2019;
7 | }
8 | ?>
9 |
14 |
15 |
16 |
17 |