Added more database fields for contacts. Worked on popovers in customers view. #3
This commit is contained in:
+36
-3
@@ -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()
|
||||||
@@ -38,7 +42,7 @@ public function getAvatarUrlAttribute()
|
|||||||
// Return null if no avatar is set
|
// Return null if no avatar is set
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Scope a query to order contacts with primary contacts first.
|
* Scope a query to order contacts with primary contacts first.
|
||||||
*/
|
*/
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
return [
|
$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),
|
'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();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
Vendored
+19
-5
@@ -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: []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user