Working on expense categories

This commit is contained in:
Hillel Coren 2016-07-06 21:35:16 +03:00
parent 965ff2e252
commit cd9aae1e22
23 changed files with 487 additions and 41 deletions

View File

@ -0,0 +1,108 @@
<?php namespace App\Http\Controllers;
use View;
use Utils;
use Input;
use Session;
use App\Services\ExpenseCategoryService;
use App\Ninja\Repositories\ExpenseCategoryRepository;
use App\Http\Requests\ExpenseCategoryRequest;
use App\Http\Requests\CreateExpenseCategoryRequest;
use App\Http\Requests\UpdateExpenseCategoryRequest;
class ExpenseCategoryController extends BaseController
{
protected $categoryRepo;
protected $categoryService;
protected $entityType = ENTITY_EXPENSE_CATEGORY;
public function __construct(ExpenseCategoryRepository $categoryRepo, ExpenseCategoryService $categoryService)
{
$this->categoryRepo = $categoryRepo;
$this->categoryService = $categoryService;
}
/**
* Display a listing of the resource.
*
* @return Response
*/
public function index()
{
return View::make('list', [
'entityType' => ENTITY_EXPENSE_CATEGORY,
'title' => trans('texts.expense_categories'),
'sortCol' => '1',
'columns' => Utils::trans([
'checkbox',
'name',
''
]),
]);
}
public function getDatatable($expensePublicId = null)
{
return $this->categoryService->getDatatable(Input::get('sSearch'));
}
public function create(ExpenseCategoryRequest $request)
{
$data = [
'category' => null,
'method' => 'POST',
'url' => 'expense_categories',
'title' => trans('texts.new_category'),
];
return View::make('expense_categories.edit', $data);
}
public function edit(ExpenseCategoryRequest $request)
{
$category = $request->entity();
$data = [
'category' => $category,
'method' => 'PUT',
'url' => 'expense_categories/' . $category->public_id,
'title' => trans('texts.edit_category'),
];
return View::make('expense_categories.edit', $data);
}
public function store(CreateExpenseCategoryRequest $request)
{
$category = $this->categoryRepo->save($request->input());
Session::flash('message', trans('texts.created_expense_category'));
return redirect()->to($category->getRoute());
}
public function update(UpdateExpenseCategoryRequest $request)
{
$category = $this->categoryRepo->save($request->input(), $request->entity());
Session::flash('message', trans('texts.updated_expense_category'));
return redirect()->to($category->getRoute());
}
public function bulk()
{
$action = Input::get('action');
$ids = Input::get('public_id') ? Input::get('public_id') : Input::get('ids');
$count = $this->categoryService->bulk($ids, $action);
if ($count > 0) {
$field = $count == 1 ? "{$action}d_expense_category" : "{$action}d_expense_categories";
$message = trans("texts.$field", ['count' => $count]);
Session::flash('message', $message);
}
return redirect()->to('/expense_categories');
}
}

View File

@ -50,6 +50,7 @@ class ExpenseController extends BaseController
'client',
'expense_date',
'amount',
'category',
'public_notes',
'status',
''
@ -97,7 +98,7 @@ class ExpenseController extends BaseController
$expense = $request->entity();
$expense->expense_date = Utils::fromSqlDate($expense->expense_date);
$actions = [];
if ($expense->invoice) {
$actions[] = ['url' => URL::to("invoices/{$expense->invoice->public_id}/edit"), 'label' => trans('texts.view_invoice')];

View File

@ -0,0 +1,27 @@
<?php namespace App\Http\Requests;
class CreateExpenseCategoryRequest extends ExpenseCategoryRequest
{
// Expenses
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return $this->user()->is_admin;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'name' => 'required',
];
}
}

View File

