diff --git a/app/Concerns/UI/HasAttributeHelpers.php b/app/Concerns/UI/HasAttributeHelpers.php
new file mode 100644
index 0000000..4959ce1
--- /dev/null
+++ b/app/Concerns/UI/HasAttributeHelpers.php
@@ -0,0 +1,75 @@
+attributes->has($attribute);
+ }
+
+ /**
+ * Get the error field name.
+ * If the parent class has an error field name (passed via props or defined), it will be returned.
+ * Otherwise, the model name will be returned.
+ */
+ public function errorFieldName(): ?string
+ {
+ return $this->errorField ?? $this->modelName();
+ }
+
+ /**
+ * This function will return the model name from the wire:model attribute.
+ * This is useful as we use this to bind wire:model props passed from high level
+ * components (ex: input) to the internal html components (ex: input tag).
+ */
+ public function modelName(): ?string
+ {
+ return $this->attributes->whereStartsWith('wire:model')->first();
+ }
+
+ /**
+ * Check if the input is required.
+ */
+ public function isRequired(): bool
+ {
+ return $this->attributes->has('required') && true === $this->attributes->get('required');
+ }
+
+ /**
+ * Check if the input is disabled.
+ */
+ public function isDisabled(): bool
+ {
+ return $this->attributes->has('disabled') && true === $this->attributes->get('disabled');
+ }
+
+ /**
+ * Check if the input is readonly.
+ */
+ public function isReadonly(): bool
+ {
+ return $this->attributes->has('readonly') && true === $this->attributes->get('readonly');
+ }
+
+ /**
+ * Check if the input is autofocus.
+ */
+ public function isAutofocus(): bool
+ {
+ return $this->attributes->has('autofocus') && true === $this->attributes->get('autofocus');
+ }
+}
diff --git a/app/Data/ConnectService/ConnectServiceData.php b/app/Data/ConnectService/ConnectServiceData.php
new file mode 100644
index 0000000..fcc04d3
--- /dev/null
+++ b/app/Data/ConnectService/ConnectServiceData.php
@@ -0,0 +1,19 @@
+ $data->name,
+ 'service_provider_id' => $data->serviceProviderId,
+ 'service_protocol_id' => $data->serviceProtocolId,
+ 'logo_url' => $data->logoUrl,
+ 'slug' => $data->slug,
+ 'service_status_id' => $data->statusId,
+ ];
+
+ // Check if the staus is filled, if not select disconnected by default
+ if (null === $data->statusId) {
+ $status = ServiceStatus::query()
+ ->where('name', '=', ConnectionStatus::Disconnected->value)
+ ->pluck('id');
+ $result['service_status_id'] = $status[0];
+ }
+
+ return $result;
+ }
+}
diff --git a/app/Livewire/Forms/ConnectServiceFrom.php b/app/Livewire/Forms/ConnectServiceFrom.php
new file mode 100644
index 0000000..0aed1a2
--- /dev/null
+++ b/app/Livewire/Forms/ConnectServiceFrom.php
@@ -0,0 +1,41 @@
+validate();
+
+ $serviceData = new ConnectServiceData(
+ name: $this->name,
+ serviceProviderId: $this->providerId,
+ serviceProtocolId: $this->protocolId,
+ logoUrl: $this->logoUrl,
+ slug: str($this->name)->slug()->toString(),
+ );
+
+ app(ConnectedServicesService::class)->create($serviceData);
+ $this->reset();
+ }
+}
diff --git a/app/Models/ConnectedService.php b/app/Models/ConnectedService.php
index 3095f94..6aa4ae7 100644
--- a/app/Models/ConnectedService.php
+++ b/app/Models/ConnectedService.php
@@ -4,6 +4,31 @@
namespace App\Models;
+use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Model;
+use Illuminate\Database\Eloquent\Relations\HasOne;
-final class ConnectedService extends Model {}
+#[Fillable([
+ 'name',
+ 'slug',
+ 'service_protocol_id',
+ 'service_provider_id',
+ 'service_status_id',
+])]
+class ConnectedService extends Model
+{
+ public function protocol(): HasOne
+ {
+ return $this->hasOne(ServiceProtocol::class, 'id', 'service_protocol_id');
+ }
+
+ public function provider(): HasOne
+ {
+ return $this->hasOne(ServiceProvider::class, 'id', 'service_provider_id');
+ }
+
+ public function status(): HasOne
+ {
+ return $this->hasOne(ServiceStatus::class, 'id', 'service_status_id');
+ }
+}
diff --git a/app/Models/ServiceStatus.php b/app/Models/ServiceStatus.php
index a7e7c70..259d564 100644
--- a/app/Models/ServiceStatus.php
+++ b/app/Models/ServiceStatus.php
@@ -4,6 +4,8 @@
namespace App\Models;
+use Illuminate\Database\Eloquent\Attributes\Table;
use Illuminate\Database\Eloquent\Model;
-final class ServiceStatus extends Model {}
+#[Table('service_statuses')]
+class ServiceStatus extends Model {}
diff --git a/app/Services/ConnectedServicesService.php b/app/Services/ConnectedServicesService.php
new file mode 100644
index 0000000..cfb709b
--- /dev/null
+++ b/app/Services/ConnectedServicesService.php
@@ -0,0 +1,23 @@
+createDataFromDto($data);
+ DB::transaction(
+ fn () => ConnectedService::create(
+ $data
+ )
+ );
+ }
+}
diff --git a/app/View/Components/Choices.php b/app/View/Components/Choices.php
new file mode 100644
index 0000000..fae0267
--- /dev/null
+++ b/app/View/Components/Choices.php
@@ -0,0 +1,430 @@
+uuid = 'component'.md5(serialize($this)).$id;
+
+ if (($this->allowAll || $this->compact) && ($this->single || $this->searchable)) {
+ throw new Exception('`allow-all` and `compact` does not work combined with `single` or `searchable`.');
+ }
+ }
+
+ public function getOptionValue($option): mixed
+ {
+ $value = data_get($option, $this->optionValue);
+
+ if ($this->valuesAsString) {
+ return "'$value'";
+ }
+
+ if ($this->escapeValues) {
+ $value = addslashes($value);
+
+ return "'$value'";
+ }
+
+ return is_numeric($value) && ! str($value)->startsWith('0') ? $value : "'$value'";
+ }
+
+ public function render(): View|Closure|string
+ {
+ return <<<'HTML'
+
+
+
+
+ {{-- OPTIONS LIST --}}
+
+
+
+ {{-- PROGRESS --}}
+ @if(!$noProgress)
+
+ @endif
+
+ {{-- SELECT ALL --}}
+ @if($allowAll)
+
+
{{ $allowAllText }}
+
{{ $removeAllText }}
+
+ @endif
+
+ {{-- NO RESULTS --}}
+
+ {{ $noResultText }}
+
+
+ @forelse($options as $option)
+
+ {{-- ITEM SLOT --}}
+ @if($item)
+ {{ $item($option) }}
+ @else
+
+ @endif
+
+ {{-- SELECTION SLOT --}}
+ @if($selection)
+
+ {{ $selection($option) }}
+
+ @endif
+
+ @empty
+
No results found.
+ @endforelse
+
+
+
+
+ HTML;
+ }
+}
diff --git a/app/View/Components/Input.php b/app/View/Components/Input.php
new file mode 100644
index 0000000..c8b38c2
--- /dev/null
+++ b/app/View/Components/Input.php
@@ -0,0 +1,62 @@
+uuid = 'component'.md5(serialize($this)).$id;
+ }
+
+ /**
+ * Get the view / contents that represent the component.
+ */
+ public function render(): View|Closure|string
+ {
+ return <<<'BLADE'
+
+
+
+ BLADE;
+ }
+}
diff --git a/app/View/Components/ListItem.php b/app/View/Components/ListItem.php
new file mode 100644
index 0000000..0cd204e
--- /dev/null
+++ b/app/View/Components/ListItem.php
@@ -0,0 +1,83 @@
+uuid = 'component'.md5(serialize($this)).$id;
+ }
+
+ public function render(): View|Closure|string
+ {
+ return <<<'HTML'
+
+ HTML;
+ }
+}
diff --git a/composer.json b/composer.json
index 97ddf3c..0a4cf8e 100644
--- a/composer.json
+++ b/composer.json
@@ -58,10 +58,10 @@
"npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan pail --timeout=0\" \"npm run dev\" --names=server,queue,logs,vite --kill-others"
],
"lint": [
- "pint --parallel --dirty"
+ "pint --parallel"
],
"lint:check": [
- "pint --parallel --dirty --test"
+ "pint --parallel --test"
],
"ci:check": [
"Composer\\Config::disableProcessTimeout",
diff --git a/database/seeders/ServiceProviderSeeder.php b/database/seeders/ServiceProviderSeeder.php
new file mode 100644
index 0000000..a972365
--- /dev/null
+++ b/database/seeders/ServiceProviderSeeder.php
@@ -0,0 +1,32 @@
+ 'Azure FD',
+ 'slug' => 'azure-fd',
+ ], [
+ 'name' => 'Keka HR',
+ 'slug' => 'keka-hr',
+ ],
+ [
+ 'name' => 'Oneclick',
+ 'slug' => 'oneclick',
+ ],
+ ];
+ ServiceProvider::upsert($data, ['slug'], ['name']);
+ }
+}
diff --git a/pint.json b/pint.json
index 3af04fc..84302a9 100644
--- a/pint.json
+++ b/pint.json
@@ -2,7 +2,6 @@
"preset": "laravel",
"rules": {
"declare_strict_types": true,
- "final_class": true,
"fully_qualified_strict_types": true,
"group_import": true,
"single_import_per_statement": false,
@@ -37,7 +36,6 @@
"no_superfluous_elseif": true,
"no_useless_else": true,
"nullable_type_declaration_for_default_null_value": true,
-
"strict_param": true,
"date_time_immutable": true,
"mb_str_functions": true,
diff --git a/resources/views/components/dashboard/connected-services/⚡connected-services.blade.php b/resources/views/components/dashboard/connected-services/⚡connected-services.blade.php
index f0bbe13..b7dc452 100644
--- a/resources/views/components/dashboard/connected-services/⚡connected-services.blade.php
+++ b/resources/views/components/dashboard/connected-services/⚡connected-services.blade.php
@@ -10,7 +10,7 @@
-
+
{{ __('Add service') }}
diff --git a/resources/views/components/shared/error.blade.php b/resources/views/components/shared/error.blade.php
new file mode 100644
index 0000000..fe66314
--- /dev/null
+++ b/resources/views/components/shared/error.blade.php
@@ -0,0 +1,12 @@
+@props(['errorFieldName', 'firstErrorOnly' => false])
+
+ @if($errors->has($errorFieldName))
+ @foreach($errors->get($errorFieldName) as $message)
+ @foreach(Arr::wrap($message) as $line)
+
{{ $line }}
+ @break($firstErrorOnly)
+ @endforeach
+ @break($firstErrorOnly)
+ @endforeach
+ @endif
+
diff --git a/resources/views/pages/connected-services/⚡create.blade.php b/resources/views/pages/connected-services/⚡create.blade.php
new file mode 100644
index 0000000..7b811bb
--- /dev/null
+++ b/resources/views/pages/connected-services/⚡create.blade.php
@@ -0,0 +1,68 @@
+get();
+ }
+
+ #[Computed]
+ public function protocols()
+ {
+ return ServiceProtocol::query()->get()->map(function (ServiceProtocol $protocol) {
+ $protocol->name = ucfirst($protocol->name);
+ return $protocol;
+ });
+ }
+
+ public function save()
+ {
+ $this->form->save();
+ }
+};
+?>
+
+
diff --git a/routes/web.php b/routes/web.php
index f5bb7ea..4657935 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -8,6 +8,10 @@
Route::group(['middleware' => ['auth', 'verified']], function (): void {
Route::livewire('dashboard', 'pages::dashboard')->name('dashboard');
+
+ Route::prefix('connected-services')->name('connected-services.')->group(function (): void {
+ Route::livewire('create', 'pages::connected-services.create')->name('create');
+ });
});
require __DIR__.'/settings.php';