選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

ClientResource.php 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332
  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. ->softRequired(),
  167. CustomSection::make('Billing Address')
  168. ->relationship('billingAddress')
  169. ->saveRelationshipsUsing(null)
  170. ->saveRelationshipsBeforeChildrenUsing(null)
  171. ->dehydrated(true)
  172. ->contained(false)
  173. ->schema([
  174. Forms\Components\Hidden::make('type')
  175. ->default('billing'),
  176. AddressFields::make(),
  177. ])->columns(),
  178. ])
  179. ->columns(1),
  180. Forms\Components\Section::make('Shipping')
  181. ->relationship('shippingAddress')
  182. ->saveRelationshipsUsing(null)
  183. ->saveRelationshipsBeforeChildrenUsing(null)
  184. ->dehydrated(true)
  185. ->schema([
  186. Forms\Components\Hidden::make('type')
  187. ->default('shipping'),
  188. Forms\Components\TextInput::make('recipient')
  189. ->label('Recipient')
  190. ->maxLength(255),
  191. Forms\Components\TextInput::make('phone')
  192. ->label('Phone')
  193. ->maxLength(255),
  194. CustomSection::make('Shipping Address')
  195. ->contained(false)
  196. ->schema([
  197. Forms\Components\Checkbox::make('same_as_billing')
  198. ->label('Same as billing address')
  199. ->live()
  200. ->afterStateHydrated(function (?Address $record, Forms\Components\Checkbox $component) {
  201. if (! $record || $record->parent_address_id) {
  202. return $component->state(true);
  203. }
  204. return $component->state(false);
  205. })
  206. ->afterStateUpdated(static function (Get $get, Set $set, $state) {
  207. if ($state) {
  208. return;
  209. }
  210. $billingAddress = $get('../billingAddress');
  211. $fieldsToSync = [
  212. 'address_line_1',
  213. 'address_line_2',
  214. 'country_code',
  215. 'state_id',
  216. 'city',
  217. 'postal_code',
  218. ];
  219. foreach ($fieldsToSync as $field) {
  220. $set($field, $billingAddress[$field]);
  221. }
  222. })
  223. ->columnSpanFull(),
  224. AddressFields::make()
  225. ->visible(static fn (Get $get) => ! $get('same_as_billing')),
  226. Forms\Components\Textarea::make('notes')
  227. ->label('Delivery instructions')
  228. ->maxLength(255)
  229. ->columnSpanFull(),
  230. ])->columns(),
  231. ])->columns(),
  232. ]);
  233. }
  234. public static function table(Table $table): Table
  235. {
  236. return $table
  237. ->columns([
  238. Columns::id(),
  239. Tables\Columns\TextColumn::make('name')
  240. ->searchable()
  241. ->sortable()
  242. ->description(static fn (Client $client) => $client->primaryContact?->full_name),
  243. Tables\Columns\TextColumn::make('primaryContact.email')
  244. ->label('Email')
  245. ->searchable()
  246. ->toggleable(),
  247. Tables\Columns\TextColumn::make('primaryContact.phones')
  248. ->label('Phone')
  249. ->toggleable()
  250. ->state(static fn (Client $client) => $client->primaryContact?->first_available_phone),
  251. Tables\Columns\TextColumn::make('billingAddress.address_string')
  252. ->label('Billing address')
  253. ->searchable()
  254. ->toggleable(isToggledHiddenByDefault: true)
  255. ->listWithLineBreaks(),
  256. Tables\Columns\TextColumn::make('balance')
  257. ->label('Balance')
  258. ->getStateUsing(function (Client $client) {
  259. return $client->invoices()
  260. ->unpaid()
  261. ->get()
  262. ->sumMoneyInDefaultCurrency('amount_due');
  263. })
  264. ->coloredDescription(function (Client $client) {
  265. $overdue = $client->invoices()
  266. ->overdue()
  267. ->get()
  268. ->sumMoneyInDefaultCurrency('amount_due');
  269. if ($overdue <= 0) {
  270. return null;
  271. }
  272. $formattedOverdue = CurrencyConverter::formatCentsToMoney($overdue);
  273. return "Overdue: {$formattedOverdue}";
  274. })
  275. ->sortable(query: function (Builder $query, string $direction) {
  276. return $query
  277. ->withSum(['invoices' => fn (Builder $query) => $query->unpaid()], 'amount_due')
  278. ->orderBy('invoices_sum_amount_due', $direction);
  279. })
  280. ->currency(convert: false)
  281. ->alignEnd(),
  282. ])
  283. ->filters([
  284. //
  285. ])
  286. ->actions([
  287. Tables\Actions\ActionGroup::make([
  288. Tables\Actions\ActionGroup::make([
  289. Tables\Actions\EditAction::make(),
  290. Tables\Actions\ViewAction::make(),
  291. ])->dropdown(false),
  292. Tables\Actions\DeleteAction::make(),
  293. ]),
  294. ])
  295. ->bulkActions([
  296. //
  297. ]);
  298. }
  299. public static function getRelations(): array
  300. {
  301. return [
  302. //
  303. ];
  304. }
  305. public static function getPages(): array
  306. {
  307. return [
  308. 'index' => Pages\ListClients::route('/'),
  309. 'create' => Pages\CreateClient::route('/create'),
  310. 'view' => Pages\ViewClient::route('/{record}'),
  311. 'edit' => Pages\EditClient::route('/{record}/edit'),
  312. ];
  313. }
  314. }