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.

DocumentResource.php 25KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431
  1. <?php
  2. namespace App\Filament\Company\Resources\Accounting;
  3. use App\Enums\Accounting\AdjustmentCategory;
  4. use App\Filament\Company\Resources\Accounting\DocumentResource\Pages;
  5. use App\Models\Accounting\Adjustment;
  6. use App\Models\Accounting\Document;
  7. use App\Models\Common\Offering;
  8. use App\Utilities\Currency\CurrencyAccessor;
  9. use Awcodes\TableRepeater\Components\TableRepeater;
  10. use Awcodes\TableRepeater\Header;
  11. use Filament\Forms;
  12. use Filament\Forms\Components\FileUpload;
  13. use Filament\Forms\Form;
  14. use Filament\Resources\Resource;
  15. use Filament\Support\Enums\MaxWidth;
  16. use Filament\Tables;
  17. use Filament\Tables\Table;
  18. use Illuminate\Support\Facades\Auth;
  19. use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
  20. class DocumentResource extends Resource
  21. {
  22. protected static ?string $model = Document::class;
  23. protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack';
  24. public static function form(Form $form): Form
  25. {
  26. $company = Auth::user()->currentCompany;
  27. return $form
  28. ->schema([
  29. Forms\Components\Section::make('Invoice Header')
  30. ->collapsible()
  31. ->schema([
  32. Forms\Components\Split::make([
  33. Forms\Components\Group::make([
  34. FileUpload::make('logo')
  35. ->openable()
  36. ->maxSize(1024)
  37. ->localizeLabel()
  38. ->visibility('public')
  39. ->disk('public')
  40. ->directory('logos/document')
  41. ->imageResizeMode('contain')
  42. ->imageCropAspectRatio('3:2')
  43. ->panelAspectRatio('3:2')
  44. ->maxWidth(MaxWidth::ExtraSmall)
  45. ->panelLayout('integrated')
  46. ->removeUploadedFileButtonPosition('center bottom')
  47. ->uploadButtonPosition('center bottom')
  48. ->uploadProgressIndicatorPosition('center bottom')
  49. ->getUploadedFileNameForStorageUsing(
  50. static fn (TemporaryUploadedFile $file): string => (string) str($file->getClientOriginalName())
  51. ->prepend(Auth::user()->currentCompany->id . '_'),
  52. )
  53. ->acceptedFileTypes(['image/png', 'image/jpeg', 'image/gif']),
  54. ]),
  55. Forms\Components\Group::make([
  56. Forms\Components\TextInput::make('header')
  57. ->default(fn () => $company->defaultInvoice->header),
  58. Forms\Components\TextInput::make('subheader')
  59. ->default(fn () => $company->defaultInvoice->subheader),
  60. Forms\Components\View::make('filament.forms.components.company-info')
  61. ->viewData([
  62. 'company_name' => $company->name,
  63. 'company_address' => $company->profile->address,
  64. 'company_city' => $company->profile->city?->name,
  65. 'company_state' => $company->profile->state?->name,
  66. 'company_zip' => $company->profile->zip_code,
  67. 'company_country' => $company->profile->state?->country->name,
  68. ]),
  69. ])->grow(true),
  70. ])->from('md'),
  71. ]),
  72. Forms\Components\Section::make('Invoice Details')
  73. ->schema([
  74. Forms\Components\Split::make([
  75. Forms\Components\Group::make([
  76. Forms\Components\Select::make('client_id')
  77. ->relationship('client', 'name')
  78. ->preload()
  79. ->searchable()
  80. ->required(),
  81. ]),
  82. Forms\Components\Group::make([
  83. Forms\Components\TextInput::make('document_number')
  84. ->label('Invoice Number')
  85. ->default(fn () => $company->defaultInvoice->getNumberNext()),
  86. Forms\Components\TextInput::make('order_number')
  87. ->label('P.O/S.O Number'),
  88. Forms\Components\DatePicker::make('date')
  89. ->label('Invoice Date')
  90. ->default(now()),
  91. Forms\Components\DatePicker::make('due_date')
  92. ->label('Payment Due')
  93. ->default(function () use ($company) {
  94. return now()->addDays($company->defaultInvoice->payment_terms->getDays());
  95. }),
  96. ])->grow(true),
  97. ])->from('md'),
  98. TableRepeater::make('lineItems')
  99. ->relationship()
  100. ->headers([
  101. Header::make('Items')->width('20%'),
  102. Header::make('Description')->width('30%'),
  103. Header::make('Quantity')->width('10%'),
  104. Header::make('Price')->width('10%'),
  105. Header::make('Taxes')->width('20%'),
  106. Header::make('Amount')->width('10%')->align('right'),
  107. ])
  108. ->live()
  109. ->schema([
  110. Forms\Components\Select::make('offering_id')
  111. ->relationship('offering', 'name')
  112. ->preload()
  113. ->searchable()
  114. ->required()
  115. ->afterStateUpdated(function (Forms\Set $set, Forms\Get $get, $state) {
  116. $offeringId = $state;
  117. $offeringRecord = Offering::with('salesTaxes')->find($offeringId);
  118. if ($offeringRecord) {
  119. $set('description', $offeringRecord->description);
  120. $set('unit_price', $offeringRecord->price);
  121. $set('salesTaxes', $offeringRecord->salesTaxes->pluck('id')->toArray());
  122. $quantity = $get('quantity');
  123. $total = $quantity * $offeringRecord->price;
  124. // Calculate taxes and update total
  125. $taxAmount = $offeringRecord->salesTaxes->sum(fn ($tax) => $total * ($tax->rate / 100));
  126. $set('total', $total + $taxAmount);
  127. }
  128. }),
  129. Forms\Components\TextInput::make('description')
  130. ->required(),
  131. Forms\Components\TextInput::make('quantity')
  132. ->required()
  133. ->numeric()
  134. ->default(1),
  135. Forms\Components\TextInput::make('unit_price')
  136. ->hiddenLabel()
  137. ->numeric()
  138. ->default(0),
  139. Forms\Components\Select::make('salesTaxes')
  140. ->relationship('salesTaxes', 'name')
  141. ->preload()
  142. ->multiple()
  143. ->searchable(),
  144. Forms\Components\Placeholder::make('total')
  145. ->hiddenLabel()
  146. ->content(function (Forms\Get $get) {
  147. $quantity = $get('quantity') ?? 0;
  148. $unitPrice = $get('unit_price') ?? 0;
  149. $salesTaxes = $get('salesTaxes') ?? [];
  150. $total = $quantity * $unitPrice;
  151. if (! empty($salesTaxes)) {
  152. $taxRates = Adjustment::whereIn('id', $salesTaxes)->pluck('rate');
  153. $taxAmount = $taxRates->sum(function ($rate) use ($total) {
  154. return $total * ($rate / 100);
  155. });
  156. $total += $taxAmount;
  157. return money($total, CurrencyAccessor::getDefaultCurrency(), true)->format();
  158. }
  159. return money($total, CurrencyAccessor::getDefaultCurrency(), true)->format();
  160. }),
  161. ]),
  162. Forms\Components\Grid::make(6)
  163. ->inlineLabel()
  164. ->extraAttributes([
  165. 'class' => 'text-right pr-16',
  166. ])
  167. ->schema([
  168. Forms\Components\Group::make([
  169. Forms\Components\Placeholder::make('subtotal')
  170. ->label('Subtotal')
  171. ->content(function (Forms\Get $get) {
  172. $lineItems = $get('lineItems');
  173. $subtotal = collect($lineItems)
  174. ->sum(fn ($item) => ($item['quantity'] ?? 0) * ($item['unit_price'] ?? 0));
  175. return money($subtotal, CurrencyAccessor::getDefaultCurrency(), true)->format();
  176. }),
  177. Forms\Components\Placeholder::make('tax_total')
  178. ->label('Taxes')
  179. ->content(function (Forms\Get $get) {
  180. $lineItems = $get('lineItems');
  181. $totalTaxes = collect($lineItems)->reduce(function ($carry, $item) {
  182. $quantity = $item['quantity'] ?? 0;
  183. $unitPrice = $item['unit_price'] ?? 0;
  184. $salesTaxes = $item['salesTaxes'] ?? [];
  185. $lineTotal = $quantity * $unitPrice;
  186. $taxAmount = Adjustment::whereIn('id', $salesTaxes)
  187. ->pluck('rate')
  188. ->sum(fn ($rate) => $lineTotal * ($rate / 100));
  189. return $carry + $taxAmount;
  190. }, 0);
  191. return money($totalTaxes, CurrencyAccessor::getDefaultCurrency(), true)->format();
  192. }),
  193. Forms\Components\Placeholder::make('total')
  194. ->label('Total')
  195. ->content(function (Forms\Get $get) {
  196. $lineItems = $get('lineItems') ?? [];
  197. $subtotal = collect($lineItems)
  198. ->sum(fn ($item) => ($item['quantity'] ?? 0) * ($item['unit_price'] ?? 0));
  199. $totalTaxes = collect($lineItems)->reduce(function ($carry, $item) {
  200. $quantity = $item['quantity'] ?? 0;
  201. $unitPrice = $item['unit_price'] ?? 0;
  202. $salesTaxes = $item['salesTaxes'] ?? [];
  203. $lineTotal = $quantity * $unitPrice;
  204. $taxAmount = Adjustment::whereIn('id', $salesTaxes)
  205. ->pluck('rate')
  206. ->sum(fn ($rate) => $lineTotal * ($rate / 100));
  207. return $carry + $taxAmount;
  208. }, 0);
  209. $grandTotal = $subtotal + $totalTaxes;
  210. return money($grandTotal, CurrencyAccessor::getDefaultCurrency(), true)->format();
  211. }),
  212. ])->columnStart(6),
  213. ]),
  214. // Forms\Components\Repeater::make('lineItems')
  215. // ->relationship()
  216. // ->columns(8)
  217. // ->schema([
  218. // Forms\Components\Select::make('offering_id')
  219. // ->relationship('offering', 'name')
  220. // ->preload()
  221. // ->columnSpan(2)
  222. // ->searchable()
  223. // ->required()
  224. // ->live()
  225. // ->afterStateUpdated(function (Forms\Set $set, $state) {
  226. // $offeringId = $state;
  227. // $offeringRecord = Offering::with('salesTaxes')->find($offeringId);
  228. //
  229. // if ($offeringRecord) {
  230. // $set('description', $offeringRecord->description);
  231. // $set('unit_price', $offeringRecord->price);
  232. // $set('total', $offeringRecord->price);
  233. //
  234. // $salesTaxes = $offeringRecord->salesTaxes->map(function ($tax) {
  235. // return [
  236. // 'id' => $tax->id,
  237. // 'amount' => null, // Amount will be calculated dynamically
  238. // ];
  239. // })->toArray();
  240. //
  241. // $set('taxes', $salesTaxes);
  242. // }
  243. // }),
  244. // Forms\Components\TextInput::make('description')
  245. // ->columnSpan(3)
  246. // ->required(),
  247. // Forms\Components\TextInput::make('quantity')
  248. // ->required()
  249. // ->numeric()
  250. // ->live()
  251. // ->default(1),
  252. // Forms\Components\TextInput::make('unit_price')
  253. // ->live()
  254. // ->numeric()
  255. // ->default(0),
  256. // Forms\Components\Placeholder::make('total')
  257. // ->content(function (Forms\Get $get) {
  258. // $quantity = $get('quantity');
  259. // $unitPrice = $get('unit_price');
  260. //
  261. // if ($quantity && $unitPrice) {
  262. // return $quantity * $unitPrice;
  263. // }
  264. // }),
  265. // TableRepeater::make('taxes')
  266. // ->relationship()
  267. // ->columnSpanFull()
  268. // ->columnStart(6)
  269. // ->headers([
  270. // Header::make('')->width('200px'),
  271. // Header::make('')->width('50px')->align('right'),
  272. // ])
  273. // ->defaultItems(0)
  274. // ->schema([
  275. // Forms\Components\Select::make('id') // The ID of the adjustment being attached.
  276. // ->label('Tax Adjustment')
  277. // ->options(
  278. // Adjustment::query()
  279. // ->where('category', AdjustmentCategory::Tax)
  280. // ->pluck('name', 'id')
  281. // )
  282. // ->preload()
  283. // ->searchable()
  284. // ->required()
  285. // ->live(),
  286. // Forms\Components\Placeholder::make('amount')
  287. // ->hiddenLabel()
  288. // ->content(function (Forms\Get $get) {
  289. // $quantity = $get('../../quantity') ?? 0; // Get parent quantity
  290. // $unitPrice = $get('../../unit_price') ?? 0; // Get parent unit price
  291. // $rate = Adjustment::find($get('id'))->rate ?? 0;
  292. //
  293. // $total = $quantity * $unitPrice;
  294. //
  295. // return $total * ($rate / 100);
  296. // }),
  297. // ]),
  298. // ]),
  299. Forms\Components\Textarea::make('terms')
  300. ->columnSpanFull(),
  301. ]),
  302. Forms\Components\Section::make('Invoice Footer')
  303. ->collapsible()
  304. ->schema([
  305. Forms\Components\Textarea::make('footer')
  306. ->columnSpanFull(),
  307. ]),
  308. ]);
  309. }
  310. public static function table(Table $table): Table
  311. {
  312. return $table
  313. ->columns([
  314. Tables\Columns\TextColumn::make('client.name')
  315. ->numeric()
  316. ->sortable(),
  317. Tables\Columns\TextColumn::make('vendor.name')
  318. ->numeric()
  319. ->sortable(),
  320. Tables\Columns\TextColumn::make('type')
  321. ->searchable(),
  322. Tables\Columns\TextColumn::make('logo')
  323. ->searchable(),
  324. Tables\Columns\TextColumn::make('header')
  325. ->searchable(),
  326. Tables\Columns\TextColumn::make('subheader')
  327. ->searchable(),
  328. Tables\Columns\TextColumn::make('document_number')
  329. ->searchable(),
  330. Tables\Columns\TextColumn::make('order_number')
  331. ->searchable(),
  332. Tables\Columns\TextColumn::make('date')
  333. ->date()
  334. ->sortable(),
  335. Tables\Columns\TextColumn::make('due_date')
  336. ->date()
  337. ->sortable(),
  338. Tables\Columns\TextColumn::make('status')
  339. ->searchable(),
  340. Tables\Columns\TextColumn::make('currency_code')
  341. ->searchable(),
  342. Tables\Columns\TextColumn::make('subtotal')
  343. ->numeric()
  344. ->sortable(),
  345. Tables\Columns\TextColumn::make('tax_total')
  346. ->numeric()
  347. ->sortable(),
  348. Tables\Columns\TextColumn::make('discount_total')
  349. ->numeric()
  350. ->sortable(),
  351. Tables\Columns\TextColumn::make('total')
  352. ->numeric()
  353. ->sortable(),
  354. Tables\Columns\TextColumn::make('amount_paid')
  355. ->numeric()
  356. ->sortable(),
  357. Tables\Columns\TextColumn::make('amount_due')
  358. ->numeric()
  359. ->sortable(),
  360. Tables\Columns\TextColumn::make('created_by')
  361. ->numeric()
  362. ->sortable(),
  363. Tables\Columns\TextColumn::make('updated_by')
  364. ->numeric()
  365. ->sortable(),
  366. Tables\Columns\TextColumn::make('created_at')
  367. ->dateTime()
  368. ->sortable()
  369. ->toggleable(isToggledHiddenByDefault: true),
  370. Tables\Columns\TextColumn::make('updated_at')
  371. ->dateTime()
  372. ->sortable()
  373. ->toggleable(isToggledHiddenByDefault: true),
  374. ])
  375. ->filters([
  376. //
  377. ])
  378. ->actions([
  379. Tables\Actions\EditAction::make(),
  380. ])
  381. ->bulkActions([
  382. Tables\Actions\BulkActionGroup::make([
  383. Tables\Actions\DeleteBulkAction::make(),
  384. ]),
  385. ]);
  386. }
  387. public static function getRelations(): array
  388. {
  389. return [
  390. //
  391. ];
  392. }
  393. public static function getPages(): array
  394. {
  395. return [
  396. 'index' => Pages\ListDocuments::route('/'),
  397. 'create' => Pages\CreateDocument::route('/create'),
  398. 'edit' => Pages\EditDocument::route('/{record}/edit'),
  399. ];
  400. }
  401. }