diff --git a/backend/.editorconfig b/backend/.editorconfig new file mode 100644 index 0000000..8f0de65 --- /dev/null +++ b/backend/.editorconfig @@ -0,0 +1,18 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false + +[*.{yml,yaml}] +indent_size = 2 + +[docker-compose.yml] +indent_size = 4 diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..ea0665b --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,59 @@ +APP_NAME=Laravel +APP_ENV=local +APP_KEY= +APP_DEBUG=true +APP_URL=http://localhost + +LOG_CHANNEL=stack +LOG_DEPRECATIONS_CHANNEL=null +LOG_LEVEL=debug + +DB_CONNECTION=mysql +DB_HOST=127.0.0.1 +DB_PORT=3306 +DB_DATABASE=laravel +DB_USERNAME=root +DB_PASSWORD= + +BROADCAST_DRIVER=log +CACHE_DRIVER=file +FILESYSTEM_DISK=local +QUEUE_CONNECTION=sync +SESSION_DRIVER=file +SESSION_LIFETIME=120 + +MEMCACHED_HOST=127.0.0.1 + +REDIS_HOST=127.0.0.1 +REDIS_PASSWORD=null +REDIS_PORT=6379 + +MAIL_MAILER=smtp +MAIL_HOST=mailpit +MAIL_PORT=1025 +MAIL_USERNAME=null +MAIL_PASSWORD=null +MAIL_ENCRYPTION=null +MAIL_FROM_ADDRESS="hello@example.com" +MAIL_FROM_NAME="${APP_NAME}" + +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= +AWS_DEFAULT_REGION=us-east-1 +AWS_BUCKET= +AWS_USE_PATH_STYLE_ENDPOINT=false + +PUSHER_APP_ID= +PUSHER_APP_KEY= +PUSHER_APP_SECRET= +PUSHER_HOST= +PUSHER_PORT=443 +PUSHER_SCHEME=https +PUSHER_APP_CLUSTER=mt1 + +VITE_APP_NAME="${APP_NAME}" +VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}" +VITE_PUSHER_HOST="${PUSHER_HOST}" +VITE_PUSHER_PORT="${PUSHER_PORT}" +VITE_PUSHER_SCHEME="${PUSHER_SCHEME}" +VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}" diff --git a/backend/.gitattributes b/backend/.gitattributes new file mode 100644 index 0000000..fcb21d3 --- /dev/null +++ b/backend/.gitattributes @@ -0,0 +1,11 @@ +* text=auto eol=lf + +*.blade.php diff=html +*.css diff=css +*.html diff=html +*.md diff=markdown +*.php diff=php + +/.github export-ignore +CHANGELOG.md export-ignore +.styleci.yml export-ignore diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000..7fe978f --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,19 @@ +/.phpunit.cache +/node_modules +/public/build +/public/hot +/public/storage +/storage/*.key +/vendor +.env +.env.backup +.env.production +.phpunit.result.cache +Homestead.json +Homestead.yaml +auth.json +npm-debug.log +yarn-error.log +/.fleet +/.idea +/.vscode diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 0000000..1a4c26b --- /dev/null +++ b/backend/README.md @@ -0,0 +1,66 @@ +
+ + + +## About Laravel + +Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as: + +- [Simple, fast routing engine](https://laravel.com/docs/routing). +- [Powerful dependency injection container](https://laravel.com/docs/container). +- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage. +- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent). +- Database agnostic [schema migrations](https://laravel.com/docs/migrations). +- [Robust background job processing](https://laravel.com/docs/queues). +- [Real-time event broadcasting](https://laravel.com/docs/broadcasting). + +Laravel is accessible, powerful, and provides tools required for large, robust applications. + +## Learning Laravel + +Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework. + +You may also try the [Laravel Bootcamp](https://bootcamp.laravel.com), where you will be guided through building a modern Laravel application from scratch. + +If you don't feel like reading, [Laracasts](https://laracasts.com) can help. Laracasts contains thousands of video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library. + +## Laravel Sponsors + +We would like to extend our thanks to the following sponsors for funding Laravel development. If you are interested in becoming a sponsor, please visit the [Laravel Partners program](https://partners.laravel.com). + +### Premium Partners + +- **[Vehikl](https://vehikl.com/)** +- **[Tighten Co.](https://tighten.co)** +- **[WebReinvent](https://webreinvent.com/)** +- **[Kirschbaum Development Group](https://kirschbaumdevelopment.com)** +- **[64 Robots](https://64robots.com)** +- **[Curotec](https://www.curotec.com/services/technologies/laravel/)** +- **[Cyber-Duck](https://cyber-duck.co.uk)** +- **[DevSquad](https://devsquad.com/hire-laravel-developers)** +- **[Jump24](https://jump24.co.uk)** +- **[Redberry](https://redberry.international/laravel/)** +- **[Active Logic](https://activelogic.com)** +- **[byte5](https://byte5.de)** +- **[OP.GG](https://op.gg)** + +## Contributing + +Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions). + +## Code of Conduct + +In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct). + +## Security Vulnerabilities + +If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed. + +## License + +The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT). diff --git a/backend/app/Console/Kernel.php b/backend/app/Console/Kernel.php new file mode 100644 index 0000000..e6b9960 --- /dev/null +++ b/backend/app/Console/Kernel.php @@ -0,0 +1,27 @@ +command('inspire')->hourly(); + } + + /** + * Register the commands for the application. + */ + protected function commands(): void + { + $this->load(__DIR__.'/Commands'); + + require base_path('routes/console.php'); + } +} diff --git a/backend/app/Exceptions/Handler.php b/backend/app/Exceptions/Handler.php new file mode 100644 index 0000000..56af264 --- /dev/null +++ b/backend/app/Exceptions/Handler.php @@ -0,0 +1,30 @@ + + */ + protected $dontFlash = [ + 'current_password', + 'password', + 'password_confirmation', + ]; + + /** + * Register the exception handling callbacks for the application. + */ + public function register(): void + { + $this->reportable(function (Throwable $e) { + // + }); + } +} diff --git a/backend/app/Filament/Resources/UserResource.php b/backend/app/Filament/Resources/UserResource.php new file mode 100644 index 0000000..8366b99 --- /dev/null +++ b/backend/app/Filament/Resources/UserResource.php @@ -0,0 +1,55 @@ +schema([ + TextInput::make('name')->required()->maxLength(255), + TextInput::make('email')->email()->required()->maxLength(255), + TextInput::make('password')->password()->dehydrateStateUsing(fn ($state) => bcrypt($state)) + ->required(fn (string $context) => $context === 'create'), + Select::make('role') + ->options(['admin' => 'Admin', 'staff' => 'Mitarbeiter']) + ->required(), + ]); + } + + public static function table(Table $table): Table + { + return $table->columns([ + TextColumn::make('name')->searchable(), + TextColumn::make('email')->searchable(), + TextColumn::make('role')->badge() + ->color(fn (string $state) => $state === 'admin' ? 'danger' : 'gray'), + TextColumn::make('created_at')->dateTime()->sortable(), + ])->actions([EditAction::make(), DeleteAction::make()]); + } + + public static function getPages(): array + { + return [ + 'index' => Pages\ListUsers::route('/'), + 'create' => Pages\CreateUser::route('/create'), + 'edit' => Pages\EditUser::route('/{record}/edit'), + ]; + } +} diff --git a/backend/app/Filament/Resources/UserResource/Pages/CreateUser.php b/backend/app/Filament/Resources/UserResource/Pages/CreateUser.php new file mode 100644 index 0000000..78a3894 --- /dev/null +++ b/backend/app/Filament/Resources/UserResource/Pages/CreateUser.php @@ -0,0 +1,11 @@ +expectsJson() ? null : route('login'); + } +} diff --git a/backend/app/Http/Middleware/EncryptCookies.php b/backend/app/Http/Middleware/EncryptCookies.php new file mode 100644 index 0000000..867695b --- /dev/null +++ b/backend/app/Http/Middleware/EncryptCookies.php @@ -0,0 +1,17 @@ + + */ + protected $except = [ + // + ]; +} diff --git a/backend/app/Http/Middleware/PreventRequestsDuringMaintenance.php b/backend/app/Http/Middleware/PreventRequestsDuringMaintenance.php new file mode 100644 index 0000000..74cbd9a --- /dev/null +++ b/backend/app/Http/Middleware/PreventRequestsDuringMaintenance.php @@ -0,0 +1,17 @@ + + */ + protected $except = [ + // + ]; +} diff --git a/backend/app/Http/Middleware/RedirectIfAuthenticated.php b/backend/app/Http/Middleware/RedirectIfAuthenticated.php new file mode 100644 index 0000000..afc78c4 --- /dev/null +++ b/backend/app/Http/Middleware/RedirectIfAuthenticated.php @@ -0,0 +1,30 @@ +check()) { + return redirect(RouteServiceProvider::HOME); + } + } + + return $next($request); + } +} diff --git a/backend/app/Http/Middleware/TrimStrings.php b/backend/app/Http/Middleware/TrimStrings.php new file mode 100644 index 0000000..88cadca --- /dev/null +++ b/backend/app/Http/Middleware/TrimStrings.php @@ -0,0 +1,19 @@ + + */ + protected $except = [ + 'current_password', + 'password', + 'password_confirmation', + ]; +} diff --git a/backend/app/Http/Middleware/TrustHosts.php b/backend/app/Http/Middleware/TrustHosts.php new file mode 100644 index 0000000..c9c58bd --- /dev/null +++ b/backend/app/Http/Middleware/TrustHosts.php @@ -0,0 +1,20 @@ + + */ + public function hosts(): array + { + return [ + $this->allSubdomainsOfApplicationUrl(), + ]; + } +} diff --git a/backend/app/Http/Middleware/TrustProxies.php b/backend/app/Http/Middleware/TrustProxies.php new file mode 100644 index 0000000..3391630 --- /dev/null +++ b/backend/app/Http/Middleware/TrustProxies.php @@ -0,0 +1,28 @@ +|string|null + */ + protected $proxies; + + /** + * The headers that should be used to detect proxies. + * + * @var int + */ + protected $headers = + Request::HEADER_X_FORWARDED_FOR | + Request::HEADER_X_FORWARDED_HOST | + Request::HEADER_X_FORWARDED_PORT | + Request::HEADER_X_FORWARDED_PROTO | + Request::HEADER_X_FORWARDED_AWS_ELB; +} diff --git a/backend/app/Http/Middleware/ValidateSignature.php b/backend/app/Http/Middleware/ValidateSignature.php new file mode 100644 index 0000000..093bf64 --- /dev/null +++ b/backend/app/Http/Middleware/ValidateSignature.php @@ -0,0 +1,22 @@ + + */ + protected $except = [ + // 'fbclid', + // 'utm_campaign', + // 'utm_content', + // 'utm_medium', + // 'utm_source', + // 'utm_term', + ]; +} diff --git a/backend/app/Http/Middleware/VerifyCsrfToken.php b/backend/app/Http/Middleware/VerifyCsrfToken.php new file mode 100644 index 0000000..9e86521 --- /dev/null +++ b/backend/app/Http/Middleware/VerifyCsrfToken.php @@ -0,0 +1,17 @@ + + */ + protected $except = [ + // + ]; +} diff --git a/backend/app/Models/User.php b/backend/app/Models/User.php new file mode 100644 index 0000000..df27263 --- /dev/null +++ b/backend/app/Models/User.php @@ -0,0 +1,27 @@ + 'datetime', + 'password' => 'hashed', + ]; + + public function isAdmin(): bool + { + return $this->role === 'admin'; + } +} diff --git a/backend/app/Modules/Client/Actions/CreateClientAction.php b/backend/app/Modules/Client/Actions/CreateClientAction.php new file mode 100644 index 0000000..29971bf --- /dev/null +++ b/backend/app/Modules/Client/Actions/CreateClientAction.php @@ -0,0 +1,18 @@ + $data->clientNumber, + 'name' => $data->name, + 'notes' => $data->notes, + ]); + } +} diff --git a/backend/app/Modules/Client/Actions/GetClientAction.php b/backend/app/Modules/Client/Actions/GetClientAction.php new file mode 100644 index 0000000..db267e2 --- /dev/null +++ b/backend/app/Modules/Client/Actions/GetClientAction.php @@ -0,0 +1,19 @@ +paginate(50); + } + + public function find(int $id): Client + { + return Client::findOrFail($id); + } +} diff --git a/backend/app/Modules/Client/Actions/UpdateClientAction.php b/backend/app/Modules/Client/Actions/UpdateClientAction.php new file mode 100644 index 0000000..b002e39 --- /dev/null +++ b/backend/app/Modules/Client/Actions/UpdateClientAction.php @@ -0,0 +1,19 @@ +update(array_filter([ + 'name' => $data->name, + 'notes' => $data->notes, + ], fn ($v) => $v !== null)); + + return $client->fresh(); + } +} diff --git a/backend/app/Modules/Client/Controllers/ClientController.php b/backend/app/Modules/Client/Controllers/ClientController.php new file mode 100644 index 0000000..4d8a703 --- /dev/null +++ b/backend/app/Modules/Client/Controllers/ClientController.php @@ -0,0 +1,45 @@ +authorize('viewAny', Client::class); + return ClientResource::collection($action->list()); + } + + public function store(CreateClientRequest $request, CreateClientAction $action): JsonResponse + { + $this->authorize('create', Client::class); + $client = $action->handle(ClientData::fromCreateRequest($request)); + return ClientResource::make($client)->response()->setStatusCode(201); + } + + public function show(int $id, GetClientAction $action): ClientResource + { + $client = $action->find($id); + $this->authorize('view', $client); + return ClientResource::make($client); + } + + public function update(UpdateClientRequest $request, Client $client, UpdateClientAction $action): ClientResource + { + $this->authorize('update', $client); + $updated = $action->handle($client, ClientData::fromUpdateRequest($request)); + return ClientResource::make($updated); + } +} diff --git a/backend/app/Modules/Client/DTOs/ClientData.php b/backend/app/Modules/Client/DTOs/ClientData.php new file mode 100644 index 0000000..efb88a0 --- /dev/null +++ b/backend/app/Modules/Client/DTOs/ClientData.php @@ -0,0 +1,33 @@ +validated('client_number'), + name: $request->validated('name'), + notes: $request->validated('notes'), + ); + } + + public static function fromUpdateRequest(UpdateClientRequest $request): self + { + return new self( + clientNumber: null, + name: $request->validated('name'), + notes: $request->validated('notes'), + ); + } +} diff --git a/backend/app/Modules/Client/Models/Client.php b/backend/app/Modules/Client/Models/Client.php new file mode 100644 index 0000000..08116b7 --- /dev/null +++ b/backend/app/Modules/Client/Models/Client.php @@ -0,0 +1,28 @@ + 'array']; + + protected static function newFactory(): ClientFactory + { + return ClientFactory::new(); + } + + public function historyEntries(): HasMany + { + return $this->hasMany(HistoryEntry::class); + } +} diff --git a/backend/app/Modules/Client/Policies/ClientPolicy.php b/backend/app/Modules/Client/Policies/ClientPolicy.php new file mode 100644 index 0000000..ec31235 --- /dev/null +++ b/backend/app/Modules/Client/Policies/ClientPolicy.php @@ -0,0 +1,15 @@ +isAdmin(); } +} diff --git a/backend/app/Modules/Client/Requests/CreateClientRequest.php b/backend/app/Modules/Client/Requests/CreateClientRequest.php new file mode 100644 index 0000000..6abaa1b --- /dev/null +++ b/backend/app/Modules/Client/Requests/CreateClientRequest.php @@ -0,0 +1,19 @@ + ['required', 'string', 'max:50', 'unique:clients,client_number'], + 'name' => ['required', 'string', 'max:255'], + 'notes' => ['nullable', 'array'], + ]; + } +} diff --git a/backend/app/Modules/Client/Requests/UpdateClientRequest.php b/backend/app/Modules/Client/Requests/UpdateClientRequest.php new file mode 100644 index 0000000..7d4768b --- /dev/null +++ b/backend/app/Modules/Client/Requests/UpdateClientRequest.php @@ -0,0 +1,18 @@ + ['sometimes', 'required', 'string', 'max:255'], + 'notes' => ['nullable', 'array'], + ]; + } +} diff --git a/backend/app/Modules/Client/Resources/ClientResource.php b/backend/app/Modules/Client/Resources/ClientResource.php new file mode 100644 index 0000000..e790f75 --- /dev/null +++ b/backend/app/Modules/Client/Resources/ClientResource.php @@ -0,0 +1,21 @@ + $this->id, + 'client_number' => $this->client_number, + 'name' => $this->name, + 'notes' => $this->notes ?? [], + 'created_at' => $this->created_at->toISOString(), + 'updated_at' => $this->updated_at->toISOString(), + ]; + } +} diff --git a/backend/app/Modules/History/Actions/CreateHistoryEntryAction.php b/backend/app/Modules/History/Actions/CreateHistoryEntryAction.php new file mode 100644 index 0000000..2e73df4 --- /dev/null +++ b/backend/app/Modules/History/Actions/CreateHistoryEntryAction.php @@ -0,0 +1,21 @@ + $client->id, + 'type' => $data->type, + 'body' => $data->body, + 'author_id' => $author->id, + ]); + } +} diff --git a/backend/app/Modules/History/Actions/UpdateHistoryEntryAction.php b/backend/app/Modules/History/Actions/UpdateHistoryEntryAction.php new file mode 100644 index 0000000..4da7ded --- /dev/null +++ b/backend/app/Modules/History/Actions/UpdateHistoryEntryAction.php @@ -0,0 +1,20 @@ +update([ + 'body' => $data->body, + 'updated_by' => $editor->id, + ]); + + return $entry->fresh(); + } +} diff --git a/backend/app/Modules/History/Controllers/HistoryController.php b/backend/app/Modules/History/Controllers/HistoryController.php new file mode 100644 index 0000000..dbb7559 --- /dev/null +++ b/backend/app/Modules/History/Controllers/HistoryController.php @@ -0,0 +1,55 @@ +authorize('viewAny', HistoryEntry::class); + $entries = $client->historyEntries() + ->with(['author', 'updatedBy']) + ->orderByDesc('created_at') + ->get(); + return HistoryEntryResource::collection($entries); + } + + public function store( + CreateHistoryEntryRequest $request, + Client $client, + CreateHistoryEntryAction $action + ): JsonResponse { + $this->authorize('create', HistoryEntry::class); + $entry = $action->handle($client, HistoryEntryData::fromCreateRequest($request), $request->user()); + return HistoryEntryResource::make($entry->load(['author']))->response()->setStatusCode(201); + } + + public function update( + UpdateHistoryEntryRequest $request, + HistoryEntry $historyEntry, + UpdateHistoryEntryAction $action + ): HistoryEntryResource { + $this->authorize('update', $historyEntry); + $updated = $action->handle($historyEntry, HistoryEntryData::fromUpdateRequest($request), $request->user()); + return HistoryEntryResource::make($updated->load(['author', 'updatedBy'])); + } + + public function destroy(HistoryEntry $historyEntry): JsonResponse + { + $this->authorize('delete', $historyEntry); + $historyEntry->delete(); + return response()->json(null, 204); + } +} diff --git a/backend/app/Modules/History/DTOs/HistoryEntryData.php b/backend/app/Modules/History/DTOs/HistoryEntryData.php new file mode 100644 index 0000000..be2a249 --- /dev/null +++ b/backend/app/Modules/History/DTOs/HistoryEntryData.php @@ -0,0 +1,30 @@ +validated('type'), + body: $request->validated('body'), + ); + } + + public static function fromUpdateRequest(UpdateHistoryEntryRequest $request): self + { + return new self( + type: null, + body: $request->validated('body'), + ); + } +} diff --git a/backend/app/Modules/History/Models/HistoryEntry.php b/backend/app/Modules/History/Models/HistoryEntry.php new file mode 100644 index 0000000..ac8808f --- /dev/null +++ b/backend/app/Modules/History/Models/HistoryEntry.php @@ -0,0 +1,40 @@ + HistoryEntryType::class]; + + public function client(): BelongsTo + { + return $this->belongsTo(Client::class); + } + + public function author(): BelongsTo + { + return $this->belongsTo(User::class, 'author_id'); + } + + public function updatedBy(): BelongsTo + { + return $this->belongsTo(User::class, 'updated_by'); + } + + protected static function newFactory(): HistoryEntryFactory + { + return HistoryEntryFactory::new(); + } +} diff --git a/backend/app/Modules/History/Models/HistoryEntryType.php b/backend/app/Modules/History/Models/HistoryEntryType.php new file mode 100644 index 0000000..4d34534 --- /dev/null +++ b/backend/app/Modules/History/Models/HistoryEntryType.php @@ -0,0 +1,13 @@ + ['required', new Enum(HistoryEntryType::class)], + 'body' => ['required', 'string', 'max:65535'], + ]; + } +} diff --git a/backend/app/Modules/History/Requests/UpdateHistoryEntryRequest.php b/backend/app/Modules/History/Requests/UpdateHistoryEntryRequest.php new file mode 100644 index 0000000..aa1a3d4 --- /dev/null +++ b/backend/app/Modules/History/Requests/UpdateHistoryEntryRequest.php @@ -0,0 +1,17 @@ + ['required', 'string', 'max:65535'], + ]; + } +} diff --git a/backend/app/Modules/History/Resources/HistoryEntryResource.php b/backend/app/Modules/History/Resources/HistoryEntryResource.php new file mode 100644 index 0000000..7476ec2 --- /dev/null +++ b/backend/app/Modules/History/Resources/HistoryEntryResource.php @@ -0,0 +1,25 @@ + $this->id, + 'client_id' => $this->client_id, + 'type' => $this->type->value, + 'body' => $this->body, + 'author_id' => $this->author_id, + 'author_name' => $this->author?->name, + 'updated_by' => $this->updated_by, + 'updated_by_name' => $this->updatedBy?->name, + 'created_at' => $this->created_at->toISOString(), + 'updated_at' => $this->updated_at->toISOString(), + ]; + } +} diff --git a/backend/app/Modules/Search/Actions/SearchClientsAction.php b/backend/app/Modules/Search/Actions/SearchClientsAction.php new file mode 100644 index 0000000..130e24a --- /dev/null +++ b/backend/app/Modules/Search/Actions/SearchClientsAction.php @@ -0,0 +1,25 @@ +getDriverName() === 'sqlite') { + return Client::where('name', 'like', "%{$query}%") + ->orWhere('client_number', 'like', "%{$query}%") + ->limit(50) + ->get(); + } + + return Client::whereRaw( + 'MATCH(client_number, name) AGAINST(? IN BOOLEAN MODE)', + [$query . '*'] + )->limit(50)->get(); + } +} diff --git a/backend/app/Modules/Search/Controllers/SearchController.php b/backend/app/Modules/Search/Controllers/SearchController.php new file mode 100644 index 0000000..06dc28a --- /dev/null +++ b/backend/app/Modules/Search/Controllers/SearchController.php @@ -0,0 +1,21 @@ +validate(['q' => ['required', 'string', 'min:1', 'max:100']]); + + $clients = $action->handle($validated['q']); + + return ClientResource::collection($clients); + } +} diff --git a/backend/app/Modules/User/Policies/UserPolicy.php b/backend/app/Modules/User/Policies/UserPolicy.php new file mode 100644 index 0000000..a05928e --- /dev/null +++ b/backend/app/Modules/User/Policies/UserPolicy.php @@ -0,0 +1,28 @@ +isAdmin(); + } + + public function create(User $user): bool + { + return $user->isAdmin(); + } + + public function update(User $user, User $target): bool + { + return $user->isAdmin(); + } + + public function delete(User $user, User $target): bool + { + return $user->isAdmin() && $user->id !== $target->id; + } +} diff --git a/backend/app/Providers/AppServiceProvider.php b/backend/app/Providers/AppServiceProvider.php new file mode 100644 index 0000000..452e6b6 --- /dev/null +++ b/backend/app/Providers/AppServiceProvider.php @@ -0,0 +1,24 @@ + + */ + protected $policies = [ + User::class => UserPolicy::class, + Client::class => ClientPolicy::class, + HistoryEntry::class => HistoryEntryPolicy::class, + ]; + + /** + * Register any authentication / authorization services. + */ + public function boot(): void + { + // + } +} diff --git a/backend/app/Providers/BroadcastServiceProvider.php b/backend/app/Providers/BroadcastServiceProvider.php new file mode 100644 index 0000000..2be04f5 --- /dev/null +++ b/backend/app/Providers/BroadcastServiceProvider.php @@ -0,0 +1,19 @@ +> + */ + protected $listen = [ + Registered::class => [ + SendEmailVerificationNotification::class, + ], + ]; + + /** + * Register any events for your application. + */ + public function boot(): void + { + // + } + + /** + * Determine if events and listeners should be automatically discovered. + */ + public function shouldDiscoverEvents(): bool + { + return false; + } +} diff --git a/backend/app/Providers/RouteServiceProvider.php b/backend/app/Providers/RouteServiceProvider.php new file mode 100644 index 0000000..1cf5f15 --- /dev/null +++ b/backend/app/Providers/RouteServiceProvider.php @@ -0,0 +1,40 @@ +by($request->user()?->id ?: $request->ip()); + }); + + $this->routes(function () { + Route::middleware('api') + ->prefix('api') + ->group(base_path('routes/api.php')); + + Route::middleware('web') + ->group(base_path('routes/web.php')); + }); + } +} diff --git a/backend/artisan b/backend/artisan new file mode 100755 index 0000000..67a3329 --- /dev/null +++ b/backend/artisan @@ -0,0 +1,53 @@ +#!/usr/bin/env php +make(Illuminate\Contracts\Console\Kernel::class); + +$status = $kernel->handle( + $input = new Symfony\Component\Console\Input\ArgvInput, + new Symfony\Component\Console\Output\ConsoleOutput +); + +/* +|-------------------------------------------------------------------------- +| Shutdown The Application +|-------------------------------------------------------------------------- +| +| Once Artisan has finished running, we will fire off the shutdown events +| so that any final work may be done by the application before we shut +| down the process. This is the last thing to happen to the request. +| +*/ + +$kernel->terminate($input, $status); + +exit($status); diff --git a/backend/bootstrap/app.php b/backend/bootstrap/app.php new file mode 100644 index 0000000..037e17d --- /dev/null +++ b/backend/bootstrap/app.php @@ -0,0 +1,55 @@ +singleton( + Illuminate\Contracts\Http\Kernel::class, + App\Http\Kernel::class +); + +$app->singleton( + Illuminate\Contracts\Console\Kernel::class, + App\Console\Kernel::class +); + +$app->singleton( + Illuminate\Contracts\Debug\ExceptionHandler::class, + App\Exceptions\Handler::class +); + +/* +|-------------------------------------------------------------------------- +| Return The Application +|-------------------------------------------------------------------------- +| +| This script returns the application instance. The instance is given to +| the calling script so we can separate the building of the instances +| from the actual running of the application and sending responses. +| +*/ + +return $app; diff --git a/backend/database/migrations/2026_06_23_095329_create_clients_table.php b/backend/database/migrations/2026_06_23_095329_create_clients_table.php index 6475674..9f5fc47 100644 --- a/backend/database/migrations/2026_06_23_095329_create_clients_table.php +++ b/backend/database/migrations/2026_06_23_095329_create_clients_table.php @@ -2,13 +2,11 @@ use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Schema; return new class extends Migration { - /** - * Run the migrations. - */ public function up(): void { Schema::create('clients', function (Blueprint $table) { @@ -18,7 +16,9 @@ public function up(): void $table->json('notes')->nullable(); $table->timestamps(); - $table->fullText(['client_number', 'name']); + if (DB::connection()->getDriverName() !== 'sqlite') { + $table->fullText(['client_number', 'name']); + } }); } diff --git a/backend/package.json b/backend/package.json new file mode 100644 index 0000000..56f5ddc --- /dev/null +++ b/backend/package.json @@ -0,0 +1,13 @@ +{ + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build" + }, + "devDependencies": { + "axios": "^1.6.4", + "laravel-vite-plugin": "^1.0.0", + "vite": "^5.0.0" + } +} diff --git a/backend/phpunit.xml b/backend/phpunit.xml index c90ec4f..7970dae 100644 --- a/backend/phpunit.xml +++ b/backend/phpunit.xml @@ -23,7 +23,7 @@+ Laravel has wonderful documentation covering every aspect of the framework. Whether you are a newcomer or have prior experience with Laravel, we recommend reading our documentation from beginning to end. +
++ Laracasts offers thousands of video tutorials on Laravel, PHP, and JavaScript development. Check them out, see for yourself, and massively level up your development skills in the process. +
++ Laravel News is a community driven portal and newsletter aggregating all of the latest and most important news in the Laravel ecosystem, including new package releases and tutorials. +
+