forked from trent_larson/crowd-funder-for-time-pwa
- Restore runMigrations functionality for database schema migrations - Remove indexedDBMigrationService.ts (was for IndexedDB to SQLite migration) - Recreate migrationService.ts and db-sql/migration.ts for schema management - Add proper TypeScript error handling with type guards in AccountViewView - Fix CreateAndSubmitClaimResult property access in QuickActionBvcBeginView - Remove LeafletMouseEvent from Vue components array (it's a type, not component) - Add null check for UserNameDialog callback to prevent undefined assignment - Implement extractErrorMessage helper function for consistent error handling - Update router to remove database-migration route The migration system now properly handles database schema evolution across app versions, while the IndexedDB to SQLite migration service has been removed as it was specific to that one-time migration.
362 lines
11 KiB
Vue
362 lines
11 KiB
Vue
<template>
|
|
<QuickNav selected="Contacts" />
|
|
<TopMessage />
|
|
|
|
<section id="ContactEdit" class="p-6 max-w-3xl mx-auto">
|
|
<div id="ViewBreadcrumb" class="mb-8">
|
|
<h1 class="text-4xl text-center font-light relative px-7">
|
|
<!-- Back -->
|
|
<button
|
|
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
|
|
@click="$router.go(-1)"
|
|
>
|
|
<font-awesome icon="chevron-left" class="fa-fw" />
|
|
</button>
|
|
{{ contact.name || AppString.NO_CONTACT_NAME }}
|
|
</h1>
|
|
</div>
|
|
|
|
<!-- Contact Name -->
|
|
<div class="mt-4 flex" data-testId="contactName">
|
|
<label
|
|
for="contactName"
|
|
class="block text-sm font-medium text-gray-700 mt-2"
|
|
>
|
|
Name
|
|
</label>
|
|
<input
|
|
v-model="contactName"
|
|
type="text"
|
|
class="block w-full ml-2 mt-1 border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Contact Notes -->
|
|
<div class="mt-4">
|
|
<label for="contactNotes" class="block text-sm font-medium text-gray-700">
|
|
Notes
|
|
</label>
|
|
<textarea
|
|
id="contactNotes"
|
|
v-model="contactNotes"
|
|
rows="4"
|
|
class="block w-full mt-1 border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500"
|
|
></textarea>
|
|
</div>
|
|
|
|
<!-- Contact Methods -->
|
|
<div class="mt-4">
|
|
<h2 class="text-lg font-medium text-gray-700">Contact Methods</h2>
|
|
<div
|
|
v-for="(method, index) in contactMethods"
|
|
:key="index"
|
|
class="flex mt-2"
|
|
>
|
|
<input
|
|
v-model="method.label"
|
|
type="text"
|
|
class="block w-1/4 border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500"
|
|
placeholder="Label"
|
|
/>
|
|
<input
|
|
v-model="method.type"
|
|
type="text"
|
|
class="block ml-2 w-1/4 border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500"
|
|
placeholder="Type"
|
|
/>
|
|
<div class="relative">
|
|
<button
|
|
class="px-2 py-1 bg-gray-200 rounded-md"
|
|
@click="toggleDropdown(index)"
|
|
>
|
|
<font-awesome icon="caret-down" class="fa-fw" />
|
|
</button>
|
|
<div
|
|
v-if="dropdownIndex === index"
|
|
class="absolute bg-white border border-gray-300 rounded-md mt-1"
|
|
>
|
|
<div
|
|
class="px-4 py-2 hover:bg-gray-100 cursor-pointer"
|
|
@click="setMethodType(index, 'CELL')"
|
|
>
|
|
CELL
|
|
</div>
|
|
<div
|
|
class="px-4 py-2 hover:bg-gray-100 cursor-pointer"
|
|
@click="setMethodType(index, 'EMAIL')"
|
|
>
|
|
EMAIL
|
|
</div>
|
|
<div
|
|
class="px-4 py-2 hover:bg-gray-100 cursor-pointer"
|
|
@click="setMethodType(index, 'WHATSAPP')"
|
|
>
|
|
WHATSAPP
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<input
|
|
v-model="method.value"
|
|
type="text"
|
|
class="block ml-2 w-1/2 border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500"
|
|
placeholder="Number, email, etc."
|
|
/>
|
|
<button class="ml-2 text-red-500" @click="removeContactMethod(index)">
|
|
<font-awesome icon="trash-can" class="fa-fw" />
|
|
</button>
|
|
</div>
|
|
<button class="mt-2" @click="addContactMethod">
|
|
<font-awesome
|
|
icon="plus"
|
|
class="fa-fw px-2 py-2.5 bg-green-500 text-green-100 rounded-full"
|
|
/>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Save Button -->
|
|
<div class="mt-8 flex justify-between">
|
|
<button
|
|
class="px-4 py-2 bg-blue-500 text-white rounded-md"
|
|
@click="saveEdit"
|
|
>
|
|
Save
|
|
</button>
|
|
<button
|
|
class="ml-4 px-4 py-2 bg-slate-500 text-white rounded-md"
|
|
@click="$router.go(-1)"
|
|
>
|
|
Cancel
|
|
</button>
|
|
</div>
|
|
</section>
|
|
</template>
|
|
|
|
<script lang="ts">
|
|
import * as R from "ramda";
|
|
import { Component, Vue } from "vue-facing-decorator";
|
|
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
|
|
|
|
import QuickNav from "../components/QuickNav.vue";
|
|
import TopMessage from "../components/TopMessage.vue";
|
|
import { NotificationIface } from "../constants/app";
|
|
import * as databaseUtil from "../db/databaseUtil";
|
|
import { parseJsonField } from "../db/databaseUtil";
|
|
import { db } from "../db/index";
|
|
import { PlatformServiceFactory } from "../services/PlatformServiceFactory";
|
|
import { Contact, ContactMethod } from "../db/tables/contacts";
|
|
import { AppString } from "../constants/app";
|
|
|
|
/**
|
|
* Contact Edit View Component
|
|
* @author Matthew Raymer
|
|
*
|
|
* This component provides a full-featured contact editing interface with support for:
|
|
* - Basic contact information (name, notes)
|
|
* - Multiple contact methods with type selection
|
|
* - Data validation and persistence
|
|
*
|
|
* Workflow:
|
|
* 1. Component loads with DID from route params
|
|
* 2. Fetches existing contact data from IndexedDB
|
|
* 3. Presents editable form with current values
|
|
* 4. Validates and saves updates back to database
|
|
*
|
|
* Contact Method Types:
|
|
* - CELL: Mobile phone numbers
|
|
* - EMAIL: Email addresses
|
|
* - WHATSAPP: WhatsApp contact info
|
|
*
|
|
* State Management:
|
|
* - Maintains separate state for form fields to prevent direct mutation
|
|
* - Handles array cloning for contact methods to prevent reference issues
|
|
* - Manages dropdown state for method type selection
|
|
*
|
|
* Navigation:
|
|
* - Back button returns to previous view
|
|
* - Save redirects to contact detail view
|
|
* - Cancel returns to previous view
|
|
* - Invalid DID redirects to contacts list
|
|
*/
|
|
@Component({
|
|
components: {
|
|
QuickNav,
|
|
TopMessage,
|
|
},
|
|
})
|
|
export default class ContactEditView extends Vue {
|
|
/** Notification function injected by Vue */
|
|
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
|
/** Current route instance */
|
|
$route!: RouteLocationNormalizedLoaded;
|
|
/** Router instance for navigation */
|
|
$router!: Router;
|
|
|
|
/** Current contact data */
|
|
contact: Contact | undefined = {
|
|
did: "",
|
|
name: "",
|
|
notes: "",
|
|
};
|
|
/** Editable contact name field */
|
|
contactName = "";
|
|
/** Editable contact notes field */
|
|
contactNotes = "";
|
|
/** Array of editable contact methods */
|
|
contactMethods: Array<ContactMethod> = [];
|
|
/** Currently open dropdown index, null if none open */
|
|
dropdownIndex: number | null = null;
|
|
|
|
/** App string constants */
|
|
AppString = AppString;
|
|
|
|
/**
|
|
* Component lifecycle hook that initializes the contact edit form
|
|
*
|
|
* Workflow:
|
|
* 1. Extracts DID from route parameters
|
|
* 2. Queries database for existing contact
|
|
* 3. Populates form fields with contact data
|
|
* 4. Handles missing contact error case
|
|
*
|
|
* @throws Will not throw but redirects on error
|
|
* @emits Notification on contact not found
|
|
* @emits Router navigation on error
|
|
*/
|
|
async created() {
|
|
const contactDid = this.$route.params.did;
|
|
const platformService = PlatformServiceFactory.getInstance();
|
|
const dbContact = await platformService.dbQuery(
|
|
"SELECT * FROM contacts WHERE did = ?",
|
|
[contactDid],
|
|
);
|
|
const contact: Contact | undefined = databaseUtil.mapQueryResultToValues(
|
|
dbContact,
|
|
)[0] as unknown as Contact;
|
|
contact.contactMethods = parseJsonField(contact?.contactMethods, []);
|
|
if (contact) {
|
|
this.contact = contact;
|
|
this.contactName = contact.name || "";
|
|
this.contactNotes = contact.notes || "";
|
|
this.contactMethods = contact.contactMethods || [];
|
|
} else {
|
|
this.$notify({
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Contact Not Found",
|
|
text: "There is no contact with DID " + contactDid,
|
|
});
|
|
(this.$router as Router).push({ path: "/contacts" });
|
|
return;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Adds a new empty contact method to the methods array
|
|
*
|
|
* Creates a new method object with empty fields for:
|
|
* - label: Custom label for the method
|
|
* - type: Communication type (CELL, EMAIL, WHATSAPP)
|
|
* - value: The contact information value
|
|
*/
|
|
addContactMethod() {
|
|
this.contactMethods.push({ label: "", type: "", value: "" });
|
|
}
|
|
|
|
/**
|
|
* Removes a contact method at the specified index
|
|
*
|
|
* @param index The array index of the method to remove
|
|
*/
|
|
removeContactMethod(index: number) {
|
|
this.contactMethods.splice(index, 1);
|
|
}
|
|
|
|
/**
|
|
* Toggles the type selection dropdown for a contact method
|
|
*
|
|
* If the clicked dropdown is already open, closes it.
|
|
* If another dropdown is open, closes it and opens the clicked one.
|
|
*
|
|
* @param index The array index of the method whose dropdown to toggle
|
|
*/
|
|
toggleDropdown(index: number) {
|
|
this.dropdownIndex = this.dropdownIndex === index ? null : index;
|
|
}
|
|
|
|
/**
|
|
* Sets the type for a contact method and closes the dropdown
|
|
*
|
|
* @param index The array index of the method to update
|
|
* @param type The new type value (CELL, EMAIL, WHATSAPP)
|
|
*/
|
|
setMethodType(index: number, type: string) {
|
|
this.contactMethods[index].type = type;
|
|
this.dropdownIndex = null;
|
|
}
|
|
|
|
/**
|
|
* Saves the edited contact information to the database
|
|
*
|
|
* Workflow:
|
|
* 1. Clones contact methods array to prevent reference issues
|
|
* 2. Normalizes method types to uppercase
|
|
* 3. Checks for changes in method types
|
|
* 4. Updates database with new values
|
|
* 5. Notifies user of success
|
|
* 6. Redirects to contact detail view
|
|
*
|
|
* @throws Will not throw but notifies on validation errors
|
|
* @emits Notification on type changes or success
|
|
* @emits Router navigation on success
|
|
*/
|
|
async saveEdit() {
|
|
// without this conversion, "Failed to execute 'put' on 'IDBObjectStore': [object Array] could not be cloned."
|
|
const contactMethodsObj = JSON.parse(JSON.stringify(this.contactMethods));
|
|
|
|
// Normalize method types to uppercase
|
|
const contactMethods = contactMethodsObj.map((method: ContactMethod) =>
|
|
R.set(R.lensProp("type"), method.type.toUpperCase(), method),
|
|
);
|
|
|
|
// Check for type changes
|
|
if (!R.equals(contactMethodsObj, contactMethods)) {
|
|
this.contactMethods = contactMethods;
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "warning",
|
|
title: "Contact Methods Updated",
|
|
text: "Note that some methods have been updated, such as uppercasing 'email' to 'EMAIL'. Save again if the changes are acceptable.",
|
|
},
|
|
15000,
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Save to database
|
|
const platformService = PlatformServiceFactory.getInstance();
|
|
const contactMethodsString = JSON.stringify(contactMethods);
|
|
await platformService.dbExec(
|
|
"UPDATE contacts SET name = ?, notes = ?, contactMethods = ? WHERE did = ?",
|
|
[
|
|
this.contactName,
|
|
this.contactNotes,
|
|
contactMethodsString,
|
|
this.contact?.did || "",
|
|
],
|
|
);
|
|
|
|
// Notify success and redirect
|
|
this.$notify({
|
|
group: "alert",
|
|
type: "success",
|
|
title: "Contact Saved",
|
|
text: "The contact info has been updated successfully.",
|
|
});
|
|
(this.$router as Router).push({
|
|
path: "/did/" + encodeURIComponent(this.contact?.did || ""),
|
|
});
|
|
}
|
|
}
|
|
</script>
|