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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335
  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. ->saveRelationshipsUsing(null)
  47. ->saveRelationshipsBeforeChildrenUsing(null)
  48. ->dehydrated(true)
  49. ->contained(false)
  50. ->schema([
  51. Forms\Components\Hidden::make('is_primary')
  52. ->default(true),
  53. Forms\Components\TextInput::make('first_name')
  54. ->label('First name')
  55. ->maxLength(255),
  56. Forms\Components\TextInput::make('last_name')
  57. ->label('Last name')
  58. ->maxLength(255),
  59. Forms\Components\TextInput::make('email')
  60. ->label('Email')
  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. ->maxLength(15),
  77. ])->maxItems(1),
  78. Forms\Components\Builder\Block::make('mobile')
  79. ->schema([
  80. Forms\Components\TextInput::make('number')
  81. ->label('Mobile')
  82. ->maxLength(15),
  83. ])->maxItems(1),
  84. Forms\Components\Builder\Block::make('toll_free')
  85. ->schema([
  86. Forms\Components\TextInput::make('number')
  87. ->label('Toll free')
  88. ->maxLength(15),
  89. ])->maxItems(1),
  90. Forms\Components\Builder\Block::make('fax')
  91. ->schema([
  92. Forms\Components\TextInput::make('number')
  93. ->label('Fax')
  94. ->live()
  95. ->maxLength(15),
  96. ])->maxItems(1),
  97. ])
  98. ->deletable(fn (PhoneBuilder $builder) => $builder->getItemsCount() > 1)
  99. ->reorderable(false)
  100. ->blockNumbers(false)
  101. ->addActionLabel('Add Phone'),
  102. ])->columns(),
  103. Forms\Components\Repeater::make('secondaryContacts')
  104. ->relationship()
  105. ->saveRelationshipsUsing(null)
  106. ->saveRelationshipsBeforeChildrenUsing(null)
  107. ->dehydrated(true)
  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. ->live(onBlur: true)
  134. ->maxLength(255),
  135. Forms\Components\TextInput::make('last_name')
  136. ->label('Last name')
  137. ->live(onBlur: true)
  138. ->maxLength(255),
  139. Forms\Components\TextInput::make('email')
  140. ->label('Email')
  141. ->email()
  142. ->maxLength(255),
  143. PhoneBuilder::make('phones')
  144. ->hiddenLabel()
  145. ->blockLabels(false)
  146. ->default([
  147. ['type' => 'primary'],
  148. ])
  149. ->blocks([
  150. Forms\Components\Builder\Block::make('primary')
  151. ->schema([
  152. Forms\Components\TextInput::make('number')
  153. ->label('Phone')
  154. ->maxLength(255),
  155. ])->maxItems(1),
  156. ])
  157. ->addable(false)
  158. ->deletable(false)
  159. ->reorderable(false)
  160. ->blockNumbers(false),
  161. ]),
  162. ])->columns(1),
  163. Forms\Components\Section::make('Billing')
  164. ->schema([
  165. CreateCurrencySelect::make('currency_code')
  166. ->required(false)
  167. ->selectablePlaceholder(false),
  168. CustomSection::make('Billing Address')
  169. ->relationship('billingAddress')
  170. ->saveRelationshipsUsing(null)
  171. ->saveRelationshipsBeforeChildrenUsing(null)
  172. ->dehydrated(true)
  173. ->contained(false)
  174. ->schema([
  175. Forms\Components\Hidden::make('type')
  176. ->default('billing'),
  177. AddressFields::make(),
  178. ])->columns(),
  179. ])
  180. ->columns(1),
  181. Forms\Components\Section::make('Shipping')
  182. ->relationship('shippingAddress')
  183. ->saveRelationshipsUsing(null)
  184. ->saveRelationshipsBeforeChildrenUsing(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. ->maxLength(255),
  192. Forms\Components\TextInput::make('phone')
  193. ->label('Phone')
  194. ->maxLength(255),
  195. CustomSection::make('Shipping Address')
  196. ->contained(false)
  197. ->schema([
  198. Forms\Components\Checkbox::make('same_as_billing')
  199. ->label('Same as billing address')
  200. ->live()
  201. ->afterStateHydrated(function (?Address $record, Forms\Components\Checkbox $component) {
  202. if (! $record || $record->parent_address_id) {
  203. return $component->state(true);
  204. }
  205. return $component->state(false);
  206. })
  207. ->afterStateUpdated(static function (Get $get, Set $set, $state) {
  208. if ($state) {
  209. return;
  210. }
  211. $billingAddress = $get('../billingAddress');
  212. $fieldsToSync = [
  213. 'address_line_1',
  214. 'address_line_2',
  215. 'country_code',
  216. 'state_id',
  217. 'city',
  218. 'postal_code',
  219. ];
  220. foreach ($fieldsToSync as $field) {
  221. $set($field, $billingAddress[$field]);
  222. }
  223. })
  224. ->columnSpanFull(),
  225. AddressFields::make()
  226. ->visible(static fn (Get $get) => ! $get('same_as_billing')),
  227. Forms\Components\Textarea::make('notes')
  228. ->label('Delivery instructions')
  229. ->maxLength(255)
  230. ->columnSpanFull(),
  231. ])->columns(),
  232. ])->columns(),
  233. ]);
  234. }
  235. public static function table(Table $table): Table
  236. {
  237. return $table
  238. ->columns([
  239. Columns::id(),
  240. Tables\Columns\TextColumn::make('name')
  241. ->searchable()
  242. ->sortable()
  243. ->description(static fn (Client $client) => $client->primaryContact->full_name),
  244. Tables\Columns\TextColumn::make('primaryContact.email')
  245. ->label('Email')
  246. ->searchable()
  247. ->toggleable(),
  248. Tables\Columns\TextColumn::make('primaryContact.phones')
  249. ->label('Phone')
  250. ->toggleable()
  251. ->state(static fn (Client $client) => $client->primaryContact->first_available_phone),
  252. Tables\Columns\TextColumn::make('billingAddress.address_string')
  253. ->label('Billing address')
  254. ->searchable()
  255. ->toggleable(isToggledHiddenByDefault: true)
  256. ->listWithLineBreaks(),
  257. Tables\Columns\TextColumn::make('balance')
  258. ->label('Balance')
  259. ->getStateUsing(function (Client $client) {
  260. return $client->invoices()
  261. ->unpaid()
  262. ->get()
  263. ->sumMoneyInDefaultCurrency('amount_due');
  264. })
  265. ->coloredDescription(function (Client $client) {
  266. $overdue = $client->invoices()
  267. ->overdue()
  268. ->get()
  269. ->sumMoneyInDefaultCurrency('amount_due');
  270. if ($overdue <= 0) {
  271. return null;
  272. }
  273. $formattedOverdue = CurrencyConverter::formatCentsToMoney($overdue);
  274. return "Overdue: {$formattedOverdue}";
  275. })
  276. ->sortable(query: function (Builder $query, string $direction) {
  277. return $query
  278. ->withSum(['invoices' => fn (Builder $query) => $query->unpaid()], 'amount_due')
  279. ->orderBy('invoices_sum_amount_due', $direction);
  280. })
  281. ->currency(convert: false)
  282. ->alignEnd(),
  283. ])
  284. ->filters([
  285. //
  286. ])
  287. ->actions([
  288. Tables\Actions\ActionGroup::make([
  289. Tables\Actions\ActionGroup::make([
  290. Tables\Actions\EditAction::make(),
  291. Tables\Actions\ViewAction::make(),
  292. ])->dropdown(false),
  293. Tables\Actions\DeleteAction::make(),
  294. ]),
  295. ])
  296. ->bulkActions([
  297. Tables\Actions\BulkActionGroup::make([
  298. Tables\Actions\DeleteBulkAction::make(),
  299. ]),
  300. ]);
  301. }
  302. public static function getRelations(): array
  303. {
  304. return [
  305. //
  306. ];
  307. }
  308. public static function getPages(): array
  309. {
  310. return [
  311. 'index' => Pages\ListClients::route('/'),
  312. 'create' => Pages\CreateClient::route('/create'),
  313. 'view' => Pages\ViewClient::route('/{record}'),
  314. 'edit' => Pages\EditClient::route('/{record}/edit'),
  315. ];
  316. }
  317. }