added category new fields(code, description)

This commit is contained in:
Cihan Şentürk 2026-03-09 22:35:54 +03:00
parent 2c12e7524c
commit 2c4d52b15e
7 changed files with 242 additions and 29 deletions

View File

@ -6,10 +6,14 @@ use App\Abstracts\Http\Controller;
use App\Http\Requests\Setting\Category as Request; use App\Http\Requests\Setting\Category as Request;
use App\Jobs\Setting\CreateCategory; use App\Jobs\Setting\CreateCategory;
use App\Models\Setting\Category; use App\Models\Setting\Category;
use App\Traits\Categories as Helper;
use App\Traits\Modules;
use Illuminate\Http\Request as IRequest; use Illuminate\Http\Request as IRequest;
class Categories extends Controller class Categories extends Controller
{ {
use Helper, Modules;
/** /**
* Instantiate a new controller instance. * Instantiate a new controller instance.
*/ */
@ -29,11 +33,32 @@ class Categories extends Controller
*/ */
public function create(IRequest $request) public function create(IRequest $request)
{ {
$type = $request->get('type', 'item'); $type = $request->get('type', Category::ITEM_TYPE);
switch ($type) {
case Category::INCOME_TYPE:
$types = $this->getIncomeCategoryTypes();
break;
case Category::EXPENSE_TYPE:
$types = $this->getExpenseCategoryTypes();
break;
case Category::ITEM_TYPE:
$types = $this->getItemCategoryTypes();
break;
case Category::OTHER_TYPE:
$types = $this->getOtherCategoryTypes();
break;
default:
$types = [$type];
}
$categories = collect(); $categories = collect();
Category::type($type)->enabled()->orderBy('name')->get()->each(function ($category) use (&$categories) { Category::type($types)
->enabled()
->orderBy('name')
->get()
->each(function ($category) use (&$categories) {
$categories->push([ $categories->push([
'id' => $category->id, 'id' => $category->id,
'title' => $category->name, 'title' => $category->name,
@ -41,7 +66,9 @@ class Categories extends Controller
]); ]);
}); });
$html = view('modals.categories.create', compact('type', 'categories'))->render(); $has_code = $this->moduleIsEnabled('double-entry');
$html = view('modals.categories.create', compact('type', 'types', 'categories', 'has_code'))->render();
return response()->json([ return response()->json([
'success' => true, 'success' => true,
@ -61,7 +88,7 @@ class Categories extends Controller
public function store(Request $request) public function store(Request $request)
{ {
$request['enabled'] = 1; $request['enabled'] = 1;
$request['type'] = $request->get('type', 'income'); $request['type'] = $request->get('type', Category::ITEM_TYPE);
$request['color'] = $request->get('color', '#' . dechex(rand(0x000000, 0xFFFFFF))); $request['color'] = $request->get('color', '#' . dechex(rand(0x000000, 0xFFFFFF)));
$response = $this->ajaxDispatch(new CreateCategory($request)); $response = $this->ajaxDispatch(new CreateCategory($request));

View File

@ -4,11 +4,13 @@ namespace App\Models\Setting;
use App\Abstracts\Model; use App\Abstracts\Model;
use App\Builders\Category as Builder; use App\Builders\Category as Builder;
use App\Models\Banking\Transaction;
use App\Models\Document\Document; use App\Models\Document\Document;
use App\Interfaces\Export\WithParentSheet; use App\Interfaces\Export\WithParentSheet;
use App\Relations\HasMany\Category as HasMany; use App\Relations\HasMany\Category as HasMany;
use App\Scopes\Category as Scope; use App\Scopes\Category as Scope;
use App\Traits\Categories; use App\Traits\Categories;
use App\Traits\DateTime;
use App\Traits\Tailwind; use App\Traits\Tailwind;
use App\Traits\Transactions; use App\Traits\Transactions;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder; use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
@ -17,7 +19,7 @@ use Illuminate\Database\Eloquent\Model as EloquentModel;
class Category extends Model class Category extends Model
{ {
use Categories, HasFactory, Tailwind, Transactions; use Categories, HasFactory, Tailwind, Transactions, DateTime;
public const INCOME_TYPE = 'income'; public const INCOME_TYPE = 'income';
public const EXPENSE_TYPE = 'expense'; public const EXPENSE_TYPE = 'expense';
@ -33,14 +35,14 @@ class Category extends Model
* *
* @var array * @var array
*/ */
protected $fillable = ['company_id', 'name', 'type', 'color', 'enabled', 'created_from', 'created_by', 'parent_id']; protected $fillable = ['company_id', 'code', 'name', 'type', 'color', 'description', 'enabled', 'created_from', 'created_by', 'parent_id'];
/** /**
* Sortable columns. * Sortable columns.
* *
* @var array * @var array
*/ */
public $sortable = ['name', 'type', 'enabled']; public $sortable = ['code', 'name', 'type', 'enabled'];
/** /**
* The "booted" method of the model. * The "booted" method of the model.
@ -137,6 +139,18 @@ class Category extends Model
return $this->hasMany('App\Models\Banking\Transaction'); return $this->hasMany('App\Models\Banking\Transaction');
} }
/**
* Scope code.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param $code
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeCode($query, $code)
{
return $query->where('code', $code);
}
/** /**
* Scope to only include categories of a given type. * Scope to only include categories of a given type.
* *
@ -155,46 +169,50 @@ class Category extends Model
/** /**
* Scope to include only income. * Scope to include only income.
* Uses Categories trait to support multiple income types (e.g. from modules).
* *
* @param \Illuminate\Database\Eloquent\Builder $query * @param \Illuminate\Database\Eloquent\Builder $query
* @return \Illuminate\Database\Eloquent\Builder * @return \Illuminate\Database\Eloquent\Builder
*/ */
public function scopeIncome($query) public function scopeIncome($query)
{ {
return $query->where($this->qualifyColumn('type'), '=', 'income'); return $query->whereIn($this->qualifyColumn('type'), $this->getIncomeCategoryTypes());
} }
/** /**
* Scope to include only expense. * Scope to include only expense.
* Uses Categories trait to support multiple expense types (e.g. from modules).
* *
* @param \Illuminate\Database\Eloquent\Builder $query * @param \Illuminate\Database\Eloquent\Builder $query
* @return \Illuminate\Database\Eloquent\Builder * @return \Illuminate\Database\Eloquent\Builder
*/ */
public function scopeExpense($query) public function scopeExpense($query)
{ {
return $query->where($this->qualifyColumn('type'), '=', 'expense'); return $query->whereIn($this->qualifyColumn('type'), $this->getExpenseCategoryTypes());
} }
/** /**
* Scope to include only item. * Scope to include only item.
* Uses Categories trait to support multiple item types (e.g. from modules).
* *
* @param \Illuminate\Database\Eloquent\Builder $query * @param \Illuminate\Database\Eloquent\Builder $query
* @return \Illuminate\Database\Eloquent\Builder * @return \Illuminate\Database\Eloquent\Builder
*/ */
public function scopeItem($query) public function scopeItem($query)
{ {
return $query->where($this->qualifyColumn('type'), '=', 'item'); return $query->whereIn($this->qualifyColumn('type'), $this->getItemCategoryTypes());
} }
/** /**
* Scope to include only other. * Scope to include only other.
* Uses Categories trait to support multiple other types (e.g. from modules).
* *
* @param \Illuminate\Database\Eloquent\Builder $query * @param \Illuminate\Database\Eloquent\Builder $query
* @return \Illuminate\Database\Eloquent\Builder * @return \Illuminate\Database\Eloquent\Builder
*/ */
public function scopeOther($query) public function scopeOther($query)
{ {
return $query->where($this->qualifyColumn('type'), '=', 'other'); return $query->whereIn($this->qualifyColumn('type'), $this->getOtherCategoryTypes());
} }
public function scopeName($query, $name) public function scopeName($query, $name)
@ -213,6 +231,17 @@ class Category extends Model
return $query->withoutGlobalScope(new Scope); return $query->withoutGlobalScope(new Scope);
} }
/**
* Scope gets only parent categories.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeIsNotSubCategory($query)
{
return $query->whereNull('parent_id');
}
/** /**
* Scope to export the rows of the current page filtered and sorted. * Scope to export the rows of the current page filtered and sorted.
* *
@ -233,7 +262,7 @@ class Category extends Model
$search = $request->get('search'); $search = $request->get('search');
$query->withSubcategory(); $query->withSubCategory();
$query->usingSearchString($search)->sortable($sort); $query->usingSearchString($search)->sortable($sort);
@ -261,9 +290,92 @@ class Category extends Model
/** /**
* Get the display name of the category. * Get the display name of the category.
*/ */
public function getDisplayNameAttribute() public function getDisplayNameAttribute(): string
{ {
return $this->name . ' (' . ucfirst($this->type) . ')'; $typeConfig = config('type.category.' . $this->type, []);
$hideCode = isset($typeConfig['hide']) && in_array('code', $typeConfig['hide']);
$typeNames = $this->getCategoryTypes();
$typeName = $typeNames[$this->type] ?? ucfirst($this->type);
$prefix = (!$hideCode && $this->code) ? $this->code . ' - ' : '';
return $prefix . $this->name . ' (' . $typeName . ')';
}
/**
* Get the balance of a category.
*
* @return double
*/
public function getBalanceAttribute()
{
// If view composer has set the balance, return it directly
if (isset($this->de_balance)) {
return $this->de_balance;
}
$financial_year = $this->getFinancialYear();
$start_date = $financial_year->getStartDate();
$end_date = $financial_year->getEndDate();
$this->transactions->whereBetween('paid_at', [$start_date, $end_date])
->each(function ($transaction) use (&$incomes, &$expenses) {
if (($transaction->isNotIncome() && $transaction->isNotExpense()) || $transaction->isTransferTransaction()) {
return;
}
if ($transaction->isIncome()) {
$incomes += $transaction->getAmountConvertedToDefault();
} else {
$expenses += $transaction->getAmountConvertedToDefault();
}
});
$balance = $incomes - $expenses;
$this->sub_categories()
->each(function ($sub_category) use (&$balance) {
$balance += $sub_category->balance;
});
return $balance;
}
/**
* Get the balance of a category without considering sub categories.
*
* @return double
*/
public function getBalanceWithoutSubcategoriesAttribute()
{
// If view composer has set the balance, return it directly
if (isset($this->without_subcategory_de_balance)) {
return $this->without_subcategory_de_balance;
}
$financial_year = $this->getFinancialYear();
$start_date = $financial_year->getStartDate();
$end_date = $financial_year->getEndDate();
$this->transactions->whereBetween('paid_at', [$start_date, $end_date])
->each(function ($transaction) use (&$incomes, &$expenses) {
if (($transaction->isNotIncome() && $transaction->isNotExpense()) || $transaction->isTransferTransaction()) {
return;
}
if ($transaction->isIncome()) {
$incomes += $transaction->getAmountConvertedToDefault();
} else {
$expenses += $transaction->getAmountConvertedToDefault();
}
});
$balance = $incomes - $expenses;
return $balance;
} }
/** /**
@ -303,6 +415,19 @@ class Category extends Model
return $actions; return $actions;
} }
/**
* A no-op callback that gets fired when a model is cloning but before it gets
* committed to the database
*
* @param Illuminate\Database\Eloquent\Model $src
* @param boolean $child
* @return void
*/
public function onCloning($src, $child = null)
{
$this->code = $this->getNextCategoryCode();
}
/** /**
* Create a new factory instance for the model. * Create a new factory instance for the model.
* *

View File

@ -1,5 +1,7 @@
<?php <?php
use App\Models\Setting\Category;
return [ return [
/* /*
@ -184,7 +186,7 @@ return [
'payment_method', 'payment_method',
'reference', 'reference',
'category_id' => [ 'category_id' => [
'route' => ['categories.index', 'search=type:income,expense enabled:1'], 'route' => ['categories.index', 'search=type:' . Category::INCOME_TYPE . ',' . Category::EXPENSE_TYPE . ' enabled:1'],
'fields' => [ 'fields' => [
'key' => 'id', 'key' => 'id',
'value' => 'display_name', 'value' => 'display_name',
@ -246,7 +248,7 @@ return [
'description' => ['searchable' => true], 'description' => ['searchable' => true],
'enabled' => ['boolean' => true], 'enabled' => ['boolean' => true],
'category_id' => [ 'category_id' => [
'route' => ['categories.index', 'search=type:item enabled:1'], 'route' => ['categories.index', 'search=type:' . Category::ITEM_TYPE . ' enabled:1'],
'fields' => [ 'fields' => [
'key' => 'id', 'key' => 'id',
'value' => 'name', 'value' => 'name',
@ -352,7 +354,7 @@ return [
'contact_phone' => ['searchable' => true], 'contact_phone' => ['searchable' => true],
'contact_address' => ['searchable' => true], 'contact_address' => ['searchable' => true],
'category_id' => [ 'category_id' => [
'route' => ['categories.index', 'search=type:income,expense enabled:1'], 'route' => ['categories.index', 'search=type:' . Category::INCOME_TYPE . ',' . Category::EXPENSE_TYPE . ' enabled:1'],
'multiple' => true, 'multiple' => true,
], ],
'parent_id', 'parent_id',
@ -403,7 +405,7 @@ return [
'contact_phone' => ['searchable' => true], 'contact_phone' => ['searchable' => true],
'contact_address' => ['searchable' => true], 'contact_address' => ['searchable' => true],
'category_id' => [ 'category_id' => [
'route' => ['categories.index', 'search=type:expense enabled:1'], 'route' => ['categories.index', 'search=type:' . Category::EXPENSE_TYPE . ' enabled:1'],
'fields' => [ 'fields' => [
'key' => 'id', 'key' => 'id',
'value' => 'name', 'value' => 'name',
@ -459,7 +461,7 @@ return [
'contact_phone' => ['searchable' => true], 'contact_phone' => ['searchable' => true],
'contact_address' => ['searchable' => true], 'contact_address' => ['searchable' => true],
'category_id' => [ 'category_id' => [
'route' => ['categories.index', 'search=type:income enabled:1'], 'route' => ['categories.index', 'search=type:' . Category::INCOME_TYPE . ' enabled:1'],
'fields' => [ 'fields' => [
'key' => 'id', 'key' => 'id',
'value' => 'name', 'value' => 'name',
@ -480,6 +482,8 @@ return [
App\Models\Setting\Category::class => [ App\Models\Setting\Category::class => [
'columns' => [ 'columns' => [
'id', 'id',
'code' => ['searchable' => true],
'description' => ['searchable' => true],
'name' => ['searchable' => true], 'name' => ['searchable' => true],
'enabled' => ['boolean' => true], 'enabled' => ['boolean' => true],
'type' => [ 'type' => [

View File

@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up(): void
{
Schema::table('categories', function (Blueprint $table) {
$table->string('code')->nullable()->after('company_id');
$table->text('description')->nullable()->after('color');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down(): void
{
Schema::table('categories', function (Blueprint $table) {
$table->dropColumn('code');
$table->dropColumn('description');
});
}
};

View File

@ -2,11 +2,22 @@
<div class="grid sm:grid-cols-6 gap-x-8 gap-y-6 my-3.5"> <div class="grid sm:grid-cols-6 gap-x-8 gap-y-6 my-3.5">
<x-form.group.text name="name" label="{{ trans('general.name') }}" form-group-class="col-span-6" /> <x-form.group.text name="name" label="{{ trans('general.name') }}" form-group-class="col-span-6" />
@if ($has_code)
<x-form.group.text name="code" label="{{ trans('general.code') }}" form-group-class="col-span-6" />
@endif
<x-form.group.color name="color" label="{{ trans('general.color') }}" form-group-class="col-span-6" /> <x-form.group.color name="color" label="{{ trans('general.color') }}" form-group-class="col-span-6" />
<x-form.group.select name="parent_id" label="{{ trans('general.parent') . ' ' . trans_choice('general.categories', 1) }}" :options="$categories" not-required sort-options="false" searchable form-group-class="col-span-6" /> <x-form.group.select name="parent_id" label="{{ trans('general.parent') . ' ' . trans_choice('general.categories', 1) }}" :options="$categories" not-required sort-options="false" searchable form-group-class="col-span-6" />
@if (!empty($types) && count($types) > 1)
<x-form.group.select name="type" label="{{ trans_choice('general.types', 1) }}" :options="$types" value="{{ $type }}" form-group-class="col-span-6" />
@else
<x-form.input.hidden name="type" value="{{ $type }}" /> <x-form.input.hidden name="type" value="{{ $type }}" />
@endif
<x-form.group.textarea name="description" label="{{ trans('general.description') }}" not-required />
<x-form.input.hidden name="enabled" value="1" /> <x-form.input.hidden name="enabled" value="1" />
</div> </div>
</x-form> </x-form>

View File

@ -20,12 +20,18 @@
<x-slot name="body"> <x-slot name="body">
<x-form.group.text name="name" label="{{ trans('general.name') }}" /> <x-form.group.text name="name" label="{{ trans('general.name') }}" />
@if ($has_code)
<x-form.group.text name="code" label="{{ trans('general.code') }}" />
@endif
<x-form.group.color name="color" label="{{ trans('general.color') }}" /> <x-form.group.color name="color" label="{{ trans('general.color') }}" />
<x-form.group.select name="type" label="{{ trans_choice('general.types', 1) }}" :options="$types" :selected="config('general.types')" change="updateParentCategories" /> <x-form.group.select name="type" label="{{ trans_choice('general.types', 1) }}" :options="$types" :selected="config('general.types')" change="updateParentCategories" group />
<x-form.group.select name="parent_id" label="{{ trans('general.parent') . ' ' . trans_choice('general.categories', 1) }}" :options="[]" not-required dynamicOptions="categoriesBasedTypes" sort-options="false" v-disabled="selected_type" /> <x-form.group.select name="parent_id" label="{{ trans('general.parent') . ' ' . trans_choice('general.categories', 1) }}" :options="[]" not-required dynamicOptions="categoriesBasedTypes" sort-options="false" v-disabled="selected_type" />
<x-form.group.textarea name="description" label="{{ trans('general.description') }}" not-required />
<x-form.input.hidden name="categories" value="{{ json_encode($categories) }}" /> <x-form.input.hidden name="categories" value="{{ json_encode($categories) }}" />
</x-slot> </x-slot>
</x-form.section> </x-form.section>

View File

@ -14,20 +14,26 @@
<x-slot name="body"> <x-slot name="body">
<x-form.group.text name="name" label="{{ trans('general.name') }}" /> <x-form.group.text name="name" label="{{ trans('general.name') }}" />
@if ($has_code)
<x-form.group.text name="code" label="{{ trans('general.code') }}" />
@endif
<x-form.group.color name="color" label="{{ trans('general.color') }}" /> <x-form.group.color name="color" label="{{ trans('general.color') }}" />
@if ($type_disabled) @if ($type_disabled)
<x-form.group.select name="type" label="{{ trans_choice('general.types', 1) }}" :options="$types" v-disabled="true" /> <x-form.group.select name="type" label="{{ trans_choice('general.types', 1) }}" :options="$types" v-disabled="true" group />
<input type="hidden" name="type" value="{{ $category->type }}" /> <input type="hidden" name="type" value="{{ $category->type }}" />
@else @else
<x-form.group.select name="type" label="{{ trans_choice('general.types', 1) }}" :options="$types" change="updateParentCategories" /> <x-form.group.select name="type" label="{{ trans_choice('general.types', 1) }}" :options="$types" change="updateParentCategories" group />
<x-form.group.select name="parent_id" label="{{ trans('general.parent') . ' ' . trans_choice('general.categories', 1) }}" :options="$parent_categories" not-required dynamicOptions="categoriesBasedTypes" sort-options="false" /> <x-form.group.select name="parent_id" label="{{ trans('general.parent') . ' ' . trans_choice('general.categories', 1) }}" :options="$parent_categories" not-required dynamicOptions="categoriesBasedTypes" sort-options="false" />
<x-form.input.hidden name="parent_category_id" value="{{ $category->parent_id }}" /> <x-form.input.hidden name="parent_category_id" value="{{ $category->parent_id }}" />
<x-form.input.hidden name="categories" value="{{ json_encode($categories) }}" /> <x-form.input.hidden name="categories" value="{{ json_encode($categories) }}" />
@endif @endif
<x-form.group.textarea name="description" label="{{ trans('general.description') }}" not-required />
</x-slot> </x-slot>
</x-form.section> </x-form.section>