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.

ClientResource.php 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337
  1. <?php
  2. namespace App\Filament\Company\Resources\Sales;
  3. use App\Filament\Company\Resources\Sales\ClientResource\Pages;
  4. use App\Filament\Forms\Components\AddressFields;
  5. use App\Filament\Forms\Components\CreateCurrencySelect;
  6. use App\Filament\Forms\Components\CustomSection;
  7. use App\Filament\Forms\Components\PhoneBuilder;
  8. use App\Filament\Tables\Columns;
  9. use App\Models\Common\Address;
  10. use App\Models\Common\Client;
  11. use App\Utilities\Currency\CurrencyConverter;
  12. use Filament\Forms;
  13. use Filament\Forms\Form;
  14. use Filament\Forms\Get;
  15. use Filament\Forms\Set;
  16. use Filament\Resources\Resource;
  17. use Filament\Tables;
  18. use Filament\Tables\Table;
  19. use Illuminate\Database\Eloquent\Builder;
  20. class ClientResource extends Resource
  21. {
  22. protected static ?string $model = Client::class;
  23. public static function form(Form $form): Form
  24. {
  25. return $form
  26. ->schema([
  27. Forms\Components\Section::make('General Information')
  28. ->schema([
  29. Forms\Components\Group::make()
  30. ->columns()
  31. ->schema([
  32. Forms\Components\TextInput::make('name')
  33. ->label('Client name')
  34. ->required()
  35. ->maxLength(255),
  36. Forms\Components\TextInput::make('account_number')
  37. ->maxLength(255)
  38. ->columnStart(1),
  39. Forms\Components\TextInput::make('website')
  40. ->maxLength(255),
  41. Forms\Components\Textarea::make('notes')
  42. ->columnSpanFull(),
  43. ]),
  44. CustomSection::make('Primary Contact')
  45. ->relationship('primaryContact')
  46. ->contained(false)
  47. ->schema([
  48. Forms\Components\Hidden::make('is_primary')
  49. ->default(true),
  50. Forms\Components\TextInput::make('first_name')
  51. ->label('First name')
  52. ->required()
  53. ->maxLength(255),
  54. Forms\Components\TextInput::make('last_name')
  55. ->label('Last name')
  56. ->required()
  57. ->maxLength(255),
  58. Forms\Components\TextInput::make('email')
  59. ->label('Email')
  60. ->required()
  61. ->email()
  62. ->columnSpanFull()
  63. ->maxLength(255),
  64. PhoneBuilder::make('phones')
  65. ->hiddenLabel()
  66. ->blockLabels(false)
  67. ->default([
  68. ['type' => 'primary'],
  69. ])
  70. ->columnSpanFull()
  71. ->blocks([
  72. Forms\Components\Builder\Block::make('primary')
  73. ->schema([
  74. Forms\Components\TextInput::make('number')
  75. ->label('Phone')
  76. ->required()
  77. ->maxLength(15),
  78. ])->maxItems(1),
  79. Forms\Components\Builder\Block::make('mobile')
  80. ->schema([
  81. Forms\Components\TextInput::make('number')
  82. ->label('Mobile')
  83. ->required()
  84. ->maxLength(15),
  85. ])->maxItems(1),
  86. Forms\Components\Builder\Block::make('toll_free')
  87. ->schema([
  88. Forms\Components\TextInput::make('number')
  89. ->label('Toll free')
  90. ->required()
  91. ->maxLength(15),
  92. ])->maxItems(1),
  93. Forms\Components\Builder\Block::make('fax')
  94. ->schema([
  95. Forms\Components\TextInput::make('number')
  96. ->label('Fax')
  97. ->live()
  98. ->maxLength(15),
  99. ])->maxItems(1),
  100. ])
  101. ->deletable(fn (PhoneBuilder $builder) => $builder->getItemsCount() > 1)
  102. ->reorderable(false)
  103. ->blockNumbers(false)
  104. ->addActionLabel('Add Phone'),
  105. ])->columns(),
  106. Forms\Components\Repeater::make('secondaryContacts')
  107. ->relationship()
  108. ->hiddenLabel()
  109. ->extraAttributes([
  110. 'class' => 'uncontained',
  111. ])
  112. ->columns()
  113. ->defaultItems(0)
  114. ->maxItems(3)
  115. ->itemLabel(function (Forms\Components\Repeater $component, array $state): ?string {
  116. if ($component->getItemsCount() === 1) {
  117. return 'Secondary Contact';
  118. }
  119. $firstName = $state['first_name'] ?? null;
  120. $lastName = $state['last_name'] ?? null;
  121. if ($firstName && $lastName) {
  122. return "{$firstName} {$lastName}";
  123. }
  124. if ($firstName) {
  125. return $firstName;
  126. }
  127. return 'Secondary Contact';
  128. })
  129. ->addActionLabel('Add Contact')
  130. ->schema([
  131. Forms\Components\TextInput::make('first_name')
  132. ->label('First name')
  133. ->required()
  134. ->live(onBlur: true)
  135. ->maxLength(255),
  136. Forms\Components\TextInput::make('last_name')
  137. ->label('Last name')
  138. ->required()
  139. ->live(onBlur: true)
  140. ->maxLength(255),
  141. Forms\Components\TextInput::make('email')
  142. ->label('Email')
  143. ->required()
  144. ->email()
  145. ->maxLength(255),
  146. PhoneBuilder::make('phones')
  147. ->hiddenLabel()
  148. ->blockLabels(false)
  149. ->default([
  150. ['type' => 'primary'],
  151. ])
  152. ->blocks([
  153. Forms\Components\Builder\Block::make('primary')
  154. ->schema([
  155. Forms\Components\TextInput::make('number')
  156. ->label('Phone')
  157. ->required()
  158. ->maxLength(255),
  159. ])->maxItems(1),
  160. ])
  161. ->addable(false)
  162. ->deletable(false)
  163. ->reorderable(false)
  164. ->blockNumbers(false),
  165. ]),
  166. ])->columns(1),
  167. Forms\Components\Section::make('Billing')
  168. ->schema([
  169. CreateCurrencySelect::make('currency_code'),
  170. CustomSection::make('Billing Address')
  171. ->relationship('billingAddress')
  172. ->saveRelationshipsUsing(null)
  173. ->dehydrated(true)
  174. ->contained(false)
  175. ->schema([
  176. Forms\Components\Hidden::make('type')
  177. ->default('billing'),
  178. AddressFields::make(),
  179. ])->columns(),
  180. ])
  181. ->columns(1),
  182. Forms\Components\Section::make('Shipping')
  183. ->relationship('shippingAddress')
  184. ->saveRelationshipsUsing(null)
  185. ->dehydrated(true)
  186. ->schema([
  187. Forms\Components\Hidden::make('type')
  188. ->default('shipping'),
  189. Forms\Components\TextInput::make('recipient')
  190. ->label('Recipient')
  191. ->required()
  192. ->maxLength(255),
  193. Forms\Components\TextInput::make('phone')
  194. ->label('Phone')
  195. ->required()
  196. ->maxLength(255),
  197. CustomSection::make('Shipping Address')
  198. ->contained(false)
  199. ->schema([
  200. Forms\Components\Checkbox::make('same_as_billing')
  201. ->label('Same as billing address')
  202. ->live()
  203. ->afterStateHydrated(function (?Address $record, Forms\Components\Checkbox $component) {
  204. if (! $record || $record->parent_address_id) {
  205. return $component->state(true);
  206. }
  207. return $component->state(false);
  208. })
  209. ->afterStateUpdated(static function (Get $get, Set $set, $state) {
  210. if ($state) {
  211. return;
  212. }
  213. $billingAddress = $get('../billingAddress');
  214. $fieldsToSync = [
  215. 'address_line_1',
  216. 'address_line_2',
  217. 'country',
  218. 'state_id',
  219. 'city',
  220. 'postal_code',
  221. ];
  222. foreach ($fieldsToSync as $field) {
  223. $set($field, $billingAddress[$field]);
  224. }
  225. })
  226. ->columnSpanFull(),
  227. AddressFields::make()
  228. ->visible(static fn (Get $get) => ! $get('same_as_billing')),
  229. Forms\Components\Textarea::make('notes')
  230. ->label('Delivery instructions')
  231. ->maxLength(255)
  232. ->columnSpanFull(),
  233. ])->columns(),
  234. ])->columns(),
  235. ]);
  236. }
  237. public static function table(Table $table): Table
  238. {
  239. return $table
  240. ->columns([
  241. Columns::id(),
  242. Tables\Columns\TextColumn::make('name')
  243. ->searchable()
  244. ->sortable()
  245. ->description(static fn (Client $client) => $client->primaryContact->full_name),
  246. Tables\Columns\TextColumn::make('primaryContact.email')
  247. ->label('Email')
  248. ->searchable()
  249. ->toggleable(),
  250. Tables\Columns\TextColumn::make('primaryContact.phones')
  251. ->label('Phone')
  252. ->toggleable()
  253. ->state(static fn (Client $client) => $client->primaryContact->first_available_phone),
  254. Tables\Columns\TextColumn::make('billingAddress.address_string')
  255. ->label('Billing address')
  256. ->searchable()
  257. ->toggleable(isToggledHiddenByDefault: true)
  258. ->listWithLineBreaks(),
  259. Tables\Columns\TextColumn::make('balance')
  260. ->label('Balance')
  261. ->getStateUsing(function (Client $client) {
  262. return $client->invoices()
  263. ->unpaid()
  264. ->get()
  265. ->sumMoneyInDefaultCurrency('amount_due');
  266. })
  267. ->coloredDescription(function (Client $client) {
  268. $overdue = $client->invoices()
  269. ->overdue()
  270. ->get()
  271. ->sumMoneyInDefaultCurrency('amount_due');
  272. if ($overdue <= 0) {
  273. return null;
  274. }
  275. $formattedOverdue = CurrencyConverter::formatCentsToMoney($overdue);
  276. return "Overdue: {$formattedOverdue}";
  277. })
  278. ->sortable(query: function (Builder $query, string $direction) {
  279. return $query
  280. ->withSum(['invoices' => fn (Builder $query) => $query->unpaid()], 'amount_due')
  281. ->orderBy('invoices_sum_amount_due', $direction);
  282. })
  283. ->currency(convert: false)
  284. ->alignEnd(),
  285. ])
  286. ->filters([
  287. //
  288. ])
  289. ->actions([
  290. Tables\Actions\ActionGroup::make([
  291. Tables\Actions\ActionGroup::make([
  292. Tables\Actions\EditAction::make(),
  293. Tables\Actions\ViewAction::make(),
  294. ])->dropdown(false),
  295. Tables\Actions\DeleteAction::make(),
  296. ]),
  297. ])
  298. ->bulkActions([
  299. Tables\Actions\BulkActionGroup::make([
  300. Tables\Actions\DeleteBulkAction::make(),
  301. ]),
  302. ]);
  303. }
  304. public static function getRelations(): array
  305. {
  306. return [
  307. //
  308. ];
  309. }
  310. public static function getPages(): array
  311. {
  312. return [
  313. 'index' => Pages\ListClients::route('/'),
  314. 'create' => Pages\CreateClient::route('/create'),
  315. 'view' => Pages\ViewClient::route('/{record}'),
  316. 'edit' => Pages\EditClient::route('/{record}/edit'),
  317. ];
  318. }
  319. }