custom_ui/frontend/src/components/pages/Client.vue

402 lines
11 KiB
Vue

<template>
<div class="client-page">
<!-- Client Header -->
<GeneralClientInfo
v-if="client.customerName"
:client-data="client"
/>
<AdditionalInfoBar :address="client.addresses[selectedAddressIdx]" v-if="client.customerName" />
<!-- Address Selector (only shows if multiple addresses) -->
<AddressSelector
v-if="!isNew && client.addresses && client.addresses.length > 1"
:addresses="client.addresses"
:selected-address-idx="selectedAddressIdx"
:contacts="client.contacts"
@update:selected-address-idx="handleAddressChange"
/>
<!-- Main Content Tabs -->
<Tabs value="0" class="overview-tabs">
<TabList>
<Tab value="0">Overview</Tab>
<Tab value="1">Projects</Tab>
<Tab value="2">Financials</Tab>
</TabList>
<TabPanels>
<!-- Overview Tab -->
<TabPanel value="0">
<Overview
:selected-address="selectedAddressData"
:all-contacts="client.contacts"
:edit-mode="editMode"
:is-new="isNew"
:full-address="fullAddress"
:client="client"
@edit-mode-enabled="enableEditMode"
@update:address-contacts="handleAddressContactsUpdate"
@update:primary-contact="handlePrimaryContactUpdate"
@update:client="handleClientUpdate"
/>
</TabPanel>
<!-- Projects Tab -->
<TabPanel value="1">
<div class="coming-soon-section">
<i class="pi pi-wrench"></i>
<h3>Projects</h3>
<p>Section coming soon</p>
</div>
</TabPanel>
<!-- Financials Tab -->
<TabPanel value="2">
<div class="coming-soon-section">
<i class="pi pi-dollar"></i>
<h3>Financials</h3>
<p>Section coming soon</p>
</div>
</TabPanel>
</TabPanels>
</Tabs>
<!-- Form Actions (for edit mode or new client) -->
<div class="form-actions" v-if="editMode || isNew">
<Button
@click="handleCancel"
label="Cancel"
severity="secondary"
:disabled="isSubmitting"
/>
<Button
@click="handleSubmit"
:label="isNew ? 'Create Client' : 'Save Changes'"
:loading="isSubmitting"
/>
</div>
</div>
</template>
<script setup>
import { computed, onMounted, ref, watch } from "vue";
import Tabs from "primevue/tabs";
import TabList from "primevue/tablist";
import Tab from "primevue/tab";
import TabPanels from "primevue/tabpanels";
import TabPanel from "primevue/tabpanel";
import Button from "primevue/button";
import Api from "../../api";
import { useRoute, useRouter } from "vue-router";
import { useLoadingStore } from "../../stores/loading";
import { useNotificationStore } from "../../stores/notifications-primevue";
import { useCompanyStore } from "../../stores/company";
import DataUtils from "../../utils";
import AddressSelector from "../clientView/AddressSelector.vue";
import GeneralClientInfo from "../clientView/GeneralClientInfo.vue";
import AdditionalInfoBar from "../clientView/AdditionalInfoBar.vue";
import Overview from "../clientView/Overview.vue";
const route = useRoute();
const router = useRouter();
const loadingStore = useLoadingStore();
const notificationStore = useNotificationStore();
const companyStore = useCompanyStore();
const address = (route.query.address || '').trim();
const clientName = route.query.client || null;
const isNew = computed(() => route.query.new === "true" || false);
const clientNames = ref([]);
const client = ref({});
const geocode = ref({});
const selectedAddress = ref(address);
const selectedAddressObject = computed(() =>
client.value.addresses?.find(
(addr) => DataUtils.calculateFullAddress(addr) === selectedAddress.value,
),
);
const addresses = computed(() => {
if (client.value && client.value.addresses) {
return client.value.addresses.map((addr) => DataUtils.calculateFullAddress(addr).trim());
}
return [];
});
const nextVisitDate = ref(null); // Placeholder, update as needed
// Tab and edit state
const editMode = ref(false);
const isSubmitting = ref(false);
const selectedAddressIdx = computed({
get: () => addresses.value.indexOf(selectedAddress.value),
set: (idx) => {
if (idx >= 0 && idx < addresses.value.length) {
selectedAddress.value = addresses.value[idx];
}
}
});
// Find the address data object that matches the selected address string
const selectedAddressData = computed(() => {
if (!client.value?.addresses || !selectedAddress.value) {
return null;
}
return client.value.addresses.find(
(addr) => DataUtils.calculateFullAddress(addr).trim() === selectedAddress.value.trim()
);
});
// Calculate full address for display
const fullAddress = computed(() => {
if (!selectedAddressData.value) return "N/A";
return DataUtils.calculateFullAddress(selectedAddressData.value);
});
const getClientNames = async (type) => {
loadingStore.setLoading(true);
try {
const names = await Api.getClientNames(type);
clientNames.value = names;
} catch (error) {
console.error("Error fetching client names in Client.vue: ", error.message || error);
} finally {
loadingStore.setLoading(false);
}
};
const getClient = async (name) => {
loadingStore.setLoading(true);
try {
const clientData = await Api.getClient(name);
client.value = clientData || {};
// Set initial selected address if provided in route or use first address
if (address && client.value.addresses) {
const fullAddresses = client.value.addresses.map((addr) =>
DataUtils.calculateFullAddress(addr).trim(),
);
const trimmedAddress = address.trim();
if (fullAddresses.includes(trimmedAddress)) {
selectedAddress.value = trimmedAddress;
} else if (fullAddresses.length > 0) {
selectedAddress.value = fullAddresses[0];
}
} else if (client.value.addresses && client.value.addresses.length > 0) {
selectedAddress.value = DataUtils.calculateFullAddress(client.value.addresses[0]);
}
if (
selectedAddressObject.value?.customLongitude &&
selectedAddressObject.value?.customLatitude
) {
geocode.value = {
longitude: selectedAddressObject.value.customLongitude || selectedAddressObject.value.longitude,
latitude: selectedAddressObject.value.customLatitude || selectedAddressObject.value.latitude,
};
} else if (selectedAddress.value) {
// geocode.value = await Api.getGeocode(selectedAddress.value);
}
} catch (error) {
console.error("Error fetching client data in Client.vue: ", error.message || error);
} finally {
loadingStore.setLoading(false);
}
};
onMounted(async () => {
if (clientName) {
await getClient(clientName);
console.log("Displaying existing client data");
}
console.debug(
"DEBUG: Client.vue mounted with clientName:",
clientName,
"isNew:",
isNew.value,
"address:",
address,
"addresses:",
addresses.value,
"selectedAddress:",
selectedAddress.value,
"Does selected address match an address in addresses?:",
selectedAddress.value && addresses.value.includes(selectedAddress.value),
"geocode:",
geocode.value,
);
});
watch(
() => route.query,
async (newQuery, oldQuery) => {
const clientName = newQuery.client || null;
const isNewClient = newQuery.new === "true" || false;
const address = newQuery.address || null;
// Clear client data if switching to new client mode
if (isNewClient) {
client.value = {};
selectedAddress.value = null;
geocode.value = {};
console.log("Switched to new client mode - cleared client data");
} else if (clientName && clientName !== oldQuery.client) {
// Load client data if switching to existing client
await getClient(clientName);
console.log("Route query changed - displaying existing client data");
}
},
);
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 (!companyIsPresent) {
notificationStore.addWarning(
`The selected company is not linked to this address.`,
);
}
}
)
// Handle address change
const handleAddressChange = (newIdx) => {
selectedAddressIdx.value = newIdx;
// TODO: Update route query with new address
};
// Enable edit mode
const enableEditMode = () => {
editMode.value = true;
};
// Handle cancel edit or new
const handleCancel = () => {
if (isNew.value) {
// For new client, clear the form data
client.value = {};
} else {
editMode.value = false;
// Restore original data if editing
}
};
// Handle save edit or create new
const handleSubmit = async () => {
isSubmitting.value = true;
try {
if (isNew.value) {
const createdClient = await Api.createClient(client.value);
console.log("Created client:", createdClient);
notificationStore.addSuccess("Client created successfully!");
stripped_name = createdClient.customerName.split("-#-")[0].trim();
// Navigate to the created client
router.push('/client?client=' + encodeURIComponent(stripped_name));
} else {
// TODO: Implement save logic
notificationStore.addSuccess("Changes saved successfully!");
editMode.value = false;
}
} catch (error) {
console.error("Error submitting:", error);
notificationStore.addError(isNew.value ? "Failed to create client" : "Failed to save changes");
} finally {
isSubmitting.value = false;
}
};
// Handle address contacts update
const handleAddressContactsUpdate = (contactNames) => {
console.log("Address contacts updated:", contactNames);
// TODO: Store this for saving
};
// Handle primary contact update
const handlePrimaryContactUpdate = (contactName) => {
console.log("Primary contact updated:", contactName);
// TODO: Store this for saving
};
// Handle client update from forms
const handleClientUpdate = (newClientData) => {
client.value = { ...client.value, ...newClientData };
};
</script>
<style lang="css">
.tab-info-alert {
background-color: #a95e46;
border-radius: 10px;
color: white;
padding-left: 5px;
padding-right: 5px;
padding-top: 2px;
padding-bottom: 2px;
}
.overview-tabs {
margin-bottom: 1rem;
}
.coming-soon-section {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 1rem;
padding: 4rem 2rem;
text-align: center;
background: var(--surface-card);
border-radius: 12px;
border: 1px solid var(--surface-border);
}
.coming-soon-section i {
font-size: 4rem;
color: var(--text-color-secondary);
opacity: 0.5;
}
.coming-soon-section h3 {
margin: 0;
font-size: 1.75rem;
font-weight: 600;
color: var(--text-color);
}
.coming-soon-section p {
margin: 0;
font-size: 1.1rem;
color: var(--text-color-secondary);
}
.client-page {
padding-bottom: 5rem; /* Add padding to prevent content from being hidden behind fixed buttons */
}
.form-actions {
position: fixed;
bottom: 0;
left: 0;
right: 0;
display: flex;
justify-content: flex-end;
gap: 1rem;
padding: 1.5rem;
background: var(--surface-card);
border-radius: 0;
border: 1px solid var(--surface-border);
border-bottom: none;
border-left: none;
border-right: none;
box-shadow: 0 -2px 4px rgba(0, 0, 0, 0.1);
z-index: 1000;
}
</style>