diff --git a/app/Models/Contact.php b/app/Models/Contact.php index 04e2a99..576efa8 100644 --- a/app/Models/Contact.php +++ b/app/Models/Contact.php @@ -11,14 +11,18 @@ class Contact extends Model protected $fillable = [ 'customer_id', + 'is_primary', 'salutation', + 'academic_title', 'first_name', 'last_name', + 'job_title', 'email', 'phone', - 'position', - 'is_primary', + 'mobile_phone', 'avatar', + 'online_accounts', + 'notes', ]; public function customer() @@ -38,7 +42,7 @@ public function getAvatarUrlAttribute() // Return null if no avatar is set return null; } - + /** * Scope a query to order contacts with primary contacts first. */ @@ -46,4 +50,33 @@ public function scopePrimaryFirst($query) { return $query->orderBy('is_primary', 'desc'); } + + /** + * Set the online accounts attribute. + * + * @param array $value + * @return void + */ + public function setOnlineAccountsAttribute($value) + { + if (is_string($value)) { + $value = json_decode($value, true); + } + + $this->attributes['online_accounts'] = json_encode([ + 'platform' => $value['platform'] ?? '', + 'user_name' => $value['user_name'] ?? '', + 'url' => $value['url'] ?? '' + ]); + } + + /** + * Get the online accounts attribute. + * + * @return array + */ + public function getOnlineAccountsAttribute() + { + return json_decode($this->attributes['online_accounts'], true) ?? null; + } } diff --git a/database/factories/ContactFactory.php b/database/factories/ContactFactory.php index 631c424..4f3455f 100644 --- a/database/factories/ContactFactory.php +++ b/database/factories/ContactFactory.php @@ -21,16 +21,40 @@ public function definition() } $gender = rand(0, 9) >= 5 ? 'female' : 'male'; - - return [ + $userName = $this->faker->userName(); + $hasLinkedIn = rand(0, 9) >= 7; + $hasMatrix = rand(0, 9) >= 7; + $hasGithub = rand(0, 9) >= 7; + + $contact = [ + 'is_primary' => $this->faker->boolean(30), 'salutation' => $this->faker->title($gender), 'first_name' => $this->faker->firstName($gender), 'last_name' => $this->faker->lastName($gender), + 'job_title' => $this->faker->jobTitle(), 'email' => $this->faker->unique()->safeEmail(), 'phone' => $this->faker->phoneNumber(), - 'position' => $this->faker->jobTitle(), - 'is_primary' => $this->faker->boolean(30), + 'mobile_phone' => $this->faker->phoneNumber(), 'avatar' => $avatar, + 'online_accounts' => [] ]; + + if ($hasLinkedIn) $contact[] = [ + 'platform' => 'linkedin', + 'user_name' => $userName, + 'url' => 'https://www.linkedin.com/in/' . $userName + ]; + if ($hasMatrix) $contact[] = [ + 'platform' => 'matrix', + 'user_name' => $userName, + 'url' => '@' . $userName . ':matrix.org' + ]; + if ($hasGithub) $contact[] = [ + 'platform' => 'github', + 'user_name' => $userName, + 'url' => 'https://www.github.com/' . $userName + ]; + + return $contact; } } diff --git a/database/migrations/2025_10_06_122048_create_contacts_table.php b/database/migrations/2025_10_06_122048_create_contacts_table.php index bd5f75e..ef621e7 100644 --- a/database/migrations/2025_10_06_122048_create_contacts_table.php +++ b/database/migrations/2025_10_06_122048_create_contacts_table.php @@ -11,14 +11,18 @@ public function up() Schema::create('contacts', function (Blueprint $table) { $table->id(); $table->foreignId('customer_id')->constrained()->onDelete('cascade'); + $table->boolean('is_primary')->default(false); $table->string('salutation', 20); + $table->string('academic_title', 20)->nullable(); $table->string('first_name', 50); $table->string('last_name', 50); + $table->string('job_title', 100)->nullable(); $table->string('email', 100)->nullable(); $table->string('phone', 20)->nullable(); - $table->string('position', 100)->nullable(); - $table->boolean('is_primary')->default(false); + $table->string('mobile_phone', 20)->nullable(); $table->string('avatar')->nullable(); + $table->json('online_accounts')->nullable(); + $table->text('notes')->nullable(); $table->timestamps(); }); } diff --git a/resources/js/pages/Customers.vue b/resources/js/pages/Customers.vue index b8744bd..7df507e 100644 --- a/resources/js/pages/Customers.vue +++ b/resources/js/pages/Customers.vue @@ -13,7 +13,7 @@ import { Input } from '@/components/ui/input' import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' import { ButtonGroup, ButtonGroupSeparator, ButtonGroupText, } from '@/components/ui/button-group' -import { Copy, Delete, Edit, Globe, House, LayoutGrid, LayoutList, Phone, Plus, Rows3, Rows4, Search } from "lucide-vue-next" +import { Copy, Delete, Edit, Globe, House, LayoutGrid, LayoutList, Mail, Phone, Plus, Rows3, Rows4, Search, Smartphone } from "lucide-vue-next" import Fuse from 'fuse.js'; import { getInitials } from '@/composables/useInitials'; import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, } from '@/components/ui/card' @@ -91,9 +91,30 @@ const showDetail = (customer: Customer) => { detailDialogOpen.value = true } +const mail = (email: string, event: Event) => { + event.stopPropagation(); + window.open('mailto:' + email, '_self') +} + const browse = (url: string, event: Event) => { event.stopPropagation(); - window.open(url, '_blank') + + if (!/^https?:\/\//i.test(url)) { + url = 'https://' + url; + } + + try { + const parsedUrl = new URL(url); + if (!parsedUrl.hostname || parsedUrl.hostname === 'sers') { + throw new Error('Ungültiger Hostname'); + } + + window.open(url, '_blank'); + } catch (e) { + toast.error('Ungültige URL', { + description: 'Die eingegebene URL ist nicht gültig.' + }); + } } const call = (number: string, event: Event) => { @@ -213,16 +234,39 @@ const call = (number: string, event: Event) => { - + {{ getInitials(contact.firstName + ' ' + contact.lastName) }} - -

{{ contact.firstName + ' ' + contact.lastName }}

+ +

+ {{ contact.academicTitle }} + {{ contact.firstName + ' ' + contact.lastName }} +

+

{{ contact.jobTitle }}

+ + + + + + +
diff --git a/resources/js/types/index.d.ts b/resources/js/types/index.d.ts index 2de035a..3c4c435 100644 --- a/resources/js/types/index.d.ts +++ b/resources/js/types/index.d.ts @@ -70,16 +70,26 @@ export function newAddress(): Address { } } +export interface OnlineAccount { + platform: string; + userName: string; + url: string; +} + export interface Contact { id: number; + isPrimary?: boolean; salutation: string; + academicTitle?: string; firstName: string; lastName: string; + jobTitle?: string; email?: string; phone?: string; - position?: string; - isPrimary?: boolean; + mobilePhone?: string; avatar?: string; + notes?: string; + onlineAccounts?: OnlineAccount[]; } export type ContactType = Contact @@ -87,14 +97,18 @@ export type ContactType = Contact export function newContact(): Contact { return { id: 0, + isPrimary: false, salutation: '', + academicTitle: null, firstName: '', lastName: '', + jobTitle: '', email: '', phone: '', - position: '', - isPrimary: false, - avatar: '' + mobilePhone: '', + avatar: '', + notes: '', + onlineAccounts: [] } }