Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

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\Exports\Common\ClientExporter;
  5. use App\Filament\Forms\Components\AddressFields;
  6. use App\Filament\Forms\Components\CreateCurrencySelect;
  7. use App\Filament\Forms\Components\CustomSection;
  8. use App\Filament\Forms\Components\PhoneBuilder;
  9. use App\Filament\Tables\Columns;
  10. use App\Models\Common\Address;
  11. use App\Models\Common\Client;
  12. use App\Utilities\Currency\CurrencyConverter;
  13. use Filament\Forms;
  14. use Filament\Forms\Form;
  15. use Filament\Forms\Get;
  16. use Filament\Forms\Set;
  17. use Filament\Resources\Resource;
  18. use Filament\Tables;
  19. use Filament\Tables\Table;
  20. use Illuminate\Database\Eloquent\Builder;
  21. class ClientResource extends Resource
  22. {
  23. protected static ?string $model = Client::class;
  24. public static function form(Form $form): Form
  25. {
  26. return $form
  27. ->schema([
  28. Forms\Components\Section::make('General Information')
  29. ->schema([
  30. Forms\Components\Group::make()
  31. ->columns()
  32. ->schema([
  33. Forms\Components\TextInput::make('name')
  34. ->label('Client name')
  35. ->required()
  36. ->maxLength(255),
  37. Forms\Components\TextInput::make('account_number')
  38. ->maxLength(255)
  39. ->columnStart(1),
  40. Forms\Components\TextInput::make('website')
  41. ->maxLength(255),
  42. Forms\Components\Textarea::make('notes')
  43. ->columnSpanFull(),
  44. ]),
  45. CustomSection::make('Primary Contact')
  46. ->relationship('primaryContact')
  47. ->saveRelationshipsUsing(null)
  48. ->saveRelationshipsBeforeChildrenUsing(null)
  49. ->dehydrated(true)
  50. ->contained(false)
  51. ->schema([
  52. Forms\Components\Hidden::make('is_primary')
  53. ->default(true),
  54. Forms\Components\TextInput::make('first_name')
  55. ->label('First name')
  56. ->maxLength(255),
  57. Forms\Components\TextInput::make('last_name')
  58. ->label('Last name')
  59. ->maxLength(255),
  60. Forms\Components\TextInput::make('email')
  61. ->label('Email')
  62. ->email()
  63. ->columnSpanFull()
  64. ->maxLength(255),
  65. PhoneBuilder::make('phones')
  66. ->hiddenLabel()
  67. ->blockLabels(false)
  68. ->default([
  69. ['type' => 'primary'],
  70. ])
  71. ->columnSpanFull()
  72. ->blocks([
  73. Forms\Components\Builder\Block::make('primary')
  74. ->schema([
  75. Forms\Components\TextInput::make('number')
  76. ->label('Phone')
  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. ->maxLength(15),
  84. ])->maxItems(1),
  85. Forms\Components\Builder\Block::make('toll_free')
  86. ->schema([
  87. Forms\Components\TextInput::make('number')
  88. ->label('Toll free')
  89. ->maxLength(15),
  90. ])->maxItems(1),
  91. Forms\Components\Builder\Block::make('fax')
  92. ->schema([
  93. Forms\Components\TextInput::make('number')
  94. ->label('Fax')
  95. ->live()
  96. ->maxLength(15),
  97. ])->maxItems(1),
  98. ])
  99. ->deletable(fn (PhoneBuilder $builder) => $builder->getItemsCount() > 1)
  100. ->reorderable(false)
  101. ->blockNumbers(false)
  102. ->addActionLabel('Add Phone'),
  103. ])->columns(),
  104. Forms\Components\Repeater::make('secondaryContacts')
  105. ->relationship()
  106. ->saveRelationshipsUsing(null)
  107. ->saveRelationshipsBeforeChildrenUsing(null)
  108. ->dehydrated(true)
  109. ->hiddenLabel()
  110. ->extraAttributes([
  111. 'class' => 'uncontained',
  112. ])
  113. ->columns()
  114. ->defaultItems(0)
  115. ->maxItems(3)
  116. ->itemLabel(function (Forms\Components\Repeater $component, array $state): ?string {
  117. if ($component->getItemsCount() === 1) {
  118. return 'Secondary Contact';
  119. }
  120. $firstName = $state['first_name'] ?? null;
  121. $lastName = $state['last_name'] ?? null;
  122. if ($firstName && $lastName) {
  123. return "{$firstName} {$lastName}";
  124. }
  125. if ($firstName) {
  126. return $firstName;
  127. }
  128. return 'Secondary Contact';
  129. })
  130. ->addActionLabel('Add Contact')
  131. ->schema([
  132. Forms\Components\TextInput::make('first_name')
  133. ->label('First name')
  134. ->live(onBlur: true)
  135. ->maxLength(255),
  136. Forms\Components\TextInput::make('last_name')
  137. ->label('Last name')
  138. ->live(onBlur: true)
  139. ->maxLength(255),
  140. Forms\Components\TextInput::make('email')
  141. ->label('Email')
  142. ->email()
  143. ->maxLength(255),
  144. PhoneBuilder::make('phones')
  145. ->hiddenLabel()
  146. ->blockLabels(false)
  147. ->default([
  148. ['type' => 'primary'],
  149. ])
  150. ->blocks([
  151. Forms\Components\Builder\Block::make('primary')
  152. ->schema([
  153. Forms\Components\TextInput::make('number')
  154. ->label('Phone')
  155. ->maxLength(255),
  156. ])->maxItems(1),
  157. ])
  158. ->addable(false)
  159. ->deletable(false)
  160. ->reorderable(false)
  161. ->blockNumbers(false),
  162. ]),
  163. ])->columns(1),
  164. Forms\Components\Section::make('Billing')
  165. ->schema([
  166. CreateCurrencySelect::make('currency_code')
  167. ->softRequired(),
  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. ->headerActions([
  288. Tables\Actions\ExportAction::make()
  289. ->exporter(ClientExporter::class),
  290. ])
  291. ->actions([
  292. Tables\Actions\ActionGroup::make([
  293. Tables\Actions\ActionGroup::make([
  294. Tables\Actions\EditAction::make(),
  295. Tables\Actions\ViewAction::make(),
  296. ])->dropdown(false),
  297. Tables\Actions\DeleteAction::make(),
  298. ]),
  299. ])
  300. ->bulkActions([
  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. }