You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

AccountChart.php 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312
  1. <?php
  2. namespace App\Filament\Company\Pages\Accounting;
  3. use App\Enums\Accounting\AccountCategory;
  4. use App\Enums\Banking\BankAccountType;
  5. use App\Filament\Forms\Components\CreateCurrencySelect;
  6. use App\Models\Accounting\Account;
  7. use App\Models\Accounting\AccountSubtype;
  8. use App\Utilities\Accounting\AccountCode;
  9. use Filament\Actions\Action;
  10. use Filament\Actions\CreateAction;
  11. use Filament\Actions\EditAction;
  12. use Filament\Forms\Components\Checkbox;
  13. use Filament\Forms\Components\Component;
  14. use Filament\Forms\Components\Group;
  15. use Filament\Forms\Components\Select;
  16. use Filament\Forms\Components\Textarea;
  17. use Filament\Forms\Components\TextInput;
  18. use Filament\Forms\Form;
  19. use Filament\Forms\Get;
  20. use Filament\Forms\Set;
  21. use Filament\Pages\Page;
  22. use Filament\Support\Enums\MaxWidth;
  23. use Illuminate\Support\Collection;
  24. use Illuminate\Support\Facades\Auth;
  25. use Illuminate\Validation\Rules\Unique;
  26. use Livewire\Attributes\Computed;
  27. use Livewire\Attributes\Url;
  28. class AccountChart extends Page
  29. {
  30. protected static ?string $title = 'Chart of Accounts';
  31. protected static ?string $slug = 'accounting/chart';
  32. protected static string $view = 'filament.company.pages.accounting.chart';
  33. #[Url]
  34. public ?string $activeTab = AccountCategory::Asset->value;
  35. protected function configureAction(Action $action): void
  36. {
  37. $action
  38. ->modal()
  39. ->slideOver()
  40. ->modalWidth(MaxWidth::TwoExtraLarge);
  41. }
  42. #[Computed]
  43. public function accountCategories(): Collection
  44. {
  45. return AccountSubtype::withCount('accounts')
  46. ->with(['accounts' => function ($query) {
  47. $query->withLastTransactionDate()->with('adjustment');
  48. }])
  49. ->get()
  50. ->groupBy('category');
  51. }
  52. public function editAccountAction(): Action
  53. {
  54. return EditAction::make('editAccount')
  55. ->label('Edit account')
  56. ->iconButton()
  57. ->icon('heroicon-m-pencil-square')
  58. ->record(fn (array $arguments) => Account::find($arguments['account']))
  59. ->form(fn (Form $form) => $this->getAccountForm($form)->operation('edit'));
  60. }
  61. public function createAccountAction(): Action
  62. {
  63. return CreateAction::make('createAccount')
  64. ->link()
  65. ->model(Account::class)
  66. ->label('Add a new account')
  67. ->icon('heroicon-o-plus-circle')
  68. ->form(fn (Form $form) => $this->getAccountForm($form)->operation('create'))
  69. ->fillForm(fn (array $arguments): array => $this->getAccountFormDefaults($arguments['accountSubtype']));
  70. }
  71. private function getAccountFormDefaults(int $accountSubtypeId): array
  72. {
  73. $accountSubtype = AccountSubtype::find($accountSubtypeId);
  74. $generatedCode = AccountCode::generate($accountSubtype);
  75. return [
  76. 'subtype_id' => $accountSubtypeId,
  77. 'code' => $generatedCode,
  78. ];
  79. }
  80. private function getAccountForm(Form $form, bool $useActiveTab = true): Form
  81. {
  82. return $form
  83. ->schema([
  84. $this->getTypeFormComponent($useActiveTab),
  85. $this->getCodeFormComponent(),
  86. $this->getNameFormComponent(),
  87. ...$this->getBankAccountFormComponents(),
  88. $this->getCurrencyFormComponent(),
  89. $this->getDescriptionFormComponent(),
  90. $this->getArchiveFormComponent(),
  91. ]);
  92. }
  93. protected function getTypeFormComponent(bool $useActiveTab = true): Component
  94. {
  95. return Select::make('subtype_id')
  96. ->label('Type')
  97. ->required()
  98. ->live()
  99. ->disabledOn('edit')
  100. ->searchable()
  101. ->options($this->getAccountSubtypeOptions($useActiveTab))
  102. ->afterStateUpdated(static function (?string $state, Set $set): void {
  103. if ($state) {
  104. $accountSubtype = AccountSubtype::find($state);
  105. $generatedCode = AccountCode::generate($accountSubtype);
  106. $set('code', $generatedCode);
  107. $set('is_bank_account', false);
  108. $set('bankAccount.type', null);
  109. $set('bankAccount.number', null);
  110. }
  111. });
  112. }
  113. protected function getCodeFormComponent(): Component
  114. {
  115. return TextInput::make('code')
  116. ->label('Code')
  117. ->required()
  118. ->hiddenOn('edit')
  119. ->validationAttribute('account code')
  120. ->unique(table: Account::class, column: 'code', ignoreRecord: true)
  121. ->validateAccountCode(static fn (Get $get) => $get('subtype_id'));
  122. }
  123. protected function getBankAccountFormComponents(): array
  124. {
  125. return [
  126. Checkbox::make('is_bank_account')
  127. ->live()
  128. ->visible(function (Get $get, string $operation) {
  129. if ($operation === 'edit') {
  130. return false;
  131. }
  132. $accountSubtypeId = $get('subtype_id');
  133. if (empty($accountSubtypeId)) {
  134. return false;
  135. }
  136. $accountSubtype = AccountSubtype::find($accountSubtypeId);
  137. if (! $accountSubtype) {
  138. return false;
  139. }
  140. return in_array($accountSubtype->category, [
  141. AccountCategory::Asset,
  142. AccountCategory::Liability,
  143. ]) && $accountSubtype->multi_currency;
  144. })
  145. ->afterStateUpdated(static function ($state, Get $get, Set $set) {
  146. if ($state) {
  147. $accountSubtypeId = $get('subtype_id');
  148. if (empty($accountSubtypeId)) {
  149. return;
  150. }
  151. $accountSubtype = AccountSubtype::find($accountSubtypeId);
  152. if (! $accountSubtype) {
  153. return;
  154. }
  155. // Set default bank account type based on account category
  156. if ($accountSubtype->category === AccountCategory::Asset) {
  157. $set('bankAccount.type', BankAccountType::Depository->value);
  158. } elseif ($accountSubtype->category === AccountCategory::Liability) {
  159. $set('bankAccount.type', BankAccountType::Credit->value);
  160. }
  161. } else {
  162. // Clear bank account fields
  163. $set('bankAccount.type', null);
  164. $set('bankAccount.number', null);
  165. }
  166. }),
  167. Group::make()
  168. ->relationship('bankAccount')
  169. ->schema([
  170. Select::make('type')
  171. ->label('Bank account type')
  172. ->options(function (Get $get) {
  173. $accountSubtypeId = $get('../subtype_id');
  174. if (empty($accountSubtypeId)) {
  175. return [];
  176. }
  177. $accountSubtype = AccountSubtype::find($accountSubtypeId);
  178. if (! $accountSubtype) {
  179. return [];
  180. }
  181. if ($accountSubtype->category === AccountCategory::Asset) {
  182. return [
  183. BankAccountType::Depository->value => BankAccountType::Depository->getLabel(),
  184. BankAccountType::Investment->value => BankAccountType::Investment->getLabel(),
  185. ];
  186. } elseif ($accountSubtype->category === AccountCategory::Liability) {
  187. return [
  188. BankAccountType::Credit->value => BankAccountType::Credit->getLabel(),
  189. BankAccountType::Loan->value => BankAccountType::Loan->getLabel(),
  190. ];
  191. }
  192. return [];
  193. })
  194. ->searchable()
  195. ->columnSpan(1)
  196. ->disabledOn('edit')
  197. ->required(),
  198. TextInput::make('number')
  199. ->label('Bank account number')
  200. ->unique(ignoreRecord: true, modifyRuleUsing: static function (Unique $rule, $state) {
  201. $companyId = Auth::user()->currentCompany->id;
  202. return $rule->where('company_id', $companyId)->where('number', $state);
  203. })
  204. ->maxLength(20)
  205. ->validationAttribute('account number'),
  206. ])
  207. ->visible(static function (Get $get, ?Account $record, string $operation) {
  208. if ($operation === 'create') {
  209. return (bool) $get('is_bank_account');
  210. }
  211. if ($operation === 'edit' && $record) {
  212. return (bool) $record->bankAccount;
  213. }
  214. return false;
  215. }),
  216. ];
  217. }
  218. protected function getNameFormComponent(): Component
  219. {
  220. return TextInput::make('name')
  221. ->label('Name')
  222. ->required();
  223. }
  224. protected function getCurrencyFormComponent(): Component
  225. {
  226. return CreateCurrencySelect::make('currency_code')
  227. ->disabledOn('edit')
  228. ->required(false)
  229. ->requiredIfAccepted('is_bank_account')
  230. ->validationMessages([
  231. 'required_if_accepted' => 'The currency is required for bank accounts.',
  232. ])
  233. ->visible(function (Get $get): bool {
  234. return filled($get('subtype_id')) && AccountSubtype::find($get('subtype_id'))->multi_currency;
  235. });
  236. }
  237. protected function getDescriptionFormComponent(): Component
  238. {
  239. return Textarea::make('description')
  240. ->label('Description');
  241. }
  242. protected function getArchiveFormComponent(): Component
  243. {
  244. return Checkbox::make('archived')
  245. ->label('Archive account')
  246. ->helperText('Archived accounts will not be available for selection in transactions, offerings, or other new records.')
  247. ->hiddenOn('create');
  248. }
  249. private function getAccountSubtypeOptions($useActiveTab = true): array
  250. {
  251. $accountSubtypes = $useActiveTab ?
  252. AccountSubtype::where('category', $this->activeTab)->get() :
  253. AccountSubtype::all();
  254. return $accountSubtypes->groupBy(fn (AccountSubtype $accountSubtype) => $accountSubtype->type->getLabel())
  255. ->map(fn (Collection $accountSubtypes, string $type) => $accountSubtypes->mapWithKeys(static fn (AccountSubtype $accountSubtype) => [$accountSubtype->id => $accountSubtype->name]))
  256. ->toArray();
  257. }
  258. protected function getHeaderActions(): array
  259. {
  260. return [
  261. CreateAction::make()
  262. ->button()
  263. ->model(Account::class)
  264. ->form(fn (Form $form) => $this->getAccountForm($form, false)->operation('create')),
  265. ];
  266. }
  267. public function getCategoryLabel($categoryValue): string
  268. {
  269. return AccountCategory::from($categoryValue)->getPluralLabel();
  270. }
  271. }