Add initial Code
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Auth\LoginRequest;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
use Laravel\Fortify\Features;
|
||||
|
||||
class AuthenticatedSessionController extends Controller
|
||||
{
|
||||
/**
|
||||
* Show the login page.
|
||||
*/
|
||||
public function create(Request $request): Response
|
||||
{
|
||||
return Inertia::render('auth/Login', [
|
||||
'canResetPassword' => Route::has('password.request'),
|
||||
'status' => $request->session()->get('status'),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an incoming authentication request.
|
||||
*/
|
||||
public function store(LoginRequest $request): RedirectResponse
|
||||
{
|
||||
$user = $request->validateCredentials();
|
||||
|
||||
if (Features::enabled(Features::twoFactorAuthentication()) && $user->hasEnabledTwoFactorAuthentication()) {
|
||||
$request->session()->put([
|
||||
'login.id' => $user->getKey(),
|
||||
'login.remember' => $request->boolean('remember'),
|
||||
]);
|
||||
|
||||
return to_route('two-factor.login');
|
||||
}
|
||||
|
||||
Auth::login($user, $request->boolean('remember'));
|
||||
|
||||
$request->session()->regenerate();
|
||||
|
||||
// Generate a sanctum token for API authentication in frontend
|
||||
$token = Auth::user()->createToken('api-token')->plainTextToken;
|
||||
|
||||
return redirect()->intended(route('dashboard', absolute: false))
|
||||
->with('token', $token);
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy an authenticated session.
|
||||
*/
|
||||
public function destroy(Request $request): RedirectResponse
|
||||
{
|
||||
// Revoke all tokens
|
||||
if ($request->user()) {
|
||||
$request->user()->tokens()->delete();
|
||||
}
|
||||
|
||||
Auth::guard('web')->logout();
|
||||
|
||||
$request->session()->invalidate();
|
||||
$request->session()->regenerateToken();
|
||||
|
||||
return redirect('/');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class EmailVerificationNotificationController extends Controller
|
||||
{
|
||||
/**
|
||||
* Send a new email verification notification.
|
||||
*/
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
if ($request->user()->hasVerifiedEmail()) {
|
||||
return redirect()->intended(route('dashboard', absolute: false));
|
||||
}
|
||||
|
||||
$request->user()->sendEmailVerificationNotification();
|
||||
|
||||
return back()->with('status', 'verification-link-sent');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class EmailVerificationPromptController extends Controller
|
||||
{
|
||||
/**
|
||||
* Show the email verification prompt page.
|
||||
*/
|
||||
public function __invoke(Request $request): RedirectResponse|Response
|
||||
{
|
||||
return $request->user()->hasVerifiedEmail()
|
||||
? redirect()->intended(route('dashboard', absolute: false))
|
||||
: Inertia::render('auth/VerifyEmail', ['status' => $request->session()->get('status')]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Auth\Events\PasswordReset;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Password;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\Rules;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class NewPasswordController extends Controller
|
||||
{
|
||||
/**
|
||||
* Show the password reset page.
|
||||
*/
|
||||
public function create(Request $request): Response
|
||||
{
|
||||
return Inertia::render('auth/ResetPassword', [
|
||||
'email' => $request->email,
|
||||
'token' => $request->route('token'),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an incoming new password request.
|
||||
*
|
||||
* @throws \Illuminate\Validation\ValidationException
|
||||
*/
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$request->validate([
|
||||
'token' => 'required',
|
||||
'email' => 'required|email',
|
||||
'password' => ['required', 'confirmed', Rules\Password::defaults()],
|
||||
]);
|
||||
|
||||
// Here we will attempt to reset the user's password. If it is successful we
|
||||
// will update the password on an actual user model and persist it to the
|
||||
// database. Otherwise we will parse the error and return the response.
|
||||
$status = Password::reset(
|
||||
$request->only('email', 'password', 'password_confirmation', 'token'),
|
||||
function ($user) use ($request) {
|
||||
$user->forceFill([
|
||||
'password' => Hash::make($request->password),
|
||||
'remember_token' => Str::random(60),
|
||||
])->save();
|
||||
|
||||
event(new PasswordReset($user));
|
||||
}
|
||||
);
|
||||
|
||||
// If the password was successfully reset, we will redirect the user back to
|
||||
// the application's home authenticated view. If there is an error we can
|
||||
// redirect them back to where they came from with their error message.
|
||||
if ($status == Password::PasswordReset) {
|
||||
return to_route('login')->with('status', __($status));
|
||||
}
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
'email' => [__($status)],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Password;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class PasswordResetLinkController extends Controller
|
||||
{
|
||||
/**
|
||||
* Show the password reset link request page.
|
||||
*/
|
||||
public function create(Request $request): Response
|
||||
{
|
||||
return Inertia::render('auth/ForgotPassword', [
|
||||
'status' => $request->session()->get('status'),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an incoming password reset link request.
|
||||
*
|
||||
* @throws \Illuminate\Validation\ValidationException
|
||||
*/
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$request->validate([
|
||||
'email' => 'required|email',
|
||||
]);
|
||||
|
||||
Password::sendResetLink(
|
||||
$request->only('email')
|
||||
);
|
||||
|
||||
return back()->with('status', __('A reset link will be sent if the account exists.'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Events\Registered;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Validation\Rules;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class RegisteredUserController extends Controller
|
||||
{
|
||||
/**
|
||||
* Show the registration page.
|
||||
*/
|
||||
public function create(): Response
|
||||
{
|
||||
return Inertia::render('auth/Register');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an incoming registration request.
|
||||
*
|
||||
* @throws \Illuminate\Validation\ValidationException
|
||||
*/
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'email' => 'required|string|lowercase|email|max:255|unique:'.User::class,
|
||||
'password' => ['required', 'confirmed', Rules\Password::defaults()],
|
||||
'avatar' => 'nullable|image|mimes:jpeg,png,jpg,gif,webp,svg|max:2048'
|
||||
]);
|
||||
|
||||
$userData = [
|
||||
'name' => $request->name,
|
||||
'email' => $request->email,
|
||||
'password' => Hash::make($request->password),
|
||||
];
|
||||
|
||||
// Handle avatar upload
|
||||
if ($request->hasFile('avatar')) {
|
||||
$avatarPath = $request->file('avatar')->store('avatars', 'public');
|
||||
$userData['avatar'] = $avatarPath;
|
||||
}
|
||||
|
||||
$user = User::create($userData);
|
||||
|
||||
event(new Registered($user));
|
||||
|
||||
Auth::login($user);
|
||||
|
||||
return to_route('dashboard');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Foundation\Auth\EmailVerificationRequest;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
|
||||
class VerifyEmailController extends Controller
|
||||
{
|
||||
/**
|
||||
* Mark the authenticated user's email address as verified.
|
||||
*/
|
||||
public function __invoke(EmailVerificationRequest $request): RedirectResponse
|
||||
{
|
||||
if ($request->user()->hasVerifiedEmail()) {
|
||||
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
|
||||
}
|
||||
|
||||
$request->fulfill();
|
||||
|
||||
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Contact;
|
||||
|
||||
class ContactController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
return Contact::with('customer')->get()->map(function ($contact) {
|
||||
return [
|
||||
'id' => $contact->id,
|
||||
'customerId' => $contact->customer_id,
|
||||
'firstName' => $contact->first_name,
|
||||
'lastName' => $contact->last_name,
|
||||
'email' => $contact->email,
|
||||
'phone' => $contact->phone,
|
||||
'position' => $contact->position,
|
||||
'isPrimary' => $contact->is_primary,
|
||||
'avatar' => $contact->avatar,
|
||||
'customer' => $contact->customer ? [
|
||||
'id' => $contact->customer->id,
|
||||
'companyName' => $contact->customer->company_name,
|
||||
'email' => $contact->customer->email,
|
||||
'phone' => $contact->customer->phone,
|
||||
] : null,
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
public function show($id)
|
||||
{
|
||||
$contact = Contact::with('customer')->findOrFail($id);
|
||||
|
||||
return [
|
||||
'id' => $contact->id,
|
||||
'customerId' => $contact->customer_id,
|
||||
'firstName' => $contact->first_name,
|
||||
'lastName' => $contact->last_name,
|
||||
'email' => $contact->email,
|
||||
'phone' => $contact->phone,
|
||||
'position' => $contact->position,
|
||||
'isPrimary' => $contact->is_primary,
|
||||
'avatar' => $contact->avatar,
|
||||
'customer' => $contact->customer ? [
|
||||
'id' => $contact->customer->id,
|
||||
'companyName' => $contact->customer->company_name,
|
||||
'email' => $contact->customer->email,
|
||||
'phone' => $contact->customer->phone,
|
||||
] : null,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
abstract class Controller
|
||||
{
|
||||
//
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Customer;
|
||||
use App\Support\ApiDataTransformer;
|
||||
|
||||
class CustomerController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$customers = Customer::with(['contacts' => function ($query) {
|
||||
$query->orderBy('is_primary', 'desc');
|
||||
}, 'paymentTerms'])->get();
|
||||
|
||||
return $customers->map(function ($customer) {
|
||||
$customerArray = $customer->toArray();
|
||||
$customerArray['payment_terms'] = $customer->paymentTerms->toArray();
|
||||
unset($customerArray['payment_terms_id']);
|
||||
return ApiDataTransformer::snakeToCamel($customerArray);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,636 @@
|
||||
<?php
|
||||
// TODO: finish XML creation
|
||||
// TODO: adapt models, views and company settings accordingly
|
||||
// TODO: define routes export <-> download, format = xml | pdf
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Invoice;
|
||||
use Barryvdh\DomPDF\Facade\Pdf;
|
||||
use horstoeko\zugferdlaravel\Facades\ZugferdLaravel;
|
||||
use horstoeko\zugferd\codelists\ZugferdInvoiceType;
|
||||
use horstoeko\zugferd\codelists\ZugferdUnitCodes;
|
||||
use horstoeko\zugferd\codelists\ZugferdVatCategoryCodes;
|
||||
use horstoeko\zugferd\codelists\ZugferdVatTypeCodes;
|
||||
use horstoeko\zugferd\codelists\ZugferdDocumentType;
|
||||
use App\Models\LineItem;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use App\Support\ApiDataTransformer;
|
||||
use DateTime;
|
||||
use tbQuar\Facades\Quar;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class InvoiceController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$invoices = Invoice::with([
|
||||
'customer.contacts' => function ($query) {
|
||||
$query->orderBy('is_primary', 'desc');
|
||||
},
|
||||
'items' => function ($query) {
|
||||
$query->orderBy('position', 'asc');
|
||||
},
|
||||
'customer.paymentTerms'
|
||||
])
|
||||
->orderBy('invoice_date', 'asc')
|
||||
->orderByRaw("CAST(SUBSTRING(nr, 4) AS UNSIGNED) ASC")
|
||||
->get();
|
||||
|
||||
return $invoices->map(function ($invoice) {
|
||||
$invoiceArray = $invoice->toArray();
|
||||
$invoiceArray['customer']['payment_terms'] = $invoice->customer->paymentTerms->toArray();
|
||||
unset($invoiceArray['customer']['payment_terms_id']);
|
||||
return ApiDataTransformer::snakeToCamel($invoiceArray);
|
||||
});
|
||||
}
|
||||
|
||||
public function single($id)
|
||||
{
|
||||
$invoice = Invoice::with([
|
||||
'customer.contacts' => function ($query) {
|
||||
$query->orderBy('is_primary', 'desc');
|
||||
},
|
||||
'items' => function ($query) {
|
||||
$query->orderBy('position', 'asc');
|
||||
},
|
||||
'customer.paymentTerms'
|
||||
])->findOrFail($id);
|
||||
|
||||
$invoiceArray = $invoice->toArray();
|
||||
$invoiceArray['customer']['payment_terms'] = $invoice->customer->paymentTerms->toArray();
|
||||
unset($invoiceArray['customer']['payment_terms_id']);
|
||||
return ApiDataTransformer::snakeToCamel($invoiceArray);
|
||||
}
|
||||
|
||||
|
||||
public function preview($id)
|
||||
{
|
||||
$invoice = Invoice::with([
|
||||
'customer.contacts' => function ($query) {
|
||||
$query->orderBy('is_primary', 'desc');
|
||||
},
|
||||
'items' => function ($query) {
|
||||
$query->orderBy('position', 'asc');
|
||||
},
|
||||
'customer.paymentTerms'
|
||||
])->findOrFail($id);
|
||||
|
||||
$invoiceArray = $invoice->toArray();
|
||||
$invoiceArray['customer']['payment_terms'] = $invoice->customer->paymentTerms->toArray();
|
||||
unset($invoiceArray['customer']['payment_terms_id']);
|
||||
|
||||
$giroCode = $this->generateGiroCode($invoice->totalAmount, $invoice->nr, $invoice->title);
|
||||
|
||||
return view('invoice', [
|
||||
'invoice' => ApiDataTransformer::snakeToCamel($invoiceArray),
|
||||
'isPDF' => false,
|
||||
'fontPath' => '/storage/fonts',
|
||||
'giroCode' => $giroCode,
|
||||
]);
|
||||
}
|
||||
|
||||
// https://www.itsolutionstuff.com/post/laravel-dompdf-table-with-page-break-exampleexample.html
|
||||
public function exportPdf($id)
|
||||
{
|
||||
$invoice = Invoice::with([
|
||||
'customer.contacts' => function ($query) {
|
||||
$query->orderBy('is_primary', 'desc');
|
||||
},
|
||||
'items' => function ($query) {
|
||||
$query->orderBy('position', 'asc');
|
||||
},
|
||||
'customer.paymentTerms'
|
||||
])->findOrFail($id);
|
||||
|
||||
$invoiceArray = $invoice->toArray();
|
||||
$invoiceArray['customer']['payment_terms'] = $invoice->customer->paymentTerms->toArray();
|
||||
unset($invoiceArray['customer']['payment_terms_id']);
|
||||
|
||||
$giroCode = $this->generateGiroCode($invoice->totalAmount, $invoice->nr, $invoice->title);
|
||||
|
||||
$pdf = Pdf::loadView('invoice', [
|
||||
'invoice' => ApiDataTransformer::snakeToCamel($invoiceArray),
|
||||
'isPDF' => true,
|
||||
'fontPath' => public_path('storage/fonts/'),
|
||||
'giroCode' => $giroCode,
|
||||
]);
|
||||
$pdf->setOption(['isRemoteEnabled' => true]);
|
||||
$pdf->setOption(['fontDir' => public_path('storage/fonts/')]);
|
||||
$pdf->setOption(['fontCache' => public_path('storage/fonts/')]);
|
||||
return $pdf->stream($invoice->nr . '.pdf');
|
||||
return $pdf->download($this->getFilename($invoice) . '.pdf');
|
||||
}
|
||||
|
||||
public function exportXml($id)
|
||||
{
|
||||
$invoice = Invoice::with([
|
||||
'customer.contacts' => function ($query) {
|
||||
$query->orderBy('is_primary', 'desc');
|
||||
},
|
||||
'items' => function ($query) {
|
||||
$query->orderBy('position', 'asc');
|
||||
},
|
||||
'customer.paymentTerms'
|
||||
])->findOrFail($id);
|
||||
|
||||
$invoiceArray = $invoice->toArray();
|
||||
$invoiceArray['customer']['payment_terms'] = $invoice->customer->paymentTerms->toArray();
|
||||
unset($invoiceArray['customer']['payment_terms_id']);
|
||||
|
||||
// Standard
|
||||
// https://easyfirma.net/e-rechnung/zugferd/bt-felder
|
||||
|
||||
// ZugferdDocumentBuilder.php
|
||||
// https://github.com/horstoeko/zugferd/blob/aaa3e8d0d775cd1621a65aac81ceab621f0ac115/src/ZugferdDocumentBuilder.php
|
||||
|
||||
// EN16931-Beispiel
|
||||
// https://github.com/horstoeko/zugferd/blob/a192d3e0eb4ffd23e38eae419097d179c4ffb0cf/examples/01_ZugferdDocumentBuilder_EN16931.php
|
||||
|
||||
|
||||
$document = ZugferdLaravel::createDocumentInEN16931Profile();
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Document information
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
// Set main information about this document.
|
||||
//
|
||||
// string $documentNo __BT-1, From MINIMUM__ The document no issued by the seller
|
||||
// string $documentTypeCode __BT-3, From MINIMUM__ The type of the document, See \horstoeko\codelists\ZugferdInvoiceType for details
|
||||
// DateTimeInterface $documentDate __BT-2, From MINIMUM__ Date of invoice. The date when the document was issued by the seller
|
||||
// string $invoiceCurrency __BT-5, From MINIMUM__ Code for the invoice currency
|
||||
// string|null $documentName __BT-X-2, From EXTENDED__ Document Type. The documenttype (free text)
|
||||
// string|null $documentLanguage __BT-X-4, From EXTENDED__ Language indicator. The language code in which the document was written
|
||||
// DateTimeInterface|null $effectiveSpecifiedPeriod __BT-X-6-000, From EXTENDED__ The contractual due date of the invoice
|
||||
$document->setDocumentInformation(
|
||||
$invoice->nr,
|
||||
ZugferdDocumentType::COMMERCIAL_INVOICE,
|
||||
new DateTime($invoice->invoice_date),
|
||||
'EUR',
|
||||
$invoice->title,
|
||||
'de',
|
||||
new DateTime($invoice->due_date),
|
||||
);
|
||||
|
||||
// TODO: start_service_date end_service_date
|
||||
$document->setDocumentBillingPeriod(DateTime::createFromFormat('Ymd', '20250101'), DateTime::createFromFormat('Ymd', '20250131'), '01.01.2025 - 31.01.2025');
|
||||
|
||||
|
||||
// Add a payment term
|
||||
//
|
||||
// string|null $description __BT-20, From _BASIC WL__ A text description of the payment terms that apply to the payment amount due (including a description of possible penalties). Note: This element can contain multiple lines and multiple conditions.
|
||||
// DateTimeInterface|null $dueDate __BT-9, From BASIC WL__ The date by which payment is due Note: The payment due date reflects the net payment due date. In the case of partial payments, this indicates the first due date of a net payment. The corresponding description of more complex payment terms can be given in BT-20.
|
||||
// string|null $directDebitMandateID __BT-89, From BASIC WL__ Unique identifier assigned by the payee to reference the direct debit authorization.
|
||||
// float|null $partialPaymentAmount __BT-X-275, From EXTENDED__ Amount of the partial payment
|
||||
$document->addDocumentPaymentTerm(
|
||||
$invoice->paymentTerms->name == 'prepaid' ?
|
||||
'Vorkasse' : ($invoice->paymentTerms->name == 'on_receipt' ?
|
||||
'Bei Rechnungserhalt' :
|
||||
$invoice->paymentTerms->days . ' Tage nach Rechnungserhalt'),
|
||||
new DateTime($invoice->due_date)
|
||||
);
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Seller information
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
|
||||
// string $name __BT-27, From MINIMUM__ The full formal name under which the seller is registered in the National Register of Legal Entities, Taxable Person or otherwise acting as person(s)
|
||||
// string|null $id __BT-29, From BASIC WL__ An identifier of the seller. In many systems, seller identification is key information. Multiple seller IDs can be assigned or specified. They can be differentiated by using different identification schemes. If no scheme is given, it should be known to the buyer and seller, e.g. a previously exchanged, buyer-assigned identifier of the seller
|
||||
// string|null $description __BT-33, From EN 16931__ Further legal information that is relevant for the seller
|
||||
$document->setDocumentSeller("Tooloop Multimedia Daniel Stock", "549910");
|
||||
|
||||
// string|null $globalID __BT-29/BT-29-0, From BASIC WL__ The seller's identifier identification scheme is an identifier uniquely assigned to a seller by a global registration organization.
|
||||
// string|null $globalIDType __BT-29-1, From BASIC WL__ If the identifier is used for the identification scheme, it must be selected from the entries in the list published by the ISO / IEC 6523 Maintenance Agency.
|
||||
$document->addDocumentSellerGlobalId("4000001123452", "0088");
|
||||
|
||||
// string|null $taxRegType __BT-31-0/BT-32-0, From MINIMUM/EN 16931__ Type of tax number of the seller (FC = Tax number, VA = Sales tax identification number)
|
||||
// string|null $taxRegId __BT-31/32, From MINIMUM/EN 16931__ Tax number of the seller or sales tax identification number of the seller
|
||||
$document->addDocumentSellerTaxRegistration("FC", "201/113/40209");
|
||||
$document->addDocumentSellerTaxRegistration("VA", "DE123456789");
|
||||
|
||||
// string|null $lineOne __BT-35, From BASIC WL__ The main line in the sellers address. This is usually the street name and house number or the post office box
|
||||
// string|null $lineTwo __BT-36, From BASIC WL__ Line 2 of the seller's address. This is an additional address line in an address that can be used to provide additional details in addition to the main line used to provide additional details in addition to the main line
|
||||
// string|null $lineThree __BT-162, From BASIC WL__ Line 3 of the seller's address. This is an additional address line in an address that can be used to provide additional details in addition to the main line
|
||||
// string|null $postCode __BT-38, From BASIC WL__ Identifier for a group of properties, such as a zip code
|
||||
// string|null $city __BT-37, From BASIC WL__ Usual name of the city or municipality in which the seller's address is located
|
||||
// string|null $country __BT-40, From MINIMUM__ Code used to identify the country. If no tax agent is specified, this is the country in which the sales tax is due. The lists of approved countries are maintained by the EN ISO 3166-1 Maintenance Agency “Codes for the representation of names of countries and their subdivisions”
|
||||
// string|null $subDivision __BT-39, From BASIC WL__ The sellers state
|
||||
$document->setDocumentSellerAddress(
|
||||
"Rehmstraße 4",
|
||||
"",
|
||||
"",
|
||||
"86161",
|
||||
"Augsburg",
|
||||
"DE"
|
||||
);
|
||||
$document->setDocumentSellerContact(
|
||||
"Daniel Stock",
|
||||
"Geschäftsführer",
|
||||
"+49-111-2222222",
|
||||
"+49-111-3333333",
|
||||
"info@tooloop.de"
|
||||
);
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Buyer information
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
// string $name __BT-44, From MINIMUM__ The full name of the buyer
|
||||
// string|null $id __BT-46, From BASIC WL__ An identifier of the buyer. In many systems, buyer identification is key information. Multiple buyer IDs can be assigned or specified. They can be differentiated by using different identification schemes. If no scheme is given, it should be known to the buyer and buyer, e.g. a previously exchanged, seller-assigned identifier of the buyer
|
||||
// string|null $description __BT-X-334, From EXTENDED__ Further legal information about the buyer
|
||||
// TODO: use invoice customerNr
|
||||
$document->setDocumentBuyer($invoice->customer->company_name, "###KUNDENNUMMER");
|
||||
|
||||
// Set contact of the buyer party
|
||||
//
|
||||
// string|null $contactPersonName __BT-56, From EN 16931__ Contact point for a legal entity, such as a personal name of the contact person
|
||||
// string|null $contactDepartmentName __BT-56-0, From EN 16931__ Contact point for a legal entity, such as a name of the department or office
|
||||
// string|null $contactPhoneNo __BT-57, From EN 16931__ A telephone number for the contact point
|
||||
// string|null $contactFaxNo __BT-X-115, From EXTENDED__ A fax number of the contact point
|
||||
// string|null $contactEmailAddress __BT-58, From EN 16931__ An e-mail address of the contact point
|
||||
$document->setDocumentBuyerContact(
|
||||
// TODO: use invoice trade contact field
|
||||
$invoice->customer->contacts[0]->first_name . ' ' . $invoice->customer->contacts[0]->last_name,
|
||||
null,
|
||||
$invoice->customer->contacts[0]->phone,
|
||||
null,
|
||||
$invoice->customer->contacts[0]->email
|
||||
);
|
||||
|
||||
// Leitweg-ID (nur bei Behörden)
|
||||
// string $buyerReference __BT-10, From MINIMUM__ An identifier assigned by the buyer and used for internal routing
|
||||
$document->setDocumentBuyerReference("34676-342323");
|
||||
|
||||
// string|null $lineOne __BT-50, From BASIC WL__ The main line in the buyers address. This is usually the street name and house number or the post office box
|
||||
// string|null $lineTwo __BT-51, From BASIC WL__ Line 2 of the buyers address. This is an additional address line in an address that can be used to provide additional details in addition to the main line
|
||||
// string|null $lineThree __BT-163, From BASIC WL__ Line 3 of the buyers address. This is an additional address line in an address that can be used to provide additional details in addition to the main line
|
||||
// string|null $postCode __BT-53, From BASIC WL__ Identifier for a group of properties, such as a zip code
|
||||
// string|null $city __BT-52, From BASIC WL__ Usual name of the city or municipality in which the buyers address is located
|
||||
// string|null $country __BT-55, From BASIC WL__ Code used to identify the country. If no tax agent is specified, this is the country in which the sales tax is due. The lists of approved countries are maintained by the EN ISO 3166-1 Maintenance Agency “Codes for the representation of names of countries and their subdivisions”
|
||||
// string|null $subDivision __BT-54, From BASIC WL__ The buyers state
|
||||
$document->setDocumentBuyerAddress(
|
||||
$mappedBillingAddress['line_one'] ?? '',
|
||||
$mappedBillingAddress['line_two'] ?? '',
|
||||
"",
|
||||
$mappedBillingAddress['postal_code'] ?? '',
|
||||
$mappedBillingAddress['city'] ?? '',
|
||||
$mappedBillingAddress['country_code'] ?? ''
|
||||
);
|
||||
|
||||
foreach ($invoice->items as $item) {
|
||||
$document->addNewPosition($item->position);
|
||||
$document->setDocumentPositionProductDetails(
|
||||
$item->title,
|
||||
$item->description
|
||||
);
|
||||
$document->setDocumentPositionNetPrice($item->price);
|
||||
|
||||
// float $billedQuantity __BT-129, From BASIC__ The quantity of individual items (goods or services) billed in the relevant line
|
||||
// string $billedQuantityUnitCode __BT-130, From BASIC__ The unit of measure applicable to the amount billed
|
||||
// float|null $chargeFreeQuantity __BT-X-46, From EXTENDED__ Quantity, free of charge
|
||||
// string|null $chargeFreeQuantityUnitCpde __BT-X-46-0, From EXTENDED__ Unit of measure code for the quantity free of charge
|
||||
// float|null $packageQuantity __BT-X-47, From EXTENDED__ Number of packages
|
||||
// string|null $packageQuantityUnitCode __BT-X-47-0, From EXTENDED__ Unit of measure code for number of packages
|
||||
$document->setDocumentPositionQuantity(
|
||||
$item->quantity,
|
||||
ZugferdUnitCodes::REC20_PIECE
|
||||
);
|
||||
|
||||
$document->addDocumentPositionTax(
|
||||
ZugferdVatCategoryCodes::STAN_RATE,
|
||||
ZugferdVatTypeCodes::VALUE_ADDED_TAX,
|
||||
19
|
||||
);
|
||||
|
||||
$document->setDocumentPositionLineSummation($item->quantity * $item->price);
|
||||
}
|
||||
|
||||
// Get the XML content as a string
|
||||
$xmlContent = $document->getContent();
|
||||
|
||||
// Create a response with the XML content
|
||||
$response = response($xmlContent, 200, [
|
||||
'Content-Type' => 'application/xml',
|
||||
// 'Content-Disposition' => 'attachment; filename="'.$this->getFilename($invoice) . '.xml"',
|
||||
]);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Stores api input to the database
|
||||
* Expects all date in camelCase but will convert and store in snake_case
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
// Validate input data (expecting camelCase)
|
||||
$validatedData = $request->validate([
|
||||
'nr' => 'nullable|string',
|
||||
'invoiceDate' => 'required|date',
|
||||
'dueDate' => 'required|date',
|
||||
'serviceStartDate' => 'nullable|date',
|
||||
'serviceEndDate' => 'nullable|date',
|
||||
'isRecurring' => 'boolean',
|
||||
'isPartialService' => 'boolean',
|
||||
'customerId' => 'nullable|integer',
|
||||
'billingData' => 'nullable|array',
|
||||
'billingData.companyName' => 'required_with:billingData|string',
|
||||
'billingData.vatId' => 'nullable|string',
|
||||
'billingData.billingAddress' => 'nullable|array',
|
||||
'billingData.billingAddress.lineOne' => 'nullable|string',
|
||||
'billingData.billingAddress.lineTwo' => 'nullable|string',
|
||||
'billingData.billingAddress.city' => 'nullable|string',
|
||||
'billingData.billingAddress.postalCode' => 'nullable|string',
|
||||
'billingData.billingAddress.countryCode' => 'nullable|string',
|
||||
'billingData.contactFirstName' => 'nullable|string',
|
||||
'billingData.contactLastName' => 'nullable|string',
|
||||
'billingData.paymentTerms' => 'nullable|array',
|
||||
'paymentStatus' => 'required|string',
|
||||
'totalAmount' => 'required|numeric',
|
||||
'title' => 'nullable|string',
|
||||
'text' => 'nullable|string',
|
||||
'items' => 'nullable|array',
|
||||
'items.*.position' => 'required|integer',
|
||||
'items.*.title' => 'nullable|string',
|
||||
'items.*.description' => 'nullable|string',
|
||||
'items.*.quantity' => 'nullable|numeric',
|
||||
'items.*.unit' => 'nullable|string',
|
||||
'items.*.price' => 'nullable|numeric',
|
||||
]);
|
||||
|
||||
// Convert camelCase to snake_case
|
||||
$snakeCaseData = ApiDataTransformer::camelToSnake($validatedData);
|
||||
|
||||
// Generate invoice number only if paymentStatus is not 'draft'
|
||||
if ($snakeCaseData['payment_status'] !== 'draft' && empty($snakeCaseData['nr'])) {
|
||||
$snakeCaseData['nr'] = $this->generateInvoiceNumber();
|
||||
}
|
||||
|
||||
DB::beginTransaction();
|
||||
|
||||
try {
|
||||
// Prepare data for invoice creation (excluding items)
|
||||
$invoiceData = $snakeCaseData;
|
||||
unset($invoiceData['items']);
|
||||
|
||||
|
||||
|
||||
// Create invoice with snake_case data
|
||||
$invoice = Invoice::create($invoiceData);
|
||||
|
||||
// Create line items if present
|
||||
if (!empty($snakeCaseData['items'])) {
|
||||
foreach ($snakeCaseData['items'] as $item) {
|
||||
LineItem::create([
|
||||
'invoice_id' => $invoice->id,
|
||||
...$item
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
DB::commit();
|
||||
|
||||
// Return created invoice with items in camelCase
|
||||
$invoice = Invoice::with(['items', 'customer.contacts'])->find($invoice->id);
|
||||
return response()->json(
|
||||
ApiDataTransformer::snakeToCamel($invoice->toArray()),
|
||||
201
|
||||
);
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
return response()->json([
|
||||
'message' => 'Fehler beim Speichern der Rechnung',
|
||||
'error' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function update(Request $request, $id)
|
||||
{
|
||||
// Validate input data (expecting camelCase)
|
||||
$validatedData = $request->validate([
|
||||
'nr' => 'nullable|string',
|
||||
'invoiceDate' => 'required|date',
|
||||
'dueDate' => 'required|date',
|
||||
'serviceStartDate' => 'nullable|date',
|
||||
'serviceEndDate' => 'nullable|date',
|
||||
'isRecurring' => 'boolean',
|
||||
'isPartialService' => 'boolean',
|
||||
'customerId' => 'nullable|integer',
|
||||
'billingData' => 'nullable|array',
|
||||
'billingData.companyName' => 'required_with:billingData|string',
|
||||
'billingData.vatId' => 'nullable|string',
|
||||
'billingData.billingAddress' => 'nullable|array',
|
||||
'billingData.billingAddress.lineOne' => 'nullable|string',
|
||||
'billingData.billingAddress.lineTwo' => 'nullable|string',
|
||||
'billingData.billingAddress.city' => 'nullable|string',
|
||||
'billingData.billingAddress.postalCode' => 'nullable|string',
|
||||
'billingData.billingAddress.countryCode' => 'nullable|string',
|
||||
'billingData.contactFirstName' => 'nullable|string',
|
||||
'billingData.contactLastName' => 'nullable|string',
|
||||
'billingData.paymentTerms' => 'nullable|array',
|
||||
'paymentStatus' => 'required|string',
|
||||
'totalAmount' => 'required|numeric',
|
||||
'title' => 'nullable|string',
|
||||
'text' => 'nullable|string',
|
||||
'items' => 'nullable|array',
|
||||
'items.*.id' => 'nullable|integer',
|
||||
'items.*.position' => 'required|integer',
|
||||
'items.*.title' => 'nullable|string',
|
||||
'items.*.description' => 'nullable|string',
|
||||
'items.*.quantity' => 'nullable|numeric',
|
||||
'items.*.unit' => 'nullable|string',
|
||||
'items.*.price' => 'nullable|numeric',
|
||||
]);
|
||||
|
||||
// Convert camelCase to snake_case
|
||||
$snakeCaseData = ApiDataTransformer::camelToSnake($validatedData);
|
||||
|
||||
// Generate invoice number if paymentStatus is not 'draft' and no invoice number exists
|
||||
if ($snakeCaseData['payment_status'] !== 'draft' && empty($snakeCaseData['nr'])) {
|
||||
$snakeCaseData['nr'] = $this->generateInvoiceNumber();
|
||||
}
|
||||
|
||||
DB::beginTransaction();
|
||||
|
||||
try {
|
||||
// Find invoice
|
||||
$invoice = Invoice::findOrFail($id);
|
||||
|
||||
// Prepare data for invoice update (excluding items)
|
||||
$invoiceData = $snakeCaseData;
|
||||
unset($invoiceData['items']);
|
||||
|
||||
// Update invoice with snake_case data
|
||||
$invoice->update($invoiceData);
|
||||
|
||||
// Handle items separately
|
||||
$existingItemIds = [];
|
||||
|
||||
if (!empty($snakeCaseData['items'])) {
|
||||
foreach ($snakeCaseData['items'] as $item) {
|
||||
// Remove unit from item data if it's empty to let the database use the default value
|
||||
if (empty($item['unit'])) {
|
||||
unset($item['unit']);
|
||||
}
|
||||
if (isset($item['id']) && $item['id'] > 0) {
|
||||
// Update existing item
|
||||
$lineItem = LineItem::findOrFail($item['id']);
|
||||
$lineItem->update($item);
|
||||
$existingItemIds[] = $item['id'];
|
||||
} else {
|
||||
// Create new item
|
||||
$lineItem = LineItem::create([
|
||||
'invoice_id' => $invoice->id,
|
||||
...$item
|
||||
]);
|
||||
$existingItemIds[] = $lineItem->id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Delete items that are no longer in the data
|
||||
LineItem::where('invoice_id', $invoice->id)
|
||||
->whereNotIn('id', $existingItemIds)
|
||||
->delete();
|
||||
|
||||
DB::commit();
|
||||
|
||||
// Return updated invoice with items in camelCase
|
||||
$invoice = Invoice::with(['items', 'customer.contacts'])->find($invoice->id);
|
||||
return response()->json(
|
||||
ApiDataTransformer::snakeToCamel($invoice->toArray()),
|
||||
200
|
||||
);
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
return response()->json([
|
||||
'message' => 'Rechnung konnte nicht aktualisiert werden',
|
||||
'error' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function delete(Request $request, $id)
|
||||
{
|
||||
DB::beginTransaction();
|
||||
|
||||
try {
|
||||
$invoice = Invoice::findOrFail($id);
|
||||
LineItem::where('invoice_id', $invoice->id)->delete();
|
||||
$invoice->delete();
|
||||
DB::commit();
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Rechnung gelöscht'
|
||||
], 200);
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
return response()->json([
|
||||
'message' => 'Rechnung konnte nicht gelöscht werden',
|
||||
'error' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the next available invoice number
|
||||
*/
|
||||
protected function generateInvoiceNumber()
|
||||
{
|
||||
$settings = \App\Models\Setting::firstOrCreate([]);
|
||||
$lastInvoice = Invoice::orderByRaw("CAST(SUBSTRING(nr, 4) AS UNSIGNED) DESC")
|
||||
->first();
|
||||
|
||||
if ($lastInvoice) {
|
||||
$lastNumber = (int) str_replace('RE-', '', $lastInvoice->nr);
|
||||
$newNumber = $lastNumber + 1;
|
||||
} else {
|
||||
$newNumber = $settings->invoice_number_start;
|
||||
}
|
||||
|
||||
return str_replace('{number}', $newNumber, $settings->invoice_number_format);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a GiroCode for the invoice
|
||||
* https://www.girocode.eu/en/home/
|
||||
* @return string Base64 encoded SVG image
|
||||
*/
|
||||
protected function generateGiroCode($amount, $invoiceNumber, $invoiceTitle)
|
||||
{
|
||||
$content = "BCD
|
||||
002
|
||||
2
|
||||
SCT
|
||||
AUGSDE77XXX
|
||||
Daniel Stock
|
||||
DE40720500000251512513
|
||||
EUR$amount
|
||||
|
||||
$invoiceNumber $invoiceTitle";
|
||||
|
||||
// Convert the content to ISO-8859-1
|
||||
$content = $this->transliterateString($content);
|
||||
|
||||
// Generate the QR code
|
||||
$giroCode = Quar::format('svg')->generate($content);
|
||||
|
||||
return base64_encode($giroCode);
|
||||
}
|
||||
|
||||
protected function transliterateString($string)
|
||||
{
|
||||
// Define replacements for special characters
|
||||
$replacements = [
|
||||
'ä' => 'ae',
|
||||
'ä' => 'ae',
|
||||
'ö' => 'oe',
|
||||
'ö' => 'oe',
|
||||
'ü' => 'ue',
|
||||
'ü' => 'ue',
|
||||
'Ä' => 'Ae',
|
||||
'Ä' => 'Ae',
|
||||
'Ö' => 'Oe',
|
||||
'Ö' => 'Oe',
|
||||
'Ü' => 'Ue',
|
||||
'Ü' => 'Ue',
|
||||
'ß' => 'ss',
|
||||
'„' => '"',
|
||||
'“' => '"',
|
||||
'‚' => "'",
|
||||
'‘' => "'",
|
||||
'’' => "'",
|
||||
'€' => 'EUR',
|
||||
// Add more replacements as needed
|
||||
];
|
||||
|
||||
// Replace special characters
|
||||
foreach ($replacements as $search => $replace) {
|
||||
$string = str_replace($search, $replace, $string);
|
||||
}
|
||||
|
||||
// Transliterate remaining characters
|
||||
$string = iconv('UTF-8', 'ISO-8859-1//IGNORE', $string);
|
||||
|
||||
return $string;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Generate a filename for the invoice export
|
||||
* Format: YYYY-MM-DD {invoice.nr} {invoice.title}
|
||||
*/
|
||||
protected function getFilename(Invoice $invoice)
|
||||
{
|
||||
$date = \DateTime::createFromFormat("Y-m-d", $invoice->invoice_date);
|
||||
$formattedDate = $date ? $date->format('Y-m-d') : (new DateTime())->format('Y-m-d');
|
||||
return "{$formattedDate} {$invoice->nr} {$invoice->title}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\PaymentTerms;
|
||||
use App\Support\ApiDataTransformer;
|
||||
|
||||
class PaymentTermsController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$paymentTerms = PaymentTerms::get();
|
||||
|
||||
return $paymentTerms->map(function ($paymentTerms) {
|
||||
return ApiDataTransformer::snakeToCamel($paymentTerms->toArray());
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Setting;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class SettingController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
return Setting::firstOrCreate([]);
|
||||
}
|
||||
|
||||
public function update(Request $request)
|
||||
{
|
||||
$validatedData = $request->validate([
|
||||
'invoice_number_format' => 'required|string',
|
||||
'invoice_number_start' => 'required|integer',
|
||||
]);
|
||||
|
||||
$setting = Setting::firstOrCreate([]);
|
||||
$setting->update($validatedData);
|
||||
|
||||
return response()->json($setting, 200);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Settings;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Validation\Rules\Password;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class PasswordController extends Controller
|
||||
{
|
||||
/**
|
||||
* Show the user's password settings page.
|
||||
*/
|
||||
public function edit(): Response
|
||||
{
|
||||
return Inertia::render('settings/Password');
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the user's password.
|
||||
*/
|
||||
public function update(Request $request): RedirectResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'current_password' => ['required', 'current_password'],
|
||||
'password' => ['required', Password::defaults(), 'confirmed'],
|
||||
]);
|
||||
|
||||
$request->user()->update([
|
||||
'password' => Hash::make($validated['password']),
|
||||
]);
|
||||
|
||||
return back();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Settings;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Settings\ProfileUpdateRequest;
|
||||
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class ProfileController extends Controller
|
||||
{
|
||||
/**
|
||||
* Show the user's profile settings page.
|
||||
*/
|
||||
public function edit(Request $request): Response
|
||||
{
|
||||
return Inertia::render('settings/Profile', [
|
||||
'mustVerifyEmail' => $request->user() instanceof MustVerifyEmail,
|
||||
'status' => $request->session()->get('status'),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the user's profile information.
|
||||
*/
|
||||
public function update(ProfileUpdateRequest $request): RedirectResponse
|
||||
{
|
||||
$request->user()->fill($request->validated());
|
||||
|
||||
if ($request->user()->isDirty('email')) {
|
||||
$request->user()->email_verified_at = null;
|
||||
}
|
||||
|
||||
$request->user()->save();
|
||||
|
||||
return to_route('profile.edit');
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the user's profile.
|
||||
*/
|
||||
public function destroy(Request $request): RedirectResponse
|
||||
{
|
||||
$request->validate([
|
||||
'password' => ['required', 'current_password'],
|
||||
]);
|
||||
|
||||
$user = $request->user();
|
||||
|
||||
Auth::logout();
|
||||
|
||||
$user->delete();
|
||||
|
||||
$request->session()->invalidate();
|
||||
$request->session()->regenerateToken();
|
||||
|
||||
return redirect('/');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Settings;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Settings\TwoFactorAuthenticationRequest;
|
||||
use Illuminate\Routing\Controllers\HasMiddleware;
|
||||
use Illuminate\Routing\Controllers\Middleware;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
use Laravel\Fortify\Features;
|
||||
|
||||
class TwoFactorAuthenticationController extends Controller implements HasMiddleware
|
||||
{
|
||||
/**
|
||||
* Get the middleware that should be assigned to the controller.
|
||||
*/
|
||||
public static function middleware(): array
|
||||
{
|
||||
return Features::optionEnabled(Features::twoFactorAuthentication(), 'confirmPassword')
|
||||
? [new Middleware('password.confirm', only: ['show'])]
|
||||
: [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the user's two-factor authentication settings page.
|
||||
*/
|
||||
public function show(TwoFactorAuthenticationRequest $request): Response
|
||||
{
|
||||
$request->ensureStateIsValid();
|
||||
|
||||
return Inertia::render('settings/TwoFactor', [
|
||||
'twoFactorEnabled' => $request->user()->hasEnabledTwoFactorAuthentication(),
|
||||
'requiresConfirmation' => Features::optionEnabled(Features::twoFactorAuthentication(), 'confirm'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\View;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class HandleAppearance
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
View::share('appearance', $request->cookie('appearance') ?? 'system');
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Illuminate\Foundation\Inspiring;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Middleware;
|
||||
|
||||
class HandleInertiaRequests extends Middleware
|
||||
{
|
||||
/**
|
||||
* The root template that's loaded on the first page visit.
|
||||
*
|
||||
* @see https://inertiajs.com/server-side-setup#root-template
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $rootView = 'app';
|
||||
|
||||
/**
|
||||
* Determines the current asset version.
|
||||
*
|
||||
* @see https://inertiajs.com/asset-versioning
|
||||
*/
|
||||
public function version(Request $request): ?string
|
||||
{
|
||||
return parent::version($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the props that are shared by default.
|
||||
*
|
||||
* @see https://inertiajs.com/shared-data
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function share(Request $request): array
|
||||
{
|
||||
[$message, $author] = str(Inspiring::quotes()->random())->explode('-');
|
||||
|
||||
return [
|
||||
...parent::share($request),
|
||||
'name' => config('app.name'),
|
||||
'quote' => ['message' => trim($message), 'author' => trim($author)],
|
||||
'auth' => [
|
||||
'user' => $request->user(),
|
||||
],
|
||||
'sidebarOpen' => ! $request->hasCookie('sidebar_state') || $request->cookie('sidebar_state') === 'true',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Auth;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Events\Lockout;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class LoginRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'email' => ['required', 'string', 'email'],
|
||||
'password' => ['required', 'string'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the request's credentials and return the user without logging them in.
|
||||
*
|
||||
* @throws \Illuminate\Validation\ValidationException
|
||||
*/
|
||||
public function validateCredentials(): User
|
||||
{
|
||||
$this->ensureIsNotRateLimited();
|
||||
|
||||
/** @var User|null $user */
|
||||
$user = Auth::getProvider()->retrieveByCredentials($this->only('email', 'password'));
|
||||
|
||||
if (! $user || ! Auth::getProvider()->validateCredentials($user, $this->only('password'))) {
|
||||
RateLimiter::hit($this->throttleKey());
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
'email' => trans('auth.failed'),
|
||||
]);
|
||||
}
|
||||
|
||||
RateLimiter::clear($this->throttleKey());
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the login request is not rate limited.
|
||||
*
|
||||
* @throws \Illuminate\Validation\ValidationException
|
||||
*/
|
||||
public function ensureIsNotRateLimited(): void
|
||||
{
|
||||
if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) {
|
||||
return;
|
||||
}
|
||||
|
||||
event(new Lockout($this));
|
||||
|
||||
$seconds = RateLimiter::availableIn($this->throttleKey());
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
'email' => trans('auth.throttle', [
|
||||
'seconds' => $seconds,
|
||||
'minutes' => ceil($seconds / 60),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the rate-limiting throttle key for the request.
|
||||
*/
|
||||
public function throttleKey(): string
|
||||
{
|
||||
return $this->string('email')
|
||||
->lower()
|
||||
->append('|'.$this->ip())
|
||||
->transliterate()
|
||||
->value();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Settings;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class ProfileUpdateRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'email' => [
|
||||
'required',
|
||||
'string',
|
||||
'lowercase',
|
||||
'email',
|
||||
'max:255',
|
||||
Rule::unique(User::class)->ignore($this->user()->id),
|
||||
],
|
||||
'avatar' => ['nullable', 'string', 'max:255', 'url'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Settings;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Laravel\Fortify\Features;
|
||||
use Laravel\Fortify\InteractsWithTwoFactorState;
|
||||
|
||||
class TwoFactorAuthenticationRequest extends FormRequest
|
||||
{
|
||||
use InteractsWithTwoFactorState;
|
||||
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return Features::enabled(Features::twoFactorAuthentication());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class StoreCustomerRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true; // Autorisierung später über Policies/Gates
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'type' => 'required|in:private,business',
|
||||
'company_name' => [
|
||||
'required_if:type,business',
|
||||
'nullable',
|
||||
'string',
|
||||
'max:100',
|
||||
],
|
||||
'first_name' => 'required|string|max:50',
|
||||
'last_name' => 'required|string|max:50',
|
||||
'email' => 'required|email|unique:customers,email|max:100',
|
||||
'phone' => 'nullable|string|max:20',
|
||||
'tax_number' => 'nullable|string|max:50',
|
||||
'vat_id' => 'nullable|string|max:50',
|
||||
'billing_address' => 'required|array',
|
||||
'billing_address.street' => 'required|string|max:100',
|
||||
'billing_address.city' => 'required|string|max:50',
|
||||
'billing_address.postal_code' => 'required|string|max:10',
|
||||
'billing_address.country' => 'required|string|size:2',
|
||||
'payment_terms' => 'nullable|integer|min:1',
|
||||
'status' => 'nullable|in:active,inactive,prospect',
|
||||
'notes' => 'nullable|string',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class UpdateCustomerRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'type' => 'sometimes|in:private,business',
|
||||
'company_name' => [
|
||||
'sometimes',
|
||||
'nullable',
|
||||
'string',
|
||||
'max:100',
|
||||
],
|
||||
'first_name' => 'sometimes|string|max:50',
|
||||
'last_name' => 'sometimes|string|max:50',
|
||||
'email' => [
|
||||
'sometimes',
|
||||
'email',
|
||||
'max:100',
|
||||
Rule::unique('customers')->ignore($this->customer),
|
||||
],
|
||||
'phone' => 'nullable|string|max:20',
|
||||
'tax_number' => 'nullable|string|max:50',
|
||||
'vat_id' => 'nullable|string|max:50',
|
||||
'billing_address' => 'sometimes|array',
|
||||
'billing_address.street' => 'required_with:billing_address|string|max:100',
|
||||
'billing_address.city' => 'required_with:billing_address|string|max:50',
|
||||
'billing_address.postal_code' => 'required_with:billing_address|string|max:10',
|
||||
'billing_address.country' => 'required_with:billing_address|string|size:2',
|
||||
'payment_terms' => 'nullable|integer|min:1',
|
||||
'status' => 'nullable|in:active,inactive,prospect',
|
||||
'notes' => 'nullable|string',
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user