add templates, update styles

This commit is contained in:
Casey 2026-01-27 16:34:06 -06:00
parent 8542a9bf37
commit 8452f57787
6 changed files with 362 additions and 153 deletions

View file

@ -4,6 +4,26 @@
<!-- Lead Badge -->
<div v-if="isLead" class="lead-badge-container">
<Badge value="LEAD" severity="warn" size="large" />
<div class="action-buttons">
<v-btn
size="small"
variant="outlined"
color="primary"
@click="addAddress"
>
<v-icon left size="small">mdi-map-marker-plus</v-icon>
Add Address
</v-btn>
<v-btn
size="small"
variant="outlined"
color="primary"
@click="addContact"
>
<v-icon left size="small">mdi-account-plus</v-icon>
Add Contact
</v-btn>
</div>
</div>
<!-- Client Name (only show for Company type) -->
@ -145,35 +165,56 @@ const formattedCreationDate = computed(() => {
day: "numeric",
});
});
// Placeholder methods for adding address and contact
const addAddress = () => {
console.log("Add Address modal would open here");
// TODO: Open add address modal
};
const addContact = () => {
console.log("Add Contact modal would open here");
// TODO: Open add contact modal
};
</script>
<style scoped>
.general-client-info {
background: var(--surface-card);
border-radius: 8px;
padding: 0.75rem 1rem;
padding: 0.5rem 0.75rem;
border: 1px solid var(--surface-border);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
margin-bottom: 0.75rem;
}
.lead-badge-container {
display: inline-flex;
margin-left: 0.5rem;
vertical-align: middle;
grid-column: 1 / -1;
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.25rem;
}
.action-buttons {
display: flex;
gap: 0.5rem;
}
.info-grid {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 1rem 2rem;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.75rem 1.5rem;
align-items: start;
}
.info-section {
display: inline-flex;
display: grid;
grid-template-columns: 120px 1fr;
align-items: center;
gap: 0.5rem;
gap: 0.75rem;
min-height: 2rem;
padding: 0.25rem 0;
}
.info-section label {
@ -182,12 +223,18 @@ const formattedCreationDate = computed(() => {
color: var(--text-color-secondary);
text-transform: uppercase;
letter-spacing: 0.3px;
justify-self: start;
align-self: center;
margin: 0;
}
.info-value {
font-size: 0.85rem;
color: var(--text-color);
font-weight: 500;
justify-self: start;
align-self: center;
margin: 0;
}
.info-value.large {
@ -197,30 +244,36 @@ const formattedCreationDate = computed(() => {
}
.companies-list {
display: inline-flex;
display: flex;
flex-wrap: wrap;
gap: 0.25rem;
justify-content: flex-start;
align-items: center;
}
.primary-contact {
display: flex;
align-items: center;
gap: 0.5rem;
grid-column: span 1;
justify-self: stretch;
}
.contact-details {
display: inline-flex;
display: flex;
flex-wrap: wrap;
gap: 1rem;
padding: 0.35rem 0.75rem;
padding: 0.5rem 0.75rem;
background: var(--surface-ground);
border-radius: 4px;
width: 100%;
justify-content: flex-start;
align-items: center;
}
.contact-item {
display: inline-flex;
display: flex;
align-items: center;
gap: 0.35rem;
min-width: 0;
flex: 1;
}
.contact-item i {
@ -234,23 +287,29 @@ const formattedCreationDate = computed(() => {
}
.stats {
display: inline-flex;
align-items: center;
gap: 0.5rem;
grid-column: span 1;
justify-self: stretch;
}
.stats-grid {
display: inline-flex;
display: flex;
gap: 1rem;
padding: 0.35rem 0.75rem;
padding: 0.5rem 0.75rem;
background: var(--surface-ground);
border-radius: 4px;
width: 100%;
justify-content: space-around;
align-items: center;
flex-wrap: wrap;
}
.stat-item {
display: inline-flex;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.35rem;
gap: 0.25rem;
text-align: center;
min-width: 60px;
}
.stat-item i {
@ -259,14 +318,18 @@ const formattedCreationDate = computed(() => {
}
.stat-value {
font-size: 0.95rem;
font-size: 1.1rem;
font-weight: 600;
color: var(--text-color);
line-height: 1.2;
}
.stat-label {
font-size: 0.75rem;
font-size: 0.7rem;
color: var(--text-color-secondary);
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.3px;
line-height: 1.2;
}
</style>

View file

@ -4,7 +4,7 @@
<div class="details-grid">
<!-- Address Information -->
<div class="detail-section full-width">
<div class="detail-section">
<div class="section-header">
<i class="pi pi-map-marker"></i>
<h4>Address</h4>
@ -31,69 +31,95 @@
</div>
</div>
<!-- Contacts Section -->
<div class="detail-section full-width">
<!-- Associated Companies -->
<div class="detail-section">
<div class="section-header">
<i class="pi pi-building"></i>
<h4>Companies</h4>
</div>
<div v-if="associatedCompanies.length > 0" class="companies-list">
<div
v-for="company in associatedCompanies"
:key="company"
class="company-item"
>
<i class="pi pi-building"></i>
<span>{{ company }}</span>
</div>
</div>
<div v-else class="empty-state">
<i class="pi pi-building"></i>
<p>No companies associated</p>
</div>
</div>
<!-- Primary Contact -->
<div class="detail-section">
<div class="section-header">
<i class="pi pi-user"></i>
<h4>Primary Contact</h4>
</div>
<div v-if="primaryContact" class="contact-card primary">
<div class="contact-badge">
<Badge value="Primary" severity="success" />
</div>
<div class="contact-info">
<h5>{{ primaryContactName }}</h5>
<div class="contact-details">
<div class="contact-detail">
<i class="pi pi-envelope"></i>
<span>{{ primaryContactEmail }}</span>
</div>
<div class="contact-detail">
<i class="pi pi-phone"></i>
<span>{{ primaryContactPhone }}</span>
</div>
<div v-if="primaryContact.role" class="contact-detail">
<i class="pi pi-briefcase"></i>
<span>{{ primaryContact.role }}</span>
</div>
</div>
</div>
</div>
<div v-else class="empty-state">
<i class="pi pi-user-minus"></i>
<p>No primary contact</p>
</div>
</div>
<!-- Other Contacts -->
<div class="detail-section">
<div class="section-header">
<i class="pi pi-users"></i>
<h4>Contacts</h4>
<h4>Other Contacts</h4>
</div>
<!-- Display Mode -->
<div v-if="!editMode" class="contacts-display">
<template v-if="addressContacts.length > 0">
<!-- Primary Contact -->
<div v-if="primaryContact" class="contact-card primary">
<div class="contact-badge">
<Badge value="Primary" severity="success" />
</div>
<div class="contact-info">
<h5>{{ primaryContactName }}</h5>
<div class="contact-details">
<div class="contact-detail">
<i class="pi pi-envelope"></i>
<span>{{ primaryContactEmail }}</span>
</div>
<div class="contact-detail">
<i class="pi pi-phone"></i>
<span>{{ primaryContactPhone }}</span>
</div>
<div v-if="primaryContact.role" class="contact-detail">
<i class="pi pi-briefcase"></i>
<span>{{ primaryContact.role }}</span>
</div>
</div>
</div>
<div v-if="otherContacts.length > 0" class="contacts-grid">
<div
v-for="contact in otherContacts"
:key="contact.name"
class="contact-card small"
>
<div class="contact-info-compact">
<span class="contact-name">{{ getContactName(contact) }}</span>
<span class="contact-email">{{ getContactEmail(contact) }}</span>
<span class="contact-phone">{{ getContactPhone(contact) }}</span>
<span v-if="contact.role" class="contact-role">{{ contact.role }}</span>
</div>
<!-- Other Contacts -->
<div v-if="otherContacts.length > 0" class="other-contacts">
<h6>Other Contacts</h6>
<div class="contacts-grid">
<div
v-for="contact in otherContacts"
:key="contact.name"
class="contact-card small"
>
<div class="contact-info-compact">
<span class="contact-name">{{ getContactName(contact) }}</span>
<span class="contact-email">{{ getContactEmail(contact) }}</span>
<span class="contact-phone">{{ getContactPhone(contact) }}</span>
<span v-if="contact.role" class="contact-role">{{ contact.role }}</span>
</div>
</div>
</div>
</div>
</template>
<template v-else>
<div class="empty-state">
<i class="pi pi-user-minus"></i>
<p>No contacts associated with this address</p>
</div>
</template>
</div>
</div>
<div v-else class="empty-state">
<i class="pi pi-user-minus"></i>
<p>No other contacts</p>
</div>
</div>
<!-- Edit Mode -->
<div v-else class="contacts-edit">
<!-- Edit Mode -->
<div v-if="editMode" class="detail-section full-width">
<div class="section-header">
<i class="pi pi-pencil"></i>
<h4>Edit Contacts</h4>
</div>
<div class="contacts-edit">
<div class="edit-instructions">
<i class="pi pi-info-circle"></i>
<span>Select contacts to associate with this address. One must be marked as primary.</span>
@ -133,28 +159,6 @@
</div>
</div>
<!-- Companies Section -->
<div class="detail-section full-width">
<div class="section-header">
<i class="pi pi-building"></i>
<h4>Associated Companies</h4>
</div>
<div v-if="associatedCompanies.length > 0" class="companies-list">
<div
v-for="company in associatedCompanies"
:key="company"
class="company-item"
>
<i class="pi pi-building"></i>
<span>{{ company }}</span>
</div>
</div>
<div v-else class="empty-state">
<i class="pi pi-building"></i>
<p>No companies associated with this address</p>
</div>
</div>
<!-- Map Section -->
<div class="detail-section full-width">
<div class="section-header">
@ -327,52 +331,53 @@ const emitChanges = () => {
.property-details {
background: var(--surface-card);
border-radius: 12px;
padding: 1rem;
padding: 0.75rem;
border: 1px solid var(--surface-border);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
margin-bottom: 1rem;
}
.property-details > h3 {
margin: 0 0 1rem 0;
font-size: 1.5rem;
margin: 0 0 0.75rem 0;
font-size: 1.25rem;
font-weight: 600;
color: var(--text-color);
}
.details-grid {
display: flex;
flex-direction: column;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
align-items: start;
}
.detail-section {
background: var(--surface-ground);
border-radius: 8px;
padding: 1rem;
padding: 0.75rem;
}
.detail-section.full-width {
width: 100%;
grid-column: span 2;
}
.section-header {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 0.75rem;
padding-bottom: 0.75rem;
border-bottom: 2px solid var(--surface-border);
gap: 0.5rem;
margin-bottom: 0.5rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--surface-border);
}
.section-header i {
font-size: 1.25rem;
font-size: 1rem;
color: var(--primary-color);
}
.section-header h4 {
margin: 0;
font-size: 1.1rem;
font-size: 1rem;
font-weight: 600;
color: var(--text-color);
}
@ -380,11 +385,11 @@ const emitChanges = () => {
.address-info {
display: flex;
flex-direction: column;
gap: 0.75rem;
gap: 0.5rem;
}
.full-address {
font-size: 1.1rem;
font-size: 1rem;
font-weight: 500;
color: var(--text-color);
margin: 0;
@ -400,13 +405,13 @@ const emitChanges = () => {
.contacts-display {
display: flex;
flex-direction: column;
gap: 1.5rem;
gap: 1rem;
}
.contact-card {
background: var(--surface-card);
border-radius: 8px;
padding: 1.25rem;
border-radius: 6px;
padding: 0.75rem;
border: 1px solid var(--surface-border);
}
@ -416,12 +421,12 @@ const emitChanges = () => {
}
.contact-badge {
margin-bottom: 0.75rem;
margin-bottom: 0.5rem;
}
.contact-info h5 {
margin: 0 0 0.75rem 0;
font-size: 1.25rem;
margin: 0 0 0.5rem 0;
font-size: 1.1rem;
font-weight: 600;
color: var(--text-color);
}
@ -429,30 +434,30 @@ const emitChanges = () => {
.contact-details {
display: flex;
flex-direction: column;
gap: 0.5rem;
gap: 0.25rem;
}
.contact-detail {
display: flex;
align-items: center;
gap: 0.75rem;
gap: 0.5rem;
}
.contact-detail i {
font-size: 1rem;
font-size: 0.9rem;
color: var(--primary-color);
min-width: 20px;
min-width: 18px;
}
.contact-detail span {
font-size: 0.95rem;
font-size: 0.9rem;
color: var(--text-color);
}
/* Other Contacts */
.other-contacts h6 {
margin: 0 0 1rem 0;
font-size: 0.9rem;
margin: 0 0 0.75rem 0;
font-size: 0.85rem;
font-weight: 600;
color: var(--text-color-secondary);
text-transform: uppercase;
@ -461,12 +466,12 @@ const emitChanges = () => {
.contacts-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 1rem;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 0.75rem;
}
.contact-card.small {
padding: 1rem;
padding: 0.75rem;
}
.contact-info-compact {
@ -581,27 +586,27 @@ const emitChanges = () => {
.companies-list {
display: flex;
flex-direction: column;
gap: 0.75rem;
gap: 0.5rem;
}
.company-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem;
gap: 0.5rem;
padding: 0.5rem;
background: var(--surface-card);
border-radius: 6px;
border-radius: 4px;
border: 1px solid var(--surface-border);
}
.company-item i {
font-size: 1rem;
font-size: 0.9rem;
color: var(--primary-color);
min-width: 20px;
min-width: 18px;
}
.company-item span {
font-size: 0.95rem;
font-size: 0.9rem;
color: var(--text-color);
font-weight: 500;
}

View file

@ -196,6 +196,16 @@ const getClient = async (name) => {
} else if (selectedAddress.value) {
// geocode.value = await Api.getGeocode(selectedAddress.value);
}
// Check if client is associated with current company
if (companyStore.currentCompany && client.value.companies) {
const clientHasCompany = client.value.companies.some(company => company.company === companyStore.currentCompany);
if (!clientHasCompany) {
notificationStore.addWarning(
`The selected company is not linked to this client.`,
);
}
}
} catch (error) {
console.error("Error fetching client data in Client.vue: ", error.message || error);
} finally {
@ -251,15 +261,26 @@ watch(
() => companyStore.currentCompany,
(newCompany) => {
console.log("############# Company changed to:", newCompany);
let companyIsPresent = false
for (company of selectedAddressData.value.companies || []) {
console.log("Checking address company:", company);
if (company.company === newCompany) {
companyIsPresent = true;
break;
}
if (!newCompany || !client.value.customerName) return;
// Check if client is associated with the company
let clientHasCompany = false;
if (client.value.companies) {
clientHasCompany = client.value.companies.some(company => company.company === newCompany);
}
if (!companyIsPresent) {
// Check if selected address is associated with the company
let addressHasCompany = false;
if (selectedAddressData.value?.companies) {
addressHasCompany = selectedAddressData.value.companies.some(company => company.company === newCompany);
}
// Show warnings for missing associations
if (!clientHasCompany) {
notificationStore.addWarning(
`The selected company is not linked to this client.`,
);
} else if (!addressHasCompany) {
notificationStore.addWarning(
`The selected company is not linked to this address.`,
);