@ -16,10 +16,14 @@ class EntityRequest extends Request {
// The entity id can appear as invoices, invoice_id, public_id or id
$publicId = false;
foreach (['_id', 's'] as $suffix) {
$field = $this->entityType . $suffix;
if ($this->$field) {
$publicId= $this->$field;
$field = $this->entityType . '_id';
if ( ! empty($this->$field)) {
$publicId = $this->$field;
}
if ( ! $publicId) {
$field = Utils::pluralizeEntityType($this->entityType);
if ( ! empty($this->$field)) {
$publicId = $this->$field;
}
}
if ( ! $publicId) {

View File

@ -0,0 +1,7 @@
<?php namespace App\Http\Requests;
class ExpenseCategoryRequest extends EntityRequest {
protected $entityType = ENTITY_EXPENSE_CATEGORY;
}

View File

@ -0,0 +1,26 @@
<?php namespace App\Http\Requests;
class UpdateExpenseCategoryRequest extends ExpenseCategoryRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return $this->user()->is_admin;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'name' => 'required',
];
}
}

View File

@ -195,6 +195,13 @@ Route::group(['middleware' => 'auth:user'], function() {
Route::get('api/expense', ['as'=>'api.expenses', 'uses'=>'ExpenseController@getDatatable']);
Route::get('api/vendor_expense/{id}', ['as'=>'api.expense', 'uses'=>'ExpenseController@getDatatableVendor']);
Route::post('expenses/bulk', 'ExpenseController@bulk');
Route::get('expense_categories', 'ExpenseCategoryController@index');
Route::get('api/expense_categories', ['as'=>'api.expense_categories', 'uses'=>'ExpenseCategoryController@getDatatable']);
Route::get('expense_categories/create', 'ExpenseCategoryController@create');
Route::post('expense_categories', 'ExpenseCategoryController@store');
Route::put('expense_categories/{expense_categories}', 'ExpenseCategoryController@update');
Route::get('expense_categories/{expense_categories}/edit', 'ExpenseCategoryController@edit');
Route::post('expense_categories/bulk', 'ExpenseCategoryController@bulk');
});
Route::group([
@ -365,7 +372,7 @@ if (!defined('CONTACT_EMAIL')) {
define('ENTITY_EXPENSE_ACTIVITY', 'expense_activity');
define('ENTITY_BANK_ACCOUNT', 'bank_account');
define('ENTITY_BANK_SUBACCOUNT', 'bank_subaccount');
define('ENTITY_EXPENSE_CATEGORIES', 'expense_categories');
define('ENTITY_EXPENSE_CATEGORY', 'expense_category');
define('INVOICE_TYPE_STANDARD', 1);
define('INVOICE_TYPE_QUOTE', 2);

View File

@ -374,6 +374,15 @@ class Utils
return $string;
}
public static function pluralizeEntityType($type)
{
if ($type === ENTITY_EXPENSE_CATEGORY) {
return 'expense_categories';
} else {
return $type . 's';
}
}
public static function maskAccountNumber($value)
{
$length = strlen($value);
@ -849,6 +858,7 @@ class Utils
}
// nouns in German and French should be uppercase
// TODO remove this
public static function transFlowText($key)
{
$str = trans("texts.$key");

View File

@ -47,7 +47,7 @@ class Expense extends EntityModel
*/
public function expense_category()
{
return $this->belongsTo('App\Models\ExpenseCategory');
return $this->belongsTo('App\Models\ExpenseCategory')->withTrashed();
}
/**

View File

@ -17,6 +17,14 @@ class ExpenseCategory extends EntityModel
'name',
];
/**
* @return mixed
*/
public function getEntityType()
{
return ENTITY_EXPENSE_CATEGORY;
}
/**
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
@ -25,4 +33,12 @@ class ExpenseCategory extends EntityModel
return $this->belongsTo('App\Models\Expense');
}
/**
* @return string
*/
public function getRoute()
{
return "/expense_categories/{$this->public_id}/edit";
}
}

View File

@ -0,0 +1,39 @@
<?php namespace App\Ninja\Datatables;
use Utils;
use URL;
use Auth;
class ExpenseCategoryDatatable extends EntityDatatable
{
public $entityType = ENTITY_EXPENSE_CATEGORY;
public function columns()
{
return [
[
'name',
function ($model)
{
return link_to("expense_categories/{$model->public_id}/edit", $model->category ?: '')->toHtml();
}
],
];
}
public function actions()
{
return [
[
trans('texts.edit_category'),
function ($model) {
return URL::to("expense_categories/{$model->public_id}/edit") ;
},
function ($model) {
return Auth::user()->is_admin;
}
],
];
}
}

View File

@ -66,6 +66,12 @@ class ExpenseDatatable extends EntityDatatable
}
}
],
[
'category',
function ($model) {
return $model->category != null ? substr($model->category, 0, 100) : '';
}
],
[
'public_notes',
function ($model) {
@ -123,7 +129,7 @@ class ExpenseDatatable extends EntityDatatable
$class = 'default';
} else {
$label = trans('texts.paid');
$class = 'success';
$class = 'success';
}
} elseif ($shouldBeInvoiced) {
$label = trans('texts.pending');

View File

@ -176,6 +176,7 @@ class AccountRepository
ENTITY_QUOTE,
ENTITY_TASK,
ENTITY_EXPENSE,
ENTITY_EXPENSE_CATEGORY,
ENTITY_VENDOR,
ENTITY_RECURRING_INVOICE,
ENTITY_PAYMENT,
@ -185,11 +186,11 @@ class AccountRepository
foreach ($entityTypes as $entityType) {
$features[] = [
"new_{$entityType}",
"/{$entityType}s/create",
Utils::pluralizeEntityType($entityType) . '/create'
];
$features[] = [
"list_{$entityType}s",
"/{$entityType}s",
'list_' . Utils::pluralizeEntityType($entityType),
Utils::pluralizeEntityType($entityType)
];
}

View File

@ -0,0 +1,51 @@
<?php namespace App\Ninja\Repositories;
use DB;
use Utils;
use Auth;
use App\Models\ExpenseCategory;
class ExpenseCategoryRepository extends BaseRepository
{
public function getClassName()
{
return 'App\Models\ExpenseCategory';
}
public function find($filter = null)
{
$query = DB::table('expense_categories')
->where('expense_categories.account_id', '=', Auth::user()->account_id)
->select(
'expense_categories.name as category',
'expense_categories.public_id',
'expense_categories.deleted_at'
);
if (!\Session::get('show_trash:expense_category')) {
$query->where('expense_categories.deleted_at', '=', null);
}
if ($filter) {
$query->where(function ($query) use ($filter) {
$query->where('expense_categories.name', 'like', '%'.$filter.'%');
});
}
return $query;
}
public function save($input, $category = false)
{
$publicId = isset($data['public_id']) ? $data['public_id'] : false;
if ( ! $category) {
$category = ExpenseCategory::createNew();
}
$category->fill($input);
$category->save();
return $category;
}
}

View File

@ -49,6 +49,7 @@ class ExpenseRepository extends BaseRepository
->leftJoin('contacts', 'contacts.client_id', '=', 'clients.id')
->leftjoin('vendors', 'vendors.id', '=', 'expenses.vendor_id')
->leftJoin('invoices', 'invoices.id', '=', 'expenses.invoice_id')
->leftJoin('expense_categories', 'expenses.expense_category_id', '=', 'expense_categories.id')
->where('expenses.account_id', '=', $accountid)
->where('contacts.deleted_at', '=', null)
->where('vendors.deleted_at', '=', null)
@ -75,6 +76,7 @@ class ExpenseRepository extends BaseRepository
'expenses.expense_currency_id',
'expenses.invoice_currency_id',
'expenses.user_id',
'expense_categories.name as category',
'invoices.public_id as invoice_public_id',
'invoices.user_id as invoice_user_id',
'invoices.balance',
@ -100,7 +102,8 @@ class ExpenseRepository extends BaseRepository
$query->where(function ($query) use ($filter) {
$query->where('expenses.public_notes', 'like', '%'.$filter.'%')
->orWhere('clients.name', 'like', '%'.$filter.'%')
->orWhere('vendors.name', 'like', '%'.$filter.'%');
->orWhere('vendors.name', 'like', '%'.$filter.'%')
->orWhere('expense_categories.name', 'like', '%'.$filter.'%');;
});
}

View File

@ -33,7 +33,7 @@ class AccountTransformer extends EntityTransformer
public function includeExpenseCategories(Account $account)
{
$transformer = new ExpenseCategoryTransformer($account, $this->serializer);
return $this->includeCollection($account->expense_categories, $transformer, ENTITY_EXPENSE_CATEGORIES);
return $this->includeCollection($account->expense_categories, $transformer, 'expense_categories');
}
/**

View File

@ -0,0 +1,5 @@
<?php
namespace App\Policies;
class ExpenseCategoryPolicy extends EntityPolicy {}

View File

@ -17,6 +17,7 @@ class AuthServiceProvider extends ServiceProvider
\App\Models\Credit::class => \App\Policies\CreditPolicy::class,
\App\Models\Document::class => \App\Policies\DocumentPolicy::class,
\App\Models\Expense::class => \App\Policies\ExpensePolicy::class,
\App\Models\ExpenseCategory::class => \App\Policies\ExpenseCategoryPolicy::class,
\App\Models\Invoice::class => \App\Policies\InvoicePolicy::class,
\App\Models\Payment::class => \App\Policies\PaymentPolicy::class,
\App\Models\Task::class => \App\Policies\TaskPolicy::class,
@ -40,7 +41,7 @@ class AuthServiceProvider extends ServiceProvider
foreach (get_class_methods(new \App\Policies\GenericEntityPolicy) as $method) {
$gate->define($method, "App\Policies\GenericEntityPolicy@{$method}");
}
$this->registerPolicies($gate);
}
}
}

View File

@ -0,0 +1,66 @@
<?php namespace App\Services;
use Utils;
use Auth;
use App\Ninja\Repositories\ExpenseCategoryRepository;
use App\Ninja\Datatables\ExpenseCategoryDatatable;
/**
* Class ExpenseCategoryService
*/
class ExpenseCategoryService extends BaseService
{
/**
* @var ExpenseCategoryRepository
*/
protected $categoryRepo;
/**
* @var DatatableService
*/
protected $datatableService;
/**
* CreditService constructor.
*
* @param ExpenseCategoryRepository $creditRepo
* @param DatatableService $datatableService
*/
public function __construct(ExpenseCategoryRepository $categoryRepo, DatatableService $datatableService)
{
$this->categoryRepo = $categoryRepo;
$this->datatableService = $datatableService;
}
/**
* @return CreditRepository
*/
protected function getRepo()
{
return $this->categoryRepo;
}
/**
* @param $data
* @return mixed|null
*/
public function save($data)
{
return $this->categoryRepo->save($data);
}
/**
* @param $clientPublicId
* @param $search
* @return \Illuminate\Http\JsonResponse
*/
public function getDatatable($search)
{
// we don't support bulk edit and hide the client on the individual client page
$datatable = new ExpenseCategoryDatatable();
$query = $this->categoryRepo->find($search);
return $this->datatableService->createDatatable($datatable, $query);
}
}

View File

@ -60,7 +60,7 @@ class ExpenseService extends BaseService
if ( ! empty($data['category'])) {
$name = trim($data['category']);
$category = ExpenseCategory::scope()->whereName($name)->first();
$category = ExpenseCategory::scope()->withTrashed()->whereName($name)->first();
if ( ! $category) {
$category = ExpenseCategory::createNew();
$category->name = $name;

View File

@ -2010,6 +2010,18 @@ $LANG = array(
'all' => 'All',
'selected' => 'Selected',
'category' => 'Category',
'categories' => 'Categories',
'new_expense_category' => 'New Expense Category',
'edit_category' => 'Edit Category',
'archive_expense_category' => 'Archive Category',
'expense_categories' => 'Expense Categories',
'list_expense_categories' => 'List Expense Categories',
'updated_expense_category' => 'Successfully updated expense category',
'created_expense_category' => 'Successfully created expense category',
'archived_expense_category' => 'Successfully archived expense category',
'archived_expense_categories' => 'Successfully archived :count expense category',
'restore_expense_category' => 'Restore expense category',
'restored_expense_category' => 'Successfully restored expense category',
);

View File

@ -0,0 +1,51 @@
@extends('header')
@section('content')
{!! Former::open($url)
->addClass('col-md-10 col-md-offset-1 warn-on-exit')
->method($method)
->rules([
'name' => 'required',
]) !!}
@if ($category)
{!! Former::populate($category) !!}
@endif
<span style="display:none">
{!! Former::text('public_id') !!}
</span>
<div class="row">
<div class="col-md-10 col-md-offset-1">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">{!! trans('texts.category') !!}</h3>
</div>
<div class="panel-body">
{!! Former::text('name') !!}
</div>
</div>
</div>
</div>
<center class="buttons">
{!! Button::normal(trans('texts.cancel'))->large()->asLinkTo(url('/expense_categories'))->appendIcon(Icon::create('remove-circle')) !!}
{!! Button::success(trans('texts.save'))->submit()->large()->appendIcon(Icon::create('floppy-disk')) !!}
</center>
{!! Former::close() !!}
<script>
$(function() {
$('#name').focus();
});
</script>
@stop

View File

@ -2,7 +2,7 @@
@section('content')
{!! Former::open($entityType . 's/bulk')->addClass('listForm') !!}
{!! Former::open(Utils::pluralizeEntityType($entityType) . '/bulk')->addClass('listForm') !!}
<div style="display:none">
{!! Former::text('action') !!}
@ -18,37 +18,42 @@
@endif
@endcan
{!! DropdownButton::normal(trans('texts.archive'))->withContents([
['label' => trans('texts.archive_'.$entityType), 'url' => 'javascript:submitForm("archive")'],
['label' => trans('texts.delete_'.$entityType), 'url' => 'javascript:submitForm("delete")'],
])->withAttributes(['class'=>'archive'])->split() !!}
@if ($entityType == ENTITY_EXPENSE_CATEGORY)
{!! Button::normal(trans('texts.archive'))->asLinkTo('javascript:submitForm("archive")')->appendIcon(Icon::create('trash')) !!}
@else
{!! DropdownButton::normal(trans('texts.archive'))->withContents([
['label' => trans('texts.archive_'.$entityType), 'url' => 'javascript:submitForm("archive")'],
['label' => trans('texts.delete_'.$entityType), 'url' => 'javascript:submitForm("delete")'],
])->withAttributes(['class'=>'archive'])->split() !!}
@endif
&nbsp;<label for="trashed" style="font-weight:normal; margin-left: 10px;">
<input id="trashed" type="checkbox" onclick="setTrashVisible()"
{{ Session::get("show_trash:{$entityType}") ? 'checked' : ''}}/>&nbsp; {{ trans('texts.show_archived_deleted')}} {{ Utils::transFlowText($entityType.'s') }}
<input id="trashed" type="checkbox" onclick="setTrashVisible()"
{{ Session::get("show_trash:{$entityType}") ? 'checked' : ''}}/>&nbsp; {{ trans('texts.show_archived_deleted')}}
</label>
<div id="top_right_buttons" class="pull-right">
<input id="tableFilter" type="text" style="width:140px;margin-right:17px;background-color: white !important"
<input id="tableFilter" type="text" style="width:140px;margin-right:17px;background-color: white !important"
class="form-control pull-left" placeholder="{{ trans('texts.filter') }}" value="{{ Input::get('filter') }}"/>
@if (Auth::user()->hasFeature(FEATURE_QUOTES) && $entityType == ENTITY_INVOICE)
{!! Button::normal(trans('texts.quotes'))->asLinkTo(URL::to('/quotes'))->appendIcon(Icon::create('list')) !!}
{!! Button::normal(trans('texts.recurring'))->asLinkTo(URL::to('/recurring_invoices'))->appendIcon(Icon::create('list')) !!}
@elseif ($entityType == ENTITY_EXPENSE)
{!! Button::normal(trans('texts.categories'))->asLinkTo(URL::to('/expense_categories'))->appendIcon(Icon::create('list')) !!}
{!! Button::normal(trans('texts.vendors'))->asLinkTo(URL::to('/vendors'))->appendIcon(Icon::create('list')) !!}
@elseif ($entityType == ENTITY_CLIENT)
{!! Button::normal(trans('texts.credits'))->asLinkTo(URL::to('/credits'))->appendIcon(Icon::create('list')) !!}
@endif
@if (Auth::user()->hasPermission('create_all'))
{!! Button::primary(trans("texts.new_$entityType"))->asLinkTo(URL::to("/{$entityType}s/create"))->appendIcon(Icon::create('plus-sign')) !!}
{!! Button::primary(trans("texts.new_{$entityType}"))->asLinkTo(url(Utils::pluralizeEntityType($entityType) . '/create'))->appendIcon(Icon::create('plus-sign')) !!}
@endif
</div>
{!! Datatable::table()
{!! Datatable::table()
->addColumn($columns)
->setUrl(route('api.' . $entityType . 's'))
->setUrl(route('api.' . Utils::pluralizeEntityType($entityType)))
->setCustomValues('rightAlign', isset($rightAlign) ? $rightAlign : [])
->setOptions('sPaginationType', 'bootstrap')
->setOptions('aaSorting', [[isset($sortCol) ? $sortCol : '1', 'desc']])
@ -94,13 +99,13 @@
function submitForm(action) {
if (action == 'delete') {
if (!confirm('{!! trans("texts.are_you_sure") !!}')) {
if (!confirm('{!! trans("texts.are_you_sure") !!}')) {
return;
}
}
}
$('#action').val(action);
$('form.listForm').submit();
$('form.listForm').submit();
}
function deleteEntity(id) {
@ -136,7 +141,7 @@
$('#public_id').val(id);
submitForm('invoice');
}
@if ($entityType == ENTITY_PAYMENT)
var paymentId = null;
function showRefundModal(id, amount, formatted, symbol){
@ -146,7 +151,7 @@
$('#refundAmount').val(amount).attr('max', amount);
$('#paymentRefundModal').modal('show');
}
function handleRefundClicked(){
$('#public_id').val(paymentId);
submitForm('refund');
@ -167,8 +172,8 @@
var searchTimeout = false;
var oTable0 = $('#DataTables_Table_0').dataTable();
var oTable1 = $('#DataTables_Table_1').dataTable();
function filterTable(val) {
var oTable1 = $('#DataTables_Table_1').dataTable();
function filterTable(val) {
if (val == tableFilter) {
return;
}
@ -190,10 +195,10 @@
filterTable($('#tableFilter').val());
}
window.onDatatableReady = function() {
window.onDatatableReady = function() {
$(':checkbox').click(function() {
setBulkActionsEnabled();
});
});
$('tbody tr').unbind('click').click(function(event) {
if (event.target.type !== 'checkbox' && event.target.type !== 'button' && event.target.tagName.toLowerCase() !== 'a') {
@ -206,7 +211,7 @@
actionListHandler();
}
@if ($entityType == ENTITY_PAYMENT)
$('#completeRefundButton').click(handleRefundClicked)
@endif
@ -223,7 +228,7 @@
function setBulkActionsEnabled() {
var buttonLabel = "{{ trans('texts.archive') }}";
var count = $('tbody :checkbox:checked').length;
$('button.archive, button.invoice').prop('disabled', !count);
$('button.archive, button.invoice').prop('disabled', !count);
if (count) {
buttonLabel += ' (' + count + ')';
}
@ -234,4 +239,4 @@
</script>
@stop
@stop