Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.

DocumentResource.php 21KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367
  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. ->saveRelationshipsUsing(function (Document $document, array $state) {
  101. $document->lineItems()->delete();
  102. collect($state)->map(function ($lineItemData) use ($document) {
  103. $lineItem = $document->lineItems()->create([
  104. 'offering_id' => $lineItemData['offering_id'],
  105. 'description' => $lineItemData['description'],
  106. 'quantity' => $lineItemData['quantity'],
  107. 'unit_price' => $lineItemData['unit_price'],
  108. 'tax_total' => collect($lineItemData['salesTaxes'] ?? [])->sum(function ($taxId) use ($lineItemData) {
  109. $tax = Adjustment::find($taxId);
  110. return $tax ? ($lineItemData['quantity'] * $lineItemData['unit_price']) * ($tax->rate / 100) : 0;
  111. }),
  112. ]);
  113. $lineItem->taxes()->sync($lineItemData['salesTaxes'] ?? []);
  114. return $lineItem;
  115. });
  116. $document->refresh();
  117. $subtotal = $document->lineItems()->sum('subtotal') / 100;
  118. $taxTotal = $document->lineItems()->sum('tax_total') / 100;
  119. $discountTotal = $document->lineItems()->sum('discount_total') / 100;
  120. $grandTotal = $subtotal + $taxTotal - $discountTotal;
  121. $document->updateQuietly([
  122. 'subtotal' => $subtotal,
  123. 'tax_total' => $taxTotal,
  124. 'discount_total' => $discountTotal,
  125. 'total' => $grandTotal,
  126. ]);
  127. })
  128. ->headers([
  129. Header::make('Items')->width('20%'),
  130. Header::make('Description')->width('30%'),
  131. Header::make('Quantity')->width('10%'),
  132. Header::make('Price')->width('10%'),
  133. Header::make('Taxes')->width('20%'),
  134. Header::make('Amount')->width('10%')->align('right'),
  135. ])
  136. ->live()
  137. ->schema([
  138. Forms\Components\Select::make('offering_id')
  139. ->relationship('offering', 'name')
  140. ->preload()
  141. ->searchable()
  142. ->required()
  143. ->afterStateUpdated(function (Forms\Set $set, Forms\Get $get, $state) {
  144. $offeringId = $state;
  145. $offeringRecord = Offering::with('salesTaxes')->find($offeringId);
  146. if ($offeringRecord) {
  147. $set('description', $offeringRecord->description);
  148. $set('unit_price', $offeringRecord->price);
  149. $set('salesTaxes', $offeringRecord->salesTaxes->pluck('id')->toArray());
  150. $quantity = $get('quantity');
  151. $total = $quantity * $offeringRecord->price;
  152. // Calculate taxes and update total
  153. $taxAmount = $offeringRecord->salesTaxes->sum(fn ($tax) => $total * ($tax->rate / 100));
  154. $set('total', $total + $taxAmount);
  155. }
  156. }),
  157. Forms\Components\TextInput::make('description'),
  158. Forms\Components\TextInput::make('quantity')
  159. ->required()
  160. ->numeric()
  161. ->default(1),
  162. Forms\Components\TextInput::make('unit_price')
  163. ->hiddenLabel()
  164. ->numeric()
  165. ->default(0),
  166. Forms\Components\Select::make('salesTaxes')
  167. ->relationship('salesTaxes', 'name')
  168. ->preload()
  169. ->multiple()
  170. ->searchable(),
  171. Forms\Components\Placeholder::make('total')
  172. ->hiddenLabel()
  173. ->content(function (Forms\Get $get) {
  174. $quantity = $get('quantity') ?? 0;
  175. $unitPrice = $get('unit_price') ?? 0;
  176. $salesTaxes = $get('salesTaxes') ?? [];
  177. $total = $quantity * $unitPrice;
  178. if (! empty($salesTaxes)) {
  179. $taxRates = Adjustment::whereIn('id', $salesTaxes)->pluck('rate');
  180. $taxAmount = $taxRates->sum(function ($rate) use ($total) {
  181. return $total * ($rate / 100);
  182. });
  183. $total += $taxAmount;
  184. return money($total, CurrencyAccessor::getDefaultCurrency(), true)->format();
  185. }
  186. return money($total, CurrencyAccessor::getDefaultCurrency(), true)->format();
  187. }),
  188. ]),
  189. Forms\Components\Grid::make(6)
  190. ->schema([
  191. Forms\Components\ViewField::make('totals')
  192. ->columnStart(5)
  193. ->columnSpan(2)
  194. ->view('filament.forms.components.invoice-totals'),
  195. ]),
  196. // Forms\Components\Repeater::make('lineItems')
  197. // ->relationship()
  198. // ->columns(8)
  199. // ->schema([
  200. // Forms\Components\Select::make('offering_id')
  201. // ->relationship('offering', 'name')
  202. // ->preload()
  203. // ->columnSpan(2)
  204. // ->searchable()
  205. // ->required()
  206. // ->live()
  207. // ->afterStateUpdated(function (Forms\Set $set, $state) {
  208. // $offeringId = $state;
  209. // $offeringRecord = Offering::with('salesTaxes')->find($offeringId);
  210. //
  211. // if ($offeringRecord) {
  212. // $set('description', $offeringRecord->description);
  213. // $set('unit_price', $offeringRecord->price);
  214. // $set('total', $offeringRecord->price);
  215. //
  216. // $salesTaxes = $offeringRecord->salesTaxes->map(function ($tax) {
  217. // return [
  218. // 'id' => $tax->id,
  219. // 'amount' => null, // Amount will be calculated dynamically
  220. // ];
  221. // })->toArray();
  222. //
  223. // $set('taxes', $salesTaxes);
  224. // }
  225. // }),
  226. // Forms\Components\TextInput::make('description')
  227. // ->columnSpan(3)
  228. // ->required(),
  229. // Forms\Components\TextInput::make('quantity')
  230. // ->required()
  231. // ->numeric()
  232. // ->live()
  233. // ->default(1),
  234. // Forms\Components\TextInput::make('unit_price')
  235. // ->live()
  236. // ->numeric()
  237. // ->default(0),
  238. // Forms\Components\Placeholder::make('total')
  239. // ->content(function (Forms\Get $get) {
  240. // $quantity = $get('quantity');
  241. // $unitPrice = $get('unit_price');
  242. //
  243. // if ($quantity && $unitPrice) {
  244. // return $quantity * $unitPrice;
  245. // }
  246. // }),
  247. // TableRepeater::make('taxes')
  248. // ->relationship()
  249. // ->columnSpanFull()
  250. // ->columnStart(6)
  251. // ->headers([
  252. // Header::make('')->width('200px'),
  253. // Header::make('')->width('50px')->align('right'),
  254. // ])
  255. // ->defaultItems(0)
  256. // ->schema([
  257. // Forms\Components\Select::make('id') // The ID of the adjustment being attached.
  258. // ->label('Tax Adjustment')
  259. // ->options(
  260. // Adjustment::query()
  261. // ->where('category', AdjustmentCategory::Tax)
  262. // ->pluck('name', 'id')
  263. // )
  264. // ->preload()
  265. // ->searchable()
  266. // ->required()
  267. // ->live(),
  268. // Forms\Components\Placeholder::make('amount')
  269. // ->hiddenLabel()
  270. // ->content(function (Forms\Get $get) {
  271. // $quantity = $get('../../quantity') ?? 0; // Get parent quantity
  272. // $unitPrice = $get('../../unit_price') ?? 0; // Get parent unit price
  273. // $rate = Adjustment::find($get('id'))->rate ?? 0;
  274. //
  275. // $total = $quantity * $unitPrice;
  276. //
  277. // return $total * ($rate / 100);
  278. // }),
  279. // ]),
  280. // ]),
  281. Forms\Components\Textarea::make('terms')
  282. ->columnSpanFull(),
  283. ]),
  284. Forms\Components\Section::make('Invoice Footer')
  285. ->collapsible()
  286. ->schema([
  287. Forms\Components\Textarea::make('footer')
  288. ->columnSpanFull(),
  289. ]),
  290. ]);
  291. }
  292. public static function table(Table $table): Table
  293. {
  294. return $table
  295. ->columns([
  296. Tables\Columns\TextColumn::make('status')
  297. ->searchable(),
  298. Tables\Columns\TextColumn::make('due_date')
  299. ->date()
  300. ->sortable(),
  301. Tables\Columns\TextColumn::make('date')
  302. ->date()
  303. ->sortable(),
  304. Tables\Columns\TextColumn::make('document_number')
  305. ->label('Number')
  306. ->searchable(),
  307. Tables\Columns\TextColumn::make('client.name')
  308. ->numeric()
  309. ->sortable(),
  310. Tables\Columns\TextColumn::make('total')
  311. ->money(),
  312. Tables\Columns\TextColumn::make('amount_paid')
  313. ->money(),
  314. Tables\Columns\TextColumn::make('amount_due')
  315. ->money(),
  316. ])
  317. ->filters([
  318. //
  319. ])
  320. ->actions([
  321. Tables\Actions\EditAction::make(),
  322. ])
  323. ->bulkActions([
  324. Tables\Actions\BulkActionGroup::make([
  325. Tables\Actions\DeleteBulkAction::make(),
  326. ]),
  327. ]);
  328. }
  329. public static function getRelations(): array
  330. {
  331. return [
  332. //
  333. ];
  334. }
  335. public static function getPages(): array
  336. {
  337. return [
  338. 'index' => Pages\ListDocuments::route('/'),
  339. 'create' => Pages\CreateDocument::route('/create'),
  340. 'edit' => Pages\EditDocument::route('/{record}/edit'),
  341. ];
  342. }
  343. }