Added more database fields for contacts. Worked on popovers in customers view. #3

This commit is contained in:
2025-11-04 15:17:23 +01:00
parent 82e04acc2c
commit 15b49f4d23
5 changed files with 139 additions and 20 deletions
+35 -2
View File
@@ -11,14 +11,18 @@ class Contact extends Model
protected $fillable = [ protected $fillable = [
'customer_id', 'customer_id',
'is_primary',
'salutation', 'salutation',
'academic_title',
'first_name', 'first_name',
'last_name', 'last_name',
'job_title',
'email', 'email',
'phone', 'phone',
'position', 'mobile_phone',
'is_primary',
'avatar', 'avatar',
'online_accounts',
'notes',
]; ];
public function customer() public function customer()
@@ -46,4 +50,33 @@ public function scopePrimaryFirst($query)
{ {
return $query->orderBy('is_primary', 'desc'); 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;
}
} }
+27 -3
View File
@@ -21,16 +21,40 @@ public function definition()
} }
$gender = rand(0, 9) >= 5 ? 'female' : 'male'; $gender = rand(0, 9) >= 5 ? 'female' : 'male';
$userName = $this->faker->userName();
$hasLinkedIn = rand(0, 9) >= 7;
$hasMatrix = rand(0, 9) >= 7;
$hasGithub = rand(0, 9) >= 7;
return [ $contact = [
'is_primary' => $this->faker->boolean(30),
'salutation' => $this->faker->title($gender), 'salutation' => $this->faker->title($gender),
'first_name' => $this->faker->firstName($gender), 'first_name' => $this->faker->firstName($gender),
'last_name' => $this->faker->lastName($gender), 'last_name' => $this->faker->lastName($gender),
'job_title' => $this->faker->jobTitle(),
'email' => $this->faker->unique()->safeEmail(), 'email' => $this->faker->unique()->safeEmail(),
'phone' => $this->faker->phoneNumber(), 'phone' => $this->faker->phoneNumber(),
'position' => $this->faker->jobTitle(), 'mobile_phone' => $this->faker->phoneNumber(),
'is_primary' => $this->faker->boolean(30),
'avatar' => $avatar, '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;
} }
} }
@@ -11,14 +11,18 @@ public function up()
Schema::create('contacts', function (Blueprint $table) { Schema::create('contacts', function (Blueprint $table) {
$table->id(); $table->id();
$table->foreignId('customer_id')->constrained()->onDelete('cascade'); $table->foreignId('customer_id')->constrained()->onDelete('cascade');
$table->boolean('is_primary')->default(false);
$table->string('salutation', 20); $table->string('salutation', 20);
$table->string('academic_title', 20)->nullable();
$table->string('first_name', 50); $table->string('first_name', 50);
$table->string('last_name', 50); $table->string('last_name', 50);
$table->string('job_title', 100)->nullable();
$table->string('email', 100)->nullable(); $table->string('email', 100)->nullable();
$table->string('phone', 20)->nullable(); $table->string('phone', 20)->nullable();
$table->string('position', 100)->nullable(); $table->string('mobile_phone', 20)->nullable();
$table->boolean('is_primary')->default(false);
$table->string('avatar')->nullable(); $table->string('avatar')->nullable();
$table->json('online_accounts')->nullable();
$table->text('notes')->nullable();
$table->timestamps(); $table->timestamps();
}); });
} }
+50 -6
View File
@@ -13,7 +13,7 @@ import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge' import { Badge } from '@/components/ui/badge'
import { ButtonGroup, ButtonGroupSeparator, ButtonGroupText, } from '@/components/ui/button-group' 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 Fuse from 'fuse.js';
import { getInitials } from '@/composables/useInitials'; import { getInitials } from '@/composables/useInitials';
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, } from '@/components/ui/card' import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, } from '@/components/ui/card'
@@ -91,9 +91,30 @@ const showDetail = (customer: Customer) => {
detailDialogOpen.value = true detailDialogOpen.value = true
} }
const mail = (email: string, event: Event) => {
event.stopPropagation();
window.open('mailto:' + email, '_self')
}
const browse = (url: string, event: Event) => { const browse = (url: string, event: Event) => {
event.stopPropagation(); 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) => { const call = (number: string, event: Event) => {
@@ -213,16 +234,39 @@ const call = (number: string, event: Event) => {
<Tooltip v-for="contact in customer.contacts"> <Tooltip v-for="contact in customer.contacts">
<TooltipTrigger> <TooltipTrigger>
<Avatar class="-mr-2 size-14 shadow"> <Avatar class="-mr-2 size-14 shadow">
<AvatarImage v-if="contact.avatar" <AvatarImage v-if="contact.avatar" :src="'/storage/uploads/' + contact.avatar"
:src="'/storage/uploads/' + contact.avatar" loading="lazy" /> loading="lazy" />
<AvatarFallback <AvatarFallback
:class="bgColorForString(getInitials(contact.firstName + ' ' + contact.lastName))"> :class="bgColorForString(getInitials(contact.firstName + ' ' + contact.lastName))">
{{ getInitials(contact.firstName + ' ' + contact.lastName) }} {{ getInitials(contact.firstName + ' ' + contact.lastName) }}
</AvatarFallback> </AvatarFallback>
</Avatar> </Avatar>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent class="p-4">
<p>{{ contact.firstName + ' ' + contact.lastName }}</p> <p class="font-bold">
<span v-if="contact.academicTitle">{{ contact.academicTitle }}</span>
<span>{{ contact.firstName + ' ' + contact.lastName }}</span>
</p>
<p v-if="contact.jobTitle" class="text-muted-foreground">{{ contact.jobTitle }}</p>
<ButtonGroup class="mt-4">
<Button size="sm" v-if="contact.email"
@click="(event: Event) => mail(contact.email as string, event)">
<Mail stroke-width="1.5" @click="" />
</Button>
<Button size="sm" v-if="contact.phone"
@click="(event: Event) => call(contact.phone as string, event)">
<Phone stroke-width="1.5" @click="" />
</Button>
<Button size="sm" v-if="contact.mobilePhone"
@click="(event: Event) => call(contact.mobilePhone as string, event)">
<Smartphone stroke-width="1.5" @click="" />
</Button>
<Button size="sm" v-for="account in contact.onlineAccounts"
@click="(event: Event) => browse(account.url as string, event)">
<span>{{ account.platform }}</span>
</Button>
</ButtonGroup>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
+19 -5
View File
@@ -70,16 +70,26 @@ export function newAddress(): Address {
} }
} }
export interface OnlineAccount {
platform: string;
userName: string;
url: string;
}
export interface Contact { export interface Contact {
id: number; id: number;
isPrimary?: boolean;
salutation: string; salutation: string;
academicTitle?: string;
firstName: string; firstName: string;
lastName: string; lastName: string;
jobTitle?: string;
email?: string; email?: string;
phone?: string; phone?: string;
position?: string; mobilePhone?: string;
isPrimary?: boolean;
avatar?: string; avatar?: string;
notes?: string;
onlineAccounts?: OnlineAccount[];
} }
export type ContactType = Contact export type ContactType = Contact
@@ -87,14 +97,18 @@ export type ContactType = Contact
export function newContact(): Contact { export function newContact(): Contact {
return { return {
id: 0, id: 0,
isPrimary: false,
salutation: '', salutation: '',
academicTitle: null,
firstName: '', firstName: '',
lastName: '', lastName: '',
jobTitle: '',
email: '', email: '',
phone: '', phone: '',
position: '', mobilePhone: '',
isPrimary: false, avatar: '',
avatar: '' notes: '',
onlineAccounts: []
} }
} }