Du kannst nicht mehr als 25 Themen auswählen Themen müssen mit entweder einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.

ClientResource.php 19KB

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