diff --git a/app/Http/Controllers/BillController.php b/app/Http/Controllers/BillController.php new file mode 100644 index 0000000000..bc743f4b7b --- /dev/null +++ b/app/Http/Controllers/BillController.php @@ -0,0 +1,240 @@ +with('periods', $periods)->with('subTitle', 'Create new'); + } + + /** + * @param Bill $bill + * + * @return $this + */ + public function delete(Bill $bill) + { + return view('bills.delete')->with('bill', $bill)->with('subTitle', 'Delete "' . e($bill->name) . '"'); + } + + /** + * @param Bill $bill + * + * @return \Illuminate\Http\RedirectResponse + */ + public function destroy(Bill $bill) + { + $bill->delete(); + Session::flash('success', 'The bill was deleted.'); + + return Redirect::route('bills.index'); + + } + + /** + * @param Bill $bill + * + * @return $this + */ + public function edit(Bill $bill) + { + $periods = \Config::get('firefly.periods_to_text'); + + return View::make('bills.edit')->with('periods', $periods)->with('bill', $bill)->with('subTitle', 'Edit "' . e($bill->name) . '"'); + } + + /** + * @param BillRepositoryInterface $repository + * + * @return \Illuminate\View\View + */ + public function index(BillRepositoryInterface $repository) + { + $bills = Auth::user()->bills()->get(); + $bills->each( + function (Bill $bill) use ($repository) { + $bill->nextExpectedMatch = $repository->nextExpectedMatch($bill); + $last = $bill->transactionjournals()->orderBy('date', 'DESC')->first(); + $bill->lastFoundMatch = null; + if ($last) { + $bill->lastFoundMatch = $last->date; + } + } + ); + + return View::make('bills.index', compact('bills')); + } + + /** + * @param Bill $bill + * + * @return mixed + */ + public function rescan(Bill $bill, BillRepositoryInterface $repository) + { + if (intval($bill->active) == 0) { + Session::flash('warning', 'Inactive bills cannot be scanned.'); + + return Redirect::intended('/'); + } + + $set = \DB::table('transactions')->where('amount', '>', 0)->where('amount', '>=', $bill->amount_min)->where('amount', '<=', $bill->amount_max)->get(['transaction_journal_id']); + $ids = []; + + /** @var Transaction $entry */ + foreach ($set as $entry) { + $ids[] = intval($entry->transaction_journal_id); + } + if (count($ids) > 0) { + $journals = Auth::user()->transactionjournals()->whereIn('id',$ids)->get(); + /** @var TransactionJournal $journal */ + foreach ($journals as $journal) { + $repository->scan($bill, $journal); + } + } + + Session::flash('success', 'Rescanned everything.'); + + return Redirect::to(URL::previous()); + } + + /** + * @param Bill $bill + * + * @return mixed + */ + public function show(Bill $bill, BillRepositoryInterface $repository) + { + $journals = $bill->transactionjournals()->withRelevantData()->orderBy('date', 'DESC')->get(); + $bill->nextExpectedMatch = $repository->nextExpectedMatch($bill); + $hideBill = true; + + + return View::make('bills.show', compact('journals', 'hideBill', 'bill'))->with('subTitle', e($bill->name)); + } + + /** + * @return $this + */ + public function store(BillFormRequest $request, BillRepositoryInterface $repository) + { + + var_dump($request->all()); + + $billData = [ + 'name' => $request->get('name'), + 'match' => $request->get('match'), + 'amount_min' => floatval($request->get('amount_min')), + 'amount_currency_id' => floatval($request->get('amount_currency_id')), + 'amount_max' => floatval($request->get('amount_max')), + 'date' => new Carbon($request->get('date')), + 'user' => Auth::user()->id, + 'repeat_freq' => $request->get('repeat_freq'), + 'skip' => intval($request->get('skip')), + 'automatch' => intval($request->get('automatch')) === 1, + 'active' => intval($request->get('active')) === 1, + ]; + + $bill = $repository->store($billData); + Session::flash('success', 'Bill "' . e($bill->name) . '" stored.'); + + return Redirect::route('bills.index'); + + } + + /** + * @param Bill $bill + * + * @return $this + */ + public function update(Bill $bill, BillFormRequest $request, BillRepositoryInterface $repository) + { + $billData = [ + 'name' => $request->get('name'), + 'match' => $request->get('match'), + 'amount_min' => floatval($request->get('amount_min')), + 'amount_currency_id' => floatval($request->get('amount_currency_id')), + 'amount_max' => floatval($request->get('amount_max')), + 'date' => new Carbon($request->get('date')), + 'user' => Auth::user()->id, + 'repeat_freq' => $request->get('repeat_freq'), + 'skip' => intval($request->get('skip')), + 'automatch' => intval($request->get('automatch')) === 1, + 'active' => intval($request->get('active')) === 1, + ]; + + $bill = $repository->update($bill, $billData); + + Session::flash('success', 'Bill "' . e($bill->name) . '" updated.'); + + return Redirect::route('bills.index'); + + + // $data = Input::except('_token'); + // $data['active'] = intval(Input::get('active')); + // $data['automatch'] = intval(Input::get('automatch')); + // $data['user_id'] = Auth::user()->id; + // + // // always validate: + // $messages = $this->_repository->validate($data); + // + // // flash messages: + // Session::flash('warnings', $messages['warnings']); + // Session::flash('successes', $messages['successes']); + // Session::flash('errors', $messages['errors']); + // if ($messages['errors']->count() > 0) { + // Session::flash('error', 'Could not update bill: ' . $messages['errors']->first()); + // + // return Redirect::route('bills.edit', $bill->id)->withInput(); + // } + // + // // return to update screen: + // if ($data['post_submit_action'] == 'validate_only') { + // return Redirect::route('bills.edit', $bill->id)->withInput(); + // } + // + // // update + // $this->_repository->update($bill, $data); + // Session::flash('success', 'Bill "' . e($data['name']) . '" updated.'); + // + // // go back to list + // if ($data['post_submit_action'] == 'update') { + // return Redirect::route('bills.index'); + // } + // + // // go back to update screen. + // return Redirect::route('bills.edit', $bill->id)->withInput(['post_submit_action' => 'return_to_edit']); + + } + +} diff --git a/app/Http/Controllers/CategoryController.php b/app/Http/Controllers/CategoryController.php index 2eb257341e..2ef107071c 100644 --- a/app/Http/Controllers/CategoryController.php +++ b/app/Http/Controllers/CategoryController.php @@ -1,6 +1,7 @@ startOfMonth()); + $end = Session::get('end', Carbon::now()->startOfMonth()); + $list = Auth::user() + ->transactionjournals() + ->leftJoin('category_transaction_journal', 'category_transaction_journal.transaction_journal_id', '=', 'transaction_journals.id') + ->whereNull('category_transaction_journal.id') + ->before($end) + ->after($start) + ->orderBy('transaction_journals.date') + ->get(['transaction_journals.*']); + $subTitle = 'Transactions without a category in ' . $start->format('F Y'); + + return View::make('categories.noCategory', compact('list', 'subTitle')); + } + /** * @param Category $category * diff --git a/app/Http/Controllers/GoogleChartController.php b/app/Http/Controllers/GoogleChartController.php index 8eafd0d496..8b2f389799 100644 --- a/app/Http/Controllers/GoogleChartController.php +++ b/app/Http/Controllers/GoogleChartController.php @@ -12,6 +12,7 @@ use FireflyIII\Models\Bill; use FireflyIII\Models\Budget; use FireflyIII\Models\LimitRepetition; use FireflyIII\Models\TransactionJournal; +use FireflyIII\Models\Transaction; use FireflyIII\Models\Category; use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; use Grumpydictator\Gchart\GChart; @@ -21,6 +22,7 @@ use Preferences; use Response; use Session; use Steam; +use Navigation; /** * Class GoogleChartController @@ -73,6 +75,50 @@ class GoogleChartController extends Controller } + /** + * @param Bill $bill + * + * @return \Illuminate\Http\JsonResponse + */ + public function billOverview(Bill $bill, GChart $chart) + { + + $chart->addColumn('Date', 'date'); + $chart->addColumn('Max amount', 'number'); + $chart->addColumn('Min amount', 'number'); + $chart->addColumn('Current entry', 'number'); + + // get first transaction or today for start: + $first = $bill->transactionjournals()->orderBy('date', 'ASC')->first(); + if ($first) { + $start = $first->date; + } else { + $start = new Carbon; + } + $end = new Carbon; + while ($start <= $end) { + $result = $bill->transactionjournals()->before($end)->after($start)->first(); + if ($result) { + /** @var Transaction $tr */ + foreach($result->transactions()->get() as $tr) { + if(floatval($tr->amount) > 0) { + $amount = floatval($tr->amount); + } + } + } else { + $amount = 0; + } + unset($result); + $chart->addRow(clone $start, $bill->amount_max, $bill->amount_min, $amount); + $start = Navigation::addPeriod($start, $bill->repeat_freq, 0); + } + + $chart->generate(); + + return Response::json($chart->getData()); + + } + /** * @param Account $account diff --git a/app/Http/Controllers/PiggyBankController.php b/app/Http/Controllers/PiggyBankController.php index 556bf8f294..0ecb765ec5 100644 --- a/app/Http/Controllers/PiggyBankController.php +++ b/app/Http/Controllers/PiggyBankController.php @@ -67,4 +67,6 @@ class PiggyBankController extends Controller { return view('piggy-banks.index', compact('piggyBanks', 'accounts')); } + + } diff --git a/app/Http/Requests/BillFormRequest.php b/app/Http/Requests/BillFormRequest.php new file mode 100644 index 0000000000..2c9452cccb --- /dev/null +++ b/app/Http/Requests/BillFormRequest.php @@ -0,0 +1,55 @@ + 0) { + $nameRule .= ','.intval(Input::get('id')); + } + + $rules = [ + 'name' => $nameRule, + 'match' => 'required|between:1,255', + 'amount_min' => 'required|numeric|min:0.01', + 'amount_max' => 'required|numeric|min:0.01', + 'amount_currency_id' => 'required|exists:transaction_currencies,id', + 'date' => 'required|date', + 'repeat_freq' => 'required|in:weekly,monthly,quarterly,half-year,yearly', + 'skip' => 'required|between:0,31', + 'automatch' => 'in:1', + 'active' => 'in:1', + ]; + + return $rules; + } +} \ No newline at end of file diff --git a/app/Http/breadcrumbs.php b/app/Http/breadcrumbs.php index b202f82636..1f6c932318 100644 --- a/app/Http/breadcrumbs.php +++ b/app/Http/breadcrumbs.php @@ -4,6 +4,7 @@ use DaveJamesMiller\Breadcrumbs\Generator; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Models\Account; use FireflyIII\Models\Budget; +use FireflyIII\Models\Bill; use FireflyIII\Models\Category; use FireflyIII\Models\LimitRepetition; use FireflyIII\Models\PiggyBank; diff --git a/app/Http/routes.php b/app/Http/routes.php index 9301cc0ec8..f2d544d234 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -95,12 +95,16 @@ Route::group( * Bills Controller */ Route::get('/bills', ['uses' => 'BillController@index', 'as' => 'bills.index']); - //Route::get('/bills/rescan/{bill}', ['uses' => 'BillController@rescan', 'as' => 'bills.rescan']); # rescan for matching. + Route::get('/bills/rescan/{bill}', ['uses' => 'BillController@rescan', 'as' => 'bills.rescan']); # rescan for matching. Route::get('/bills/create', ['uses' => 'BillController@create', 'as' => 'bills.create']); - //Route::get('/bills/edit/{bill}', ['uses' => 'BillController@edit', 'as' => 'bills.edit']); - // Route::get('/bills/delete/{bill}', ['uses' => 'BillController@delete', 'as' => 'bills.delete']); + Route::get('/bills/edit/{bill}', ['uses' => 'BillController@edit', 'as' => 'bills.edit']); + Route::get('/bills/delete/{bill}', ['uses' => 'BillController@delete', 'as' => 'bills.delete']); Route::get('/bills/show/{bill}', ['uses' => 'BillController@show', 'as' => 'bills.show']); + Route::post('/bills/store', ['uses' => 'BillController@store', 'as' => 'bills.store']); + Route::post('/bills/update/{bill}', ['uses' => 'BillController@update', 'as' => 'bills.update']); + Route::post('/bills/destroy/{bill}', ['uses' => 'BillController@destroy', 'as' => 'bills.destroy']); + /** * Budget Controller */ @@ -155,7 +159,7 @@ Route::group( Route::get('/chart/reports/income-expenses/{year}', ['uses' => 'GoogleChartController@yearInExp']); Route::get('/chart/reports/income-expenses-sum/{year}', ['uses' => 'GoogleChartController@yearInExpSum']); - //Route::get('/chart/bills/{bill}', ['uses' => 'GoogleChartController@billOverview']); + Route::get('/chart/bills/{bill}', ['uses' => 'GoogleChartController@billOverview']); // JSON controller Route::get('/json/expense-accounts', ['uses' => 'JsonController@expenseAccounts', 'as' => 'json.expense-accounts']); diff --git a/app/Models/Bill.php b/app/Models/Bill.php index 63c0381554..7cdcc2e67c 100644 --- a/app/Models/Bill.php +++ b/app/Models/Bill.php @@ -10,6 +10,8 @@ use Illuminate\Database\Eloquent\Model; class Bill extends Model { + protected $fillable = ['name', 'match', 'amount_min','user_id', 'amount_max', 'date', 'repeat_freq', 'skip', 'automatch', 'active',]; + /** * @return array */ diff --git a/app/Providers/FireflyServiceProvider.php b/app/Providers/FireflyServiceProvider.php index 49c7c2907a..6d9a785905 100644 --- a/app/Providers/FireflyServiceProvider.php +++ b/app/Providers/FireflyServiceProvider.php @@ -60,6 +60,7 @@ class FireflyServiceProvider extends ServiceProvider $this->app->bind('FireflyIII\Repositories\Budget\BudgetRepositoryInterface', 'FireflyIII\Repositories\Budget\BudgetRepository'); $this->app->bind('FireflyIII\Repositories\Category\CategoryRepositoryInterface', 'FireflyIII\Repositories\Category\CategoryRepository'); $this->app->bind('FireflyIII\Repositories\Journal\JournalRepositoryInterface', 'FireflyIII\Repositories\Journal\JournalRepository'); + $this->app->bind('FireflyIII\Repositories\Bill\BillRepositoryInterface', 'FireflyIII\Repositories\Bill\BillRepository'); $this->app->bind('FireflyIII\Helpers\Report\ReportHelperInterface', 'FireflyIII\Helpers\Report\ReportHelper'); $this->app->bind('FireflyIII\Helpers\Report\ReportQueryInterface', 'FireflyIII\Helpers\Report\ReportQuery'); diff --git a/app/Repositories/Bill/BillRepository.php b/app/Repositories/Bill/BillRepository.php new file mode 100644 index 0000000000..3a5a2abf67 --- /dev/null +++ b/app/Repositories/Bill/BillRepository.php @@ -0,0 +1,198 @@ +active == 0) { + return $finalDate; + } + + /* + * $today is the start of the next period, to make sure FF3 won't miss anything + * when the current period has a transaction journal. + */ + $today = Navigation::addPeriod(new Carbon, $bill->repeat_freq, 0); + + $skip = $bill->skip + 1; + $start = Navigation::startOfPeriod(new Carbon, $bill->repeat_freq); + /* + * go back exactly one month/week/etc because FF3 does not care about 'next' + * bills if they're too far into the past. + */ + + $counter = 0; + while ($start <= $today) { + if (($counter % $skip) == 0) { + // do something. + $end = Navigation::endOfPeriod(clone $start, $bill->repeat_freq); + $journalCount = $bill->transactionjournals()->before($end)->after($start)->count(); + if ($journalCount == 0) { + $finalDate = clone $start; + break; + } + } + + // add period for next round! + $start = Navigation::addPeriod($start, $bill->repeat_freq, 0); + $counter++; + } + + return $finalDate; + } + + /** + * @param Bill $bill + * @param TransactionJournal $journal + * + * @return bool + */ + public function scan(Bill $bill, TransactionJournal $journal) + { + /* + * Match words. + */ + $wordMatch = false; + $matches = explode(',', $bill->match); + $description = strtolower($journal->description); + Log::debug('Now scanning ' . $description); + + /* + * Attach expense account to description for more narrow matching. + */ + if (count($journal->transactions) < 2) { + $transactions = $journal->transactions()->get(); + } else { + $transactions = $journal->transactions; + } + /** @var Transaction $transaction */ + foreach ($transactions as $transaction) { + /** @var Account $account */ + $account = $transaction->account()->first(); + /** @var AccountType $type */ + $type = $account->accountType()->first(); + if ($type->type == 'Expense account' || $type->type == 'Beneficiary account') { + $description .= ' ' . strtolower($account->name); + } + } + Log::debug('Final description: ' . $description); + Log::debug('Matches searched: ' . join(':', $matches)); + + $count = 0; + foreach ($matches as $word) { + if (!(strpos($description, strtolower($word)) === false)) { + $count++; + } + } + if ($count >= count($matches)) { + $wordMatch = true; + Log::debug('word match is true'); + } else { + Log::debug('Count: ' . $count.', count(matches): ' . count($matches)); + } + + + /* + * Match amount. + */ + + $amountMatch = false; + if (count($transactions) > 1) { + + $amount = max(floatval($transactions[0]->amount), floatval($transactions[1]->amount)); + $min = floatval($bill->amount_min); + $max = floatval($bill->amount_max); + if ($amount >= $min && $amount <= $max) { + $amountMatch = true; + Log::debug('Amount match is true!'); + } + } + + + /* + * If both, update! + */ + if ($wordMatch && $amountMatch) { + Log::debug('TOTAL match is true!'); + $journal->bill()->associate($bill); + $journal->save(); + } + } + + /** + * @param array $data + * + * @return Bill + */ + public function store(array $data) + { + + + $bill = Bill::create( + [ + 'name' => $data['name'], + 'match' => $data['match'], + 'amount_min' => $data['amount_min'], + 'user_id' => $data['user'], + 'amount_max' => $data['amount_max'], + 'date' => $data['date'], + 'repeat_freq' => $data['repeat_freq'], + 'skip' => $data['skip'], + 'automatch' => $data['automatch'], + 'active' => $data['active'], + + ] + ); + + return $bill; + } + + /** + * @param Bill $bill + * @param array $data + * + * @return Bill|static + */ + public function update(Bill $bill, array $data) + { + + + $bill->name = $data['name']; + $bill->match = $data['match']; + $bill->amount_min = $data['amount_min']; + $bill->amount_max = $data['amount_max']; + $bill->date = $data['date']; + $bill->repeat_freq = $data['repeat_freq']; + $bill->skip = $data['skip']; + $bill->automatch = $data['automatch']; + $bill->active = $data['active']; + $bill->save(); + + return $bill; + } +} diff --git a/app/Repositories/Bill/BillRepositoryInterface.php b/app/Repositories/Bill/BillRepositoryInterface.php new file mode 100644 index 0000000000..ead38877b1 --- /dev/null +++ b/app/Repositories/Bill/BillRepositoryInterface.php @@ -0,0 +1,51 @@ +label($name, $options); + $options = $this->expandOptionArray($name, $label, $options); + $classes = $this->getHolderClasses($name); + $value = $this->fillFieldValue($name, $value); + $options['step'] = '1'; + $html = \View::make('form.integer', compact('classes', 'name', 'label', 'value', 'options'))->render(); + + return $html; + + } + + + /** + * @param $name + * @param null $value + * @param array $options + * + * @return string + */ + public function tags($name, $value = null, array $options = []) + { + $label = $this->label($name, $options); + $options = $this->expandOptionArray($name, $label, $options); + $classes = $this->getHolderClasses($name); + $value = $this->fillFieldValue($name, $value); + $options['data-role'] = 'tagsinput'; + $html = \View::make('form.tags', compact('classes', 'name', 'label', 'value', 'options'))->render(); + + return $html; + } + /** * @param $name * @param null $value diff --git a/app/Support/Navigation.php b/app/Support/Navigation.php index 6c1bd66329..4bdec6aa1a 100644 --- a/app/Support/Navigation.php +++ b/app/Support/Navigation.php @@ -15,31 +15,93 @@ class Navigation /** - * @param Carbon $date - * @param $repeatFrequency + * @param Carbon $theDate + * @param $repeatFreq + * @param $skip * - * @return string + * @return \Carbon\Carbon * @throws FireflyException */ - public function periodShow(Carbon $date, $repeatFrequency) + public function addPeriod(Carbon $theDate, $repeatFreq, $skip) { - $formatMap = [ - 'daily' => 'j F Y', - 'week' => '\W\e\e\k W, Y', - 'weekly' => '\W\e\e\k W, Y', - 'quarter' => 'F Y', - 'month' => 'F Y', - 'monthly' => 'F Y', - 'year' => 'Y', - 'yearly' => 'Y', + $date = clone $theDate; + $add = ($skip + 1); + $functionMap = [ + 'daily' => 'addDays', + 'weekly' => 'addWeeks', + 'week' => 'addWeeks', + 'month' => 'addMonths', + 'monthly' => 'addMonths', + 'quarter' => 'addMonths', + 'quarterly' => 'addMonths', + 'half-year' => 'addMonths', + 'year' => 'addYears', + 'yearly' => 'addYears', ]; - if (isset($formatMap[$repeatFrequency])) { - return $date->format($formatMap[$repeatFrequency]); + $modifierMap = [ + 'quarter' => 3, + 'quarterly' => 3, + 'half-year' => 6, + ]; + if (!isset($functionMap[$repeatFreq])) { + throw new FireflyException('Cannot do addPeriod for $repeat_freq "' . $repeatFreq . '"'); } - throw new FireflyException('No date formats for frequency "' . $repeatFrequency . '"!'); + if (isset($modifierMap[$repeatFreq])) { + $add = $add * $modifierMap[$repeatFreq]; + } + $function = $functionMap[$repeatFreq]; + $date->$function($add); + + return $date; } + /** + * @param Carbon $theCurrentEnd + * @param $repeatFreq + * + * @return Carbon + * @throws FireflyException + */ + public function endOfPeriod(Carbon $theCurrentEnd, $repeatFreq) + { + $currentEnd = clone $theCurrentEnd; + + $functionMap = [ + 'daily' => 'addDay', + 'week' => 'addWeek', + 'weekly' => 'addWeek', + 'month' => 'addMonth', + 'monthly' => 'addMonth', + 'quarter' => 'addMonths', + 'quarterly' => 'addMonths', + 'half-year' => 'addMonths', + 'year' => 'addYear', + 'yearly' => 'addYear', + ]; + $modifierMap = [ + 'quarter' => 3, + 'quarterly' => 3, + 'half-year' => 6, + ]; + + $subDay = ['week', 'weekly', 'month', 'monthly', 'quarter', 'quarterly', 'half-year', 'year', 'yearly']; + + if (!isset($functionMap[$repeatFreq])) { + throw new FireflyException('Cannot do endOfPeriod for $repeat_freq ' . $repeatFreq); + } + $function = $functionMap[$repeatFreq]; + if (isset($modifierMap[$repeatFreq])) { + $currentEnd->$function($modifierMap[$repeatFreq]); + } else { + $currentEnd->$function(); + } + if (in_array($repeatFreq, $subDay)) { + $currentEnd->subDay(); + } + + return $currentEnd; + } /** * @param $range @@ -154,6 +216,72 @@ class Navigation throw new FireflyException('No _periodName() for range "' . $range . '"'); } + /** + * @param Carbon $date + * @param $repeatFrequency + * + * @return string + * @throws FireflyException + */ + public function periodShow(Carbon $date, $repeatFrequency) + { + $formatMap = [ + 'daily' => 'j F Y', + 'week' => '\W\e\e\k W, Y', + 'weekly' => '\W\e\e\k W, Y', + 'quarter' => 'F Y', + 'month' => 'F Y', + 'monthly' => 'F Y', + 'year' => 'Y', + 'yearly' => 'Y', + + ]; + if (isset($formatMap[$repeatFrequency])) { + return $date->format($formatMap[$repeatFrequency]); + } + throw new FireflyException('No date formats for frequency "' . $repeatFrequency . '"!'); + } + + /** + * @param Carbon $theDate + * @param $repeatFreq + * + * @return Carbon + * @throws FireflyException + */ + public function startOfPeriod(Carbon $theDate, $repeatFreq) + { + $date = clone $theDate; + + $functionMap = [ + 'daily' => 'startOfDay', + 'week' => 'startOfWeek', + 'weekly' => 'startOfWeek', + 'month' => 'startOfMonth', + 'monthly' => 'startOfMonth', + 'quarter' => 'firstOfQuarter', + 'quartly' => 'firstOfQuarter', + 'year' => 'startOfYear', + 'yearly' => 'startOfYear', + ]; + if (isset($functionMap[$repeatFreq])) { + $function = $functionMap[$repeatFreq]; + $date->$function(); + + return $date; + } + if ($repeatFreq == 'half-year') { + $month = intval($date->format('m')); + $date->startOfYear(); + if ($month >= 7) { + $date->addMonths(6); + } + + return $date; + } + throw new FireflyException('Cannot do startOfPeriod for $repeat_freq ' . $repeatFreq); + } + /** * @param $range * @param Carbon $start @@ -224,47 +352,5 @@ class Navigation throw new FireflyException('updateStartDate cannot handle $range ' . $range); } - /** - * @param Carbon $theDate - * @param $repeatFreq - * @param $skip - * - * @return \Carbon\Carbon - * @throws FireflyException - */ - public function addPeriod(Carbon $theDate, $repeatFreq, $skip) - { - $date = clone $theDate; - $add = ($skip + 1); - - $functionMap = [ - 'daily' => 'addDays', - 'weekly' => 'addWeeks', - 'week' => 'addWeeks', - 'month' => 'addMonths', - 'monthly' => 'addMonths', - 'quarter' => 'addMonths', - 'quarterly' => 'addMonths', - 'half-year' => 'addMonths', - 'year' => 'addYears', - 'yearly' => 'addYears', - ]; - $modifierMap = [ - 'quarter' => 3, - 'quarterly' => 3, - 'half-year' => 6, - ]; - if (!isset($functionMap[$repeatFreq])) { - throw new FireflyException('Cannot do addPeriod for $repeat_freq "' . $repeatFreq . '"'); - } - if (isset($modifierMap[$repeatFreq])) { - $add = $add * $modifierMap[$repeatFreq]; - } - $function = $functionMap[$repeatFreq]; - $date->$function($add); - - return $date; - } - } \ No newline at end of file diff --git a/database/seeds/TestDataSeeder.php b/database/seeds/TestDataSeeder.php index b48e34dade..35bd3d8d9f 100644 --- a/database/seeds/TestDataSeeder.php +++ b/database/seeds/TestDataSeeder.php @@ -392,7 +392,7 @@ class TestDataSeeder extends Seeder $user = User::whereEmail('thegrumpydictator@gmail.com')->first(); // bill Bill::create( - ['user_id' => $user->id, 'name' => 'Rent', 'match' => 'rent,landlord', 'amount_min' => 700, 'amount_max' => 900, 'date' => $this->som, + ['user_id' => $user->id, 'name' => 'Rent', 'match' => 'rent,land,lord', 'amount_min' => 700, 'amount_max' => 900, 'date' => $this->som, 'active' => 1, 'automatch' => 1, 'repeat_freq' => 'monthly', 'skip' => 0,] ); diff --git a/public/css/bootstrap-tagsinput.css b/public/css/bootstrap-tagsinput.css new file mode 100644 index 0000000000..8686f20a6b --- /dev/null +++ b/public/css/bootstrap-tagsinput.css @@ -0,0 +1,47 @@ +.bootstrap-tagsinput { + background-color: #fff; + border: 1px solid #ccc; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + display: inline-block; + padding: 4px 6px; + margin-bottom: 10px; + color: #555; + vertical-align: middle; + border-radius: 4px; + max-width: 100%; + width:100%; + line-height: 22px; + cursor: text; +} +.bootstrap-tagsinput input { + border: none; + box-shadow: none; + outline: none; + background-color: transparent; + padding: 0; + margin: 0; + width: auto !important; + max-width: inherit; +} +.bootstrap-tagsinput input:focus { + border: none; + box-shadow: none; +} +.bootstrap-tagsinput .tag { + margin-right: 2px; + color: white; +} +.bootstrap-tagsinput .tag [data-role="remove"] { + margin-left: 8px; + cursor: pointer; +} +.bootstrap-tagsinput .tag [data-role="remove"]:after { + content: "x"; + padding: 0px 2px; +} +.bootstrap-tagsinput .tag [data-role="remove"]:hover { + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); +} +.bootstrap-tagsinput .tag [data-role="remove"]:hover:active { + box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); +} diff --git a/public/js/bills.js b/public/js/bills.js new file mode 100644 index 0000000000..0bddb7f3ef --- /dev/null +++ b/public/js/bills.js @@ -0,0 +1,7 @@ +$(document).ready(function () { + + if (typeof(googleComboChart) === 'function' && typeof(billID) !== 'undefined') { + googleComboChart('chart/bills/' + billID, 'bill-overview'); + } + } +); \ No newline at end of file diff --git a/public/js/bootstrap-tagsinput.min.js b/public/js/bootstrap-tagsinput.min.js new file mode 100755 index 0000000000..16be0abb93 --- /dev/null +++ b/public/js/bootstrap-tagsinput.min.js @@ -0,0 +1,7 @@ +/* + * bootstrap-tagsinput v0.4.2 by Tim Schlechter + * + */ + +!function(a){"use strict";function b(b,c){this.itemsArray=[],this.$element=a(b),this.$element.hide(),this.isSelect="SELECT"===b.tagName,this.multiple=this.isSelect&&b.hasAttribute("multiple"),this.objectItems=c&&c.itemValue,this.placeholderText=b.hasAttribute("placeholder")?this.$element.attr("placeholder"):"",this.inputSize=Math.max(1,this.placeholderText.length),this.$container=a('
'),this.$input=a('').appendTo(this.$container),this.$element.after(this.$container);var d=(this.inputSize<3?3:this.inputSize)+"em";this.$input.get(0).style.cssText="width: "+d+" !important;",this.build(c)}function c(a,b){if("function"!=typeof a[b]){var c=a[b];a[b]=function(a){return a[c]}}}function d(a,b){if("function"!=typeof a[b]){var c=a[b];a[b]=function(){return c}}}function e(a){return a?i.text(a).html():""}function f(a){var b=0;if(document.selection){a.focus();var c=document.selection.createRange();c.moveStart("character",-a.value.length),b=c.text.length}else(a.selectionStart||"0"==a.selectionStart)&&(b=a.selectionStart);return b}function g(b,c){var d=!1;return a.each(c,function(a,c){if("number"==typeof c&&b.which===c)return d=!0,!1;if(b.which===c.which){var e=!c.hasOwnProperty("altKey")||b.altKey===c.altKey,f=!c.hasOwnProperty("shiftKey")||b.shiftKey===c.shiftKey,g=!c.hasOwnProperty("ctrlKey")||b.ctrlKey===c.ctrlKey;if(e&&f&&g)return d=!0,!1}}),d}var h={tagClass:function(){return"label label-info"},itemValue:function(a){return a?a.toString():a},itemText:function(a){return this.itemValue(a)},freeInput:!0,addOnBlur:!0,maxTags:void 0,maxChars:void 0,confirmKeys:[13,44],onTagExists:function(a,b){b.hide().fadeIn()},trimValue:!1,allowDuplicates:!1};b.prototype={constructor:b,add:function(b,c){var d=this;if(!(d.options.maxTags&&d.itemsArray.length>=d.options.maxTags||b!==!1&&!b)){if("string"==typeof b&&d.options.trimValue&&(b=a.trim(b)),"object"==typeof b&&!d.objectItems)throw"Can't add objects when itemValue option is not set";if(!b.toString().match(/^\s*$/)){if(d.isSelect&&!d.multiple&&d.itemsArray.length>0&&d.remove(d.itemsArray[0]),"string"==typeof b&&"INPUT"===this.$element[0].tagName){var f=b.split(",");if(f.length>1){for(var g=0;g+ +
++ Are you sure? +
+ ++ + Cancel +
++ +
++ Matching on + @foreach(explode(',',$bill->match) as $word) + {{{$word}}} + @endforeach + between {!! Amount::format($bill->amount_min) !!} and {!! Amount::format($bill->amount_max) !!}. + Repeats {!! $bill->repeat_freq !!}. | + +|
Next expected match | ++ @if($bill->nextExpectedMatch) + {{$bill->nextExpectedMatch->format('j F Y')}} + @else + Unknown + @endif + | +
+ | Name | +Matches on | +Matching amount | +Last seen match | +Next expected match | +Is active | +Will be automatched | +Repeats every | +
---|---|---|---|---|---|---|---|---|
+ + | ++ {{{$entry->name}}} + | ++ @foreach(explode(',',$entry->match) as $match) + {{{$match}}} + @endforeach + | ++ {!! Amount::format($entry->amount_min) !!} + — + {!! Amount::format($entry->amount_max) !!} + | ++ @if($entry->lastFoundMatch) + {{$entry->lastFoundMatch->format('j F Y')}} + @else + Unknown + @endif + | ++ @if($entry->nextExpectedMatch) + {{$entry->nextExpectedMatch->format('j F Y')}} + @else + Unknown + @endif + | ++ @if($entry->active) + + @else + + @endif + | ++ @if($entry->automatch) + + @else + + @endif + | ++ {{{$entry->repeat_freq}}} + @if($entry->skip > 0) + skips over {{$entry->skip}} + @endif + | +