Updated A LOT
This commit is contained in:
253
src/views/ContactsView.vue
Normal file
253
src/views/ContactsView.vue
Normal file
@@ -0,0 +1,253 @@
|
||||
<template>
|
||||
<section class="content-grid">
|
||||
<v-card class="span-12 panel-pad">
|
||||
<div class="toolbar-row mb-3">
|
||||
<div>
|
||||
<h2 class="section-title">Contacts</h2>
|
||||
<div class="quiet text-body-2">Employees, vendors, service providers, contractors, work locations, and more.</div>
|
||||
</div>
|
||||
<v-spacer />
|
||||
<v-select v-model="typeFilter" :items="typeFilterItems" item-title="title" item-value="value" label="Type" density="compact" hide-details style="max-width:200px" @update:model-value="load" />
|
||||
<v-btn color="primary" prepend-icon="mdi-account-plus" @click="openNew">New contact</v-btn>
|
||||
</div>
|
||||
|
||||
<DataTable
|
||||
:columns="columns"
|
||||
:rows="contacts"
|
||||
row-key="id"
|
||||
:page-size="15"
|
||||
has-actions
|
||||
searchable
|
||||
search-placeholder="Filter contacts"
|
||||
empty-text="No contacts yet."
|
||||
>
|
||||
<template #cell-display_name="{ row }">
|
||||
<span class="font-weight-bold">{{ row.display_name }}</span>
|
||||
<v-chip v-if="row.source === 'workday'" size="x-small" variant="tonal" class="ml-2">Workday</v-chip>
|
||||
</template>
|
||||
<template #cell-type="{ row }">{{ typeLabel(row.type) }}</template>
|
||||
<template #actions="{ row }">
|
||||
<v-btn icon="mdi-pencil" size="small" variant="text" @click="openEdit(row)" />
|
||||
<v-btn v-if="canDelete" icon="mdi-delete-outline" size="small" variant="text" color="error" @click="remove(row)" />
|
||||
</template>
|
||||
</DataTable>
|
||||
<v-alert v-if="error" type="error" density="compact" class="mt-3">{{ error }}</v-alert>
|
||||
</v-card>
|
||||
|
||||
<v-dialog v-model="dialog" max-width="760" scrollable>
|
||||
<v-card>
|
||||
<v-card-title>{{ form.id ? 'Edit contact' : 'New contact' }}</v-card-title>
|
||||
<v-card-text>
|
||||
<div class="form-grid">
|
||||
<v-select v-model="form.type" :items="contactTypeItems" item-title="title" item-value="value" label="Type" />
|
||||
<v-select v-model="form.status" :items="['active', 'inactive']" label="Status" />
|
||||
|
||||
<template v-if="showName">
|
||||
<v-text-field v-model="form.first_name" label="First name" />
|
||||
<v-text-field v-model="form.last_name" label="Last name" />
|
||||
</template>
|
||||
<v-text-field v-if="showOrg" v-model="form.organization" class="full" :label="form.type === 'work_location' ? 'Location name' : 'Organization'" />
|
||||
|
||||
<v-text-field v-model="form.email" label="Email" />
|
||||
<v-text-field v-model="form.phone" label="Phone (international)" placeholder="+1 555-0100" />
|
||||
|
||||
<!-- Employee / contractor job info -->
|
||||
<template v-if="showJob">
|
||||
<v-text-field v-if="form.type === 'employee'" v-model="form.employee_id" label="Employee ID" />
|
||||
<v-text-field v-if="form.type === 'contractor'" v-model="form.tax_id" label="Tax ID / business license #" />
|
||||
<v-text-field v-model="form.department" label="Department" />
|
||||
<v-text-field v-model="form.work_location" label="Work location" />
|
||||
<v-text-field v-model="form.start_date" type="date" label="Start date" />
|
||||
<v-text-field v-model="form.manager_first_name" label="Manager first name" />
|
||||
<v-text-field v-model="form.manager_last_name" label="Manager last name" />
|
||||
<v-text-field v-model="form.manager_email" label="Manager email" />
|
||||
</template>
|
||||
|
||||
<!-- Vendor / service provider -->
|
||||
<template v-if="showVendor">
|
||||
<div>
|
||||
<div class="quiet text-caption mb-1">Rating</div>
|
||||
<v-rating v-model="form.rating" length="5" color="amber" active-color="amber" half-increments="false" density="compact" clearable />
|
||||
</div>
|
||||
<v-select v-model="form.credit_term" :items="creditTermItems" item-title="title" item-value="value" label="Credit term" />
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<v-divider class="my-4" />
|
||||
<h3 class="section-title mb-2">Address</h3>
|
||||
<div class="form-grid">
|
||||
<v-text-field v-model="form.address_line1" class="full" label="Address line 1" />
|
||||
<v-text-field v-model="form.address_line2" class="full" label="Address line 2" />
|
||||
<v-text-field v-model="form.city" label="City / locality" />
|
||||
<v-text-field v-model="form.state_region" label="State / region / province" />
|
||||
<v-text-field v-model="form.postal_code" label="Postal code" />
|
||||
<v-text-field v-model="form.country" label="Country" />
|
||||
</div>
|
||||
|
||||
<v-textarea v-model="form.notes" class="mt-3" label="Notes" rows="2" />
|
||||
|
||||
<template v-if="form.id">
|
||||
<v-divider class="my-4" />
|
||||
<div class="toolbar-row mb-2">
|
||||
<h3 class="section-title">Notes log</h3>
|
||||
</div>
|
||||
<div class="toolbar-row">
|
||||
<v-text-field v-model="noteDraft" label="Add a note" density="compact" hide-details @keyup.enter="addNote" />
|
||||
<v-btn variant="tonal" prepend-icon="mdi-plus" :disabled="!noteDraft.trim()" @click="addNote">Add</v-btn>
|
||||
</div>
|
||||
<v-list v-if="form.contact_notes && form.contact_notes.length" density="compact">
|
||||
<v-list-item v-for="note in form.contact_notes" :key="note.id">
|
||||
<v-list-item-title style="white-space:normal">{{ note.body }}</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ note.author || 'Unknown' }} · {{ note.created_at }}</v-list-item-subtitle>
|
||||
<template #append>
|
||||
<v-btn icon="mdi-delete-outline" size="x-small" variant="text" color="error" @click="deleteNote(note.id)" />
|
||||
</template>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</template>
|
||||
|
||||
<v-alert v-if="dialogError" type="error" density="compact" class="mt-3">{{ dialogError }}</v-alert>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn variant="text" @click="dialog = false">Cancel</v-btn>
|
||||
<v-btn color="primary" prepend-icon="mdi-content-save" :loading="saving" @click="save">Save contact</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import DataTable from '../components/DataTable.vue';
|
||||
import { apiRequest } from '../services/api';
|
||||
import { contactTypeItems, creditTermItems } from '../constants';
|
||||
import { clonePlain } from '../utils/clone';
|
||||
|
||||
function blankContact() {
|
||||
return {
|
||||
id: null, type: 'employee', status: 'active', first_name: '', last_name: '', organization: '',
|
||||
email: '', phone: '', address_line1: '', address_line2: '', city: '', state_region: '', postal_code: '', country: '',
|
||||
notes: '', employee_id: '', tax_id: '', department: '', work_location: '',
|
||||
manager_first_name: '', manager_last_name: '', manager_email: '', start_date: '', rating: null, credit_term: 'na',
|
||||
contact_notes: []
|
||||
};
|
||||
}
|
||||
|
||||
export default {
|
||||
components: { DataTable },
|
||||
props: {
|
||||
token: { type: String, required: true },
|
||||
canDelete: { type: Boolean, default: false }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
contactTypeItems,
|
||||
creditTermItems,
|
||||
contacts: [],
|
||||
typeFilter: null,
|
||||
dialog: false,
|
||||
form: blankContact(),
|
||||
noteDraft: '',
|
||||
saving: false,
|
||||
error: '',
|
||||
dialogError: '',
|
||||
columns: [
|
||||
{ key: 'display_name', label: 'Name / organization' },
|
||||
{ key: 'type', label: 'Type' },
|
||||
{ key: 'email', label: 'Email' },
|
||||
{ key: 'phone', label: 'Phone' },
|
||||
{ key: 'department', label: 'Department' },
|
||||
{ key: 'work_location', label: 'Location' }
|
||||
]
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
typeFilterItems() {
|
||||
return [{ title: 'All types', value: null }, ...contactTypeItems];
|
||||
},
|
||||
showName() {
|
||||
return ['employee', 'contractor', 'other'].includes(this.form.type);
|
||||
},
|
||||
showOrg() {
|
||||
return ['vendor', 'service_provider', 'work_location', 'other'].includes(this.form.type);
|
||||
},
|
||||
showJob() {
|
||||
return ['employee', 'contractor'].includes(this.form.type);
|
||||
},
|
||||
showVendor() {
|
||||
return ['vendor', 'service_provider'].includes(this.form.type);
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.load();
|
||||
},
|
||||
methods: {
|
||||
async api(path, options = {}) {
|
||||
return apiRequest(path, this.token, options);
|
||||
},
|
||||
typeLabel(type) {
|
||||
return (contactTypeItems.find((t) => t.value === type) || {}).title || type;
|
||||
},
|
||||
async load() {
|
||||
this.error = '';
|
||||
try {
|
||||
const query = this.typeFilter ? `?type=${this.typeFilter}` : '';
|
||||
const data = await (await this.api(`/api/contacts${query}`)).json();
|
||||
this.contacts = data.contacts;
|
||||
} catch (error) {
|
||||
this.error = error.message;
|
||||
}
|
||||
},
|
||||
openNew() {
|
||||
this.form = blankContact();
|
||||
this.dialogError = '';
|
||||
this.noteDraft = '';
|
||||
this.dialog = true;
|
||||
},
|
||||
async openEdit(contact) {
|
||||
this.dialogError = '';
|
||||
this.noteDraft = '';
|
||||
const data = await (await this.api(`/api/contacts/${contact.id}`)).json();
|
||||
this.form = { ...blankContact(), ...clonePlain(data.contact) };
|
||||
this.dialog = true;
|
||||
},
|
||||
async save() {
|
||||
this.saving = true;
|
||||
this.dialogError = '';
|
||||
try {
|
||||
const method = this.form.id ? 'PUT' : 'POST';
|
||||
const url = this.form.id ? `/api/contacts/${this.form.id}` : '/api/contacts';
|
||||
const data = await (await this.api(url, { method, body: JSON.stringify(this.form) })).json();
|
||||
this.form = { ...blankContact(), ...data.contact };
|
||||
await this.load();
|
||||
} catch (error) {
|
||||
this.dialogError = error.message;
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
},
|
||||
async remove(contact) {
|
||||
if (!window.confirm(`Delete ${contact.display_name}?`)) return;
|
||||
try {
|
||||
await this.api(`/api/contacts/${contact.id}`, { method: 'DELETE' });
|
||||
await this.load();
|
||||
} catch (error) {
|
||||
this.error = error.message;
|
||||
}
|
||||
},
|
||||
async addNote() {
|
||||
const body = this.noteDraft.trim();
|
||||
if (!body || !this.form.id) return;
|
||||
await this.api(`/api/contacts/${this.form.id}/notes`, { method: 'POST', body: JSON.stringify({ body }) });
|
||||
this.noteDraft = '';
|
||||
const data = await (await this.api(`/api/contacts/${this.form.id}`)).json();
|
||||
this.form.contact_notes = data.contact.contact_notes;
|
||||
},
|
||||
async deleteNote(noteId) {
|
||||
await this.api(`/api/contacts/${this.form.id}/notes/${noteId}`, { method: 'DELETE' });
|
||||
this.form.contact_notes = this.form.contact_notes.filter((n) => n.id !== noteId);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
Reference in New Issue
Block a user