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.

OfferingResource.php 8.7KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  1. <?php
  2. namespace App\Filament\Company\Resources\Common;
  3. use App\Enums\Accounting\AccountCategory;
  4. use App\Enums\Accounting\AccountType;
  5. use App\Enums\Accounting\AdjustmentCategory;
  6. use App\Enums\Accounting\AdjustmentType;
  7. use App\Enums\Common\OfferingType;
  8. use App\Filament\Company\Resources\Common\OfferingResource\Pages;
  9. use App\Filament\Forms\Components\Banner;
  10. use App\Filament\Forms\Components\CreateAccountSelect;
  11. use App\Filament\Forms\Components\CreateAdjustmentSelect;
  12. use App\Models\Common\Offering;
  13. use App\Utilities\Currency\CurrencyAccessor;
  14. use Filament\Forms;
  15. use Filament\Forms\Form;
  16. use Filament\Resources\Resource;
  17. use Filament\Tables;
  18. use Filament\Tables\Table;
  19. use Illuminate\Database\Eloquent\Builder;
  20. use Illuminate\Support\HtmlString;
  21. use Illuminate\Support\Str;
  22. use JaOcero\RadioDeck\Forms\Components\RadioDeck;
  23. class OfferingResource extends Resource
  24. {
  25. protected static ?string $model = Offering::class;
  26. protected static ?string $navigationIcon = 'heroicon-o-square-3-stack-3d';
  27. public static function form(Form $form): Form
  28. {
  29. return $form
  30. ->schema([
  31. Banner::make('inactiveAdjustments')
  32. ->label('Inactive adjustments')
  33. ->warning()
  34. ->icon('heroicon-o-exclamation-triangle')
  35. ->visible(fn (?Offering $record) => $record?->hasInactiveAdjustments())
  36. ->columnSpanFull()
  37. ->description(function (Offering $record) {
  38. $inactiveAdjustments = collect();
  39. foreach ($record->adjustments as $adjustment) {
  40. if ($adjustment->isInactive() && $inactiveAdjustments->doesntContain($adjustment->name)) {
  41. $inactiveAdjustments->push($adjustment->name);
  42. }
  43. }
  44. $adjustmentsList = $inactiveAdjustments->map(static function ($name) {
  45. return "<span class='font-medium'>{$name}</span>";
  46. })->join(', ');
  47. $output = "<p class='text-sm'>This offering contains inactive adjustments that need to be addressed: {$adjustmentsList}</p>";
  48. return new HtmlString($output);
  49. }),
  50. static::getGeneralSection(),
  51. // Sellable Section
  52. static::getSellableSection(),
  53. // Purchasable Section
  54. static::getPurchasableSection(),
  55. ])->columns();
  56. }
  57. public static function getGeneralSection(bool $hasAttributeChoices = true): Forms\Components\Section
  58. {
  59. return Forms\Components\Section::make('General')
  60. ->schema([
  61. RadioDeck::make('type')
  62. ->options(OfferingType::class)
  63. ->default(OfferingType::Product)
  64. ->icons(OfferingType::class)
  65. ->color('primary')
  66. ->columns()
  67. ->required(),
  68. Forms\Components\TextInput::make('name')
  69. ->autofocus()
  70. ->required()
  71. ->columnStart(1)
  72. ->maxLength(255),
  73. Forms\Components\TextInput::make('price')
  74. ->required()
  75. ->money(),
  76. Forms\Components\Textarea::make('description')
  77. ->label('Description')
  78. ->columnSpan(2)
  79. ->rows(3),
  80. Forms\Components\CheckboxList::make('attributes')
  81. ->options([
  82. 'Sellable' => 'Sellable',
  83. 'Purchasable' => 'Purchasable',
  84. ])
  85. ->visible($hasAttributeChoices)
  86. ->hiddenLabel()
  87. ->required()
  88. ->live()
  89. ->bulkToggleable()
  90. ->validationMessages([
  91. 'required' => 'The offering must be either sellable or purchasable.',
  92. ]),
  93. ])->columns();
  94. }
  95. public static function getSellableSection(bool $showByDefault = false): Forms\Components\Section
  96. {
  97. return Forms\Components\Section::make('Sale Information')
  98. ->schema([
  99. CreateAccountSelect::make('income_account_id')
  100. ->label('Income account')
  101. ->category(AccountCategory::Revenue)
  102. ->type(AccountType::OperatingRevenue)
  103. ->required()
  104. ->validationMessages([
  105. 'required' => 'The income account is required for sellable offerings.',
  106. ]),
  107. CreateAdjustmentSelect::make('salesTaxes')
  108. ->label('Sales tax')
  109. ->category(AdjustmentCategory::Tax)
  110. ->type(AdjustmentType::Sales)
  111. ->multiple(),
  112. CreateAdjustmentSelect::make('salesDiscounts')
  113. ->label('Sales discount')
  114. ->category(AdjustmentCategory::Discount)
  115. ->type(AdjustmentType::Sales)
  116. ->multiple(),
  117. ])
  118. ->columns()
  119. ->visible(static fn (Forms\Get $get) => in_array('Sellable', $get('attributes') ?? []));
  120. }
  121. public static function getPurchasableSection(): Forms\Components\Section
  122. {
  123. return Forms\Components\Section::make('Purchase Information')
  124. ->schema([
  125. CreateAccountSelect::make('expense_account_id')
  126. ->label('Expense account')
  127. ->category(AccountCategory::Expense)
  128. ->type(AccountType::OperatingExpense)
  129. ->required()
  130. ->validationMessages([
  131. 'required' => 'The expense account is required for purchasable offerings.',
  132. ]),
  133. CreateAdjustmentSelect::make('purchaseTaxes')
  134. ->label('Purchase tax')
  135. ->category(AdjustmentCategory::Tax)
  136. ->type(AdjustmentType::Purchase)
  137. ->multiple(),
  138. CreateAdjustmentSelect::make('purchaseDiscounts')
  139. ->label('Purchase discount')
  140. ->category(AdjustmentCategory::Discount)
  141. ->type(AdjustmentType::Purchase)
  142. ->multiple(),
  143. ])
  144. ->columns()
  145. ->visible(static fn (Forms\Get $get) => in_array('Purchasable', $get('attributes') ?? []));
  146. }
  147. public static function table(Table $table): Table
  148. {
  149. return $table
  150. ->modifyQueryUsing(function (Builder $query) {
  151. $query->selectRaw("
  152. *,
  153. CONCAT_WS(' & ',
  154. CASE WHEN sellable THEN 'Sellable' END,
  155. CASE WHEN purchasable THEN 'Purchasable' END
  156. ) AS attributes
  157. ");
  158. })
  159. ->columns([
  160. Tables\Columns\TextColumn::make('name')
  161. ->label('Name'),
  162. Tables\Columns\TextColumn::make('attributes')
  163. ->label('Attributes')
  164. ->badge(),
  165. Tables\Columns\TextColumn::make('type')
  166. ->searchable(),
  167. Tables\Columns\TextColumn::make('price')
  168. ->currency(CurrencyAccessor::getDefaultCurrency(), true)
  169. ->sortable()
  170. ->description(function (Offering $record) {
  171. $adjustments = $record->adjustments()
  172. ->pluck('name')
  173. ->join(', ');
  174. if (empty($adjustments)) {
  175. return null;
  176. }
  177. $adjustmentsList = Str::of($adjustments)->limit(40);
  178. return "+ {$adjustmentsList}";
  179. }),
  180. ])
  181. ->filters([
  182. //
  183. ])
  184. ->actions([
  185. Tables\Actions\EditAction::make(),
  186. ])
  187. ->bulkActions([
  188. Tables\Actions\BulkActionGroup::make([
  189. Tables\Actions\DeleteBulkAction::make(),
  190. ]),
  191. ]);
  192. }
  193. public static function getRelations(): array
  194. {
  195. return [
  196. //
  197. ];
  198. }
  199. public static function getPages(): array
  200. {
  201. return [
  202. 'index' => Pages\ListOfferings::route('/'),
  203. 'create' => Pages\CreateOffering::route('/create'),
  204. 'edit' => Pages\EditOffering::route('/{record}/edit'),
  205. ];
  206. }
  207. }