*/ public array $company = []; public float $taxRate = 20.0; /** New logo upload, validated on save. */ public $logo = null; public string $logoPath = ''; public function mount(): void { $this->authorize('site.manage'); $this->company = CompanyProfile::all(); $this->logoPath = (string) ($this->company['logo_path'] ?? ''); $this->taxRate = CompanyProfile::taxRate(); } public function saveCompany(): void { $this->authorize('site.manage'); $data = $this->validate([ 'company.name' => 'required|string|max:190', 'company.address' => 'required|string|max:190', 'company.postcode' => 'required|string|max:20', 'company.city' => 'required|string|max:120', 'company.country' => 'required|string|max:120', 'company.phone' => 'nullable|string|max:60', 'company.email' => 'nullable|email|max:190', 'company.website' => 'nullable|string|max:190', 'company.register_number' => 'nullable|string|max:60', 'company.register_court' => 'nullable|string|max:120', 'company.vat_id' => 'required|string|max:40', 'company.bank_name' => 'nullable|string|max:120', 'company.iban' => 'nullable|string|max:42', 'company.bic' => 'nullable|string|max:15', 'company.payment_days' => 'required|integer|min:0|max:180', 'company.payment_terms' => 'nullable|string|max:500', // PNG or WEBP only, and small. A PDF embeds whatever it is given, // and a four-megabyte photograph would be embedded in every invoice // ever rendered from then on. 'logo' => 'nullable|image|mimes:png,webp|max:1024', 'taxRate' => 'required|numeric|min:0|max:100', ]); if ($this->logo !== null) { $previous = $this->logoPath; $this->logoPath = $this->logo->store('company', 'public'); $this->company['logo_path'] = $this->logoPath; // The old file goes only once the new one is stored. The other // order loses the logo entirely if the upload fails, and every // invoice rendered afterwards is missing it. if ($previous !== '' && $previous !== $this->logoPath) { Storage::disk('public')->delete($previous); } $this->logo = null; } CompanyProfile::put($data['company']); Settings::set('company.tax_rate', (float) $data['taxRate']); $this->dispatch('notify', message: __('finance.company_saved')); } public function removeLogo(): void { $this->authorize('site.manage'); if ($this->logoPath !== '') { Storage::disk('public')->delete($this->logoPath); } $this->logoPath = ''; $this->company['logo_path'] = ''; CompanyProfile::put(['logo_path' => '']); $this->dispatch('notify', message: __('finance.logo_removed')); } public function render() { return view('livewire.admin.finance', [ 'series' => InvoiceSeries::query()->orderBy('kind')->get(), // What is still missing before an invoice may be issued at all. An // invoice without a name, an address or a VAT number is not a valid // invoice here, and issuing one is worse than refusing to. 'missing' => CompanyProfile::missingForInvoicing(), // Counted from the join table, never by asking the destinations. // A stat() on a network mount that has gone away blocks for the // mount's whole timeout, and this is a page an operator opens when // something is already wrong. 'targets' => ExportTarget::query()->orderBy('name')->get()->map(fn (ExportTarget $t) => [ 'model' => $t, 'pending' => Invoice::query()->whereDoesntHave('exports', fn ($q) => $q ->where('export_target_id', $t->id)->whereNotNull('exported_at'))->count(), 'failed' => InvoiceExport::query()->where('export_target_id', $t->id) ->whereNotNull('error')->whereNull('exported_at')->count(), ]), ]); } }