From 9746703433c5bf0639669223dd2e8579c752dbf2 Mon Sep 17 00:00:00 2001 From: Samuel Helbling Date: Fri, 31 Jan 2025 08:38:52 +0100 Subject: [PATCH] Added customer and supplier page, improved contact page, added email upload --- crm/api/customer.py | 87 ++ crm/api/doc.py | 343 ++++++- crm/api/supplier.py | 77 ++ .../src/components/Contact/ContactHeader.vue | 54 + .../src/components/Contact/ContactProfile.vue | 85 ++ .../src/components/Contact/ContactTabs.vue | 172 ++++ frontend/src/components/EmailDropZone.vue | 140 +++ .../src/components/Icons/CustomerIcon.vue | 19 + frontend/src/components/Icons/DealIcon.vue | 17 + frontend/src/components/Icons/ImageIcon.vue | 18 + frontend/src/components/Icons/PdfIcon.vue | 20 + frontend/src/components/Icons/SMSIcon.vue | 16 + .../src/components/Icons/SupplierIcon.vue | 19 + .../src/components/Layouts/AppSidebar.vue | 122 ++- .../src/components/ListViews/BaseListView.vue | 225 ++++ .../ListViews/CustomersListView.vue | 55 + .../ListViews/SuppliersListView.vue | 55 + .../src/components/Mobile/MobileHeader.vue | 15 + .../src/components/Modals/CommentModal.vue | 172 ++++ .../components/Timeline/ActivityContent.vue | 21 + .../src/components/Timeline/EmailContent.vue | 111 ++ .../src/components/Timeline/TimelineGroup.vue | 28 + .../src/components/Timeline/TimelineItem.vue | 39 + .../src/components/Timeline/TimelineView.vue | 97 ++ frontend/src/pages/Contact.vue | 961 ++++++++---------- frontend/src/pages/Customer.vue | 400 ++++++++ frontend/src/pages/Customers.vue | 139 +++ frontend/src/pages/MobileCustomer.vue | 132 +++ frontend/src/pages/MobileSupplier.vue | 124 +++ frontend/src/pages/Supplier.vue | 392 +++++++ frontend/src/pages/Suppliers.vue | 167 +++ frontend/src/router.js | 24 + frontend/src/utils/erpnext.js | 22 + yarn.lock | 20 +- 34 files changed, 3817 insertions(+), 571 deletions(-) create mode 100644 crm/api/customer.py create mode 100644 crm/api/supplier.py create mode 100644 frontend/src/components/Contact/ContactHeader.vue create mode 100644 frontend/src/components/Contact/ContactProfile.vue create mode 100644 frontend/src/components/Contact/ContactTabs.vue create mode 100644 frontend/src/components/EmailDropZone.vue create mode 100644 frontend/src/components/Icons/CustomerIcon.vue create mode 100644 frontend/src/components/Icons/DealIcon.vue create mode 100644 frontend/src/components/Icons/ImageIcon.vue create mode 100644 frontend/src/components/Icons/PdfIcon.vue create mode 100644 frontend/src/components/Icons/SMSIcon.vue create mode 100644 frontend/src/components/Icons/SupplierIcon.vue create mode 100644 frontend/src/components/ListViews/BaseListView.vue create mode 100644 frontend/src/components/ListViews/CustomersListView.vue create mode 100644 frontend/src/components/ListViews/SuppliersListView.vue create mode 100644 frontend/src/components/Mobile/MobileHeader.vue create mode 100644 frontend/src/components/Modals/CommentModal.vue create mode 100644 frontend/src/components/Timeline/ActivityContent.vue create mode 100644 frontend/src/components/Timeline/EmailContent.vue create mode 100644 frontend/src/components/Timeline/TimelineGroup.vue create mode 100644 frontend/src/components/Timeline/TimelineItem.vue create mode 100644 frontend/src/components/Timeline/TimelineView.vue create mode 100644 frontend/src/pages/Customer.vue create mode 100644 frontend/src/pages/Customers.vue create mode 100644 frontend/src/pages/MobileCustomer.vue create mode 100644 frontend/src/pages/MobileSupplier.vue create mode 100644 frontend/src/pages/Supplier.vue create mode 100644 frontend/src/pages/Suppliers.vue create mode 100644 frontend/src/utils/erpnext.js diff --git a/crm/api/customer.py b/crm/api/customer.py new file mode 100644 index 000000000..111566a46 --- /dev/null +++ b/crm/api/customer.py @@ -0,0 +1,87 @@ +import frappe +from frappe import _ + +@frappe.whitelist() +def get_customer_sections(customer): + """Get sections for customer information""" + doc = frappe.get_doc("Customer", customer) + + basic_info = { + "title": "Basic Info", + "fields": [ + { + "label": "Customer Name", + "value": doc.customer_name, + "type": "Data" + }, + { + "label": "Customer Group", + "value": doc.customer_group, + "type": "Link" + }, + { + "label": "Customer Type", + "value": doc.customer_type, + "type": "Select" + }, + { + "label": "Territory", + "value": doc.territory, + "type": "Link" + } + ] + } + + contact_info = { + "title": "Contact Info", + "fields": [ + { + "label": "Email", + "value": doc.email_id, + "type": "Data" + }, + { + "label": "Phone", + "value": doc.phone, + "type": "Data" + }, + { + "label": "Mobile", + "value": doc.mobile_no, + "type": "Data" + }, + { + "label": "Website", + "value": doc.website, + "type": "Data" + } + ] + } + + financial_info = { + "title": "Financial Info", + "fields": [ + { + "label": "Tax ID", + "value": doc.tax_id, + "type": "Data" + }, + { + "label": "Default Currency", + "value": doc.default_currency, + "type": "Link" + }, + { + "label": "Credit Limit", + "value": doc.credit_limit, + "type": "Currency" + }, + { + "label": "Payment Terms", + "value": doc.payment_terms, + "type": "Link" + } + ] + } + + return [basic_info, contact_info, financial_info] \ No newline at end of file diff --git a/crm/api/doc.py b/crm/api/doc.py index ad2cf99ce..ad1715bd1 100644 --- a/crm/api/doc.py +++ b/crm/api/doc.py @@ -1,11 +1,16 @@ import json +import email +from email import policy +from email.parser import BytesParser import frappe from frappe import _ from frappe.model import no_value_fields from frappe.model.document import get_controller from frappe.utils import make_filter_tuple +from frappe.utils.file_manager import save_file from pypika import Criterion +import email.utils from crm.api.views import get_views from crm.fcrm.doctype.crm_form_script.crm_form_script import get_form_script @@ -205,6 +210,74 @@ def get_quick_filters(doctype: str): return quick_filters +def is_erpnext_doctype(doctype): + return doctype in ['Customer', 'Supplier'] + + +def get_erpnext_enabled(): + try: + return frappe.db.get_single_value('ERPNext CRM Settings', 'enabled') + except: + return False + + +def format_erpnext_data(data, doctype): + formatted_data = [] + for d in data: + if doctype == 'Customer': + formatted_doc = { + 'name': d.get('name'), + 'customer_name': { + 'name': d.get('name'), + 'label': d.get('customer_name') or d.get('name'), + 'logo': d.get('image') + }, + 'customer_group': { + 'name': d.get('customer_group'), + 'label': d.get('customer_group') + }, + 'territory': { + 'name': d.get('territory'), + 'label': d.get('territory') + }, + 'customer_type': { + 'name': d.get('customer_type'), + 'label': d.get('customer_type') + }, + 'modified': { + 'label': str(d.get('modified')), + 'timeAgo': str(d.get('modified')) + } + } + elif doctype == 'Supplier': + formatted_doc = { + 'name': d.get('name'), + 'supplier_name': { + 'name': d.get('name'), + 'label': d.get('supplier_name') or d.get('name'), + 'logo': d.get('image') + }, + 'supplier_group': { + 'name': d.get('supplier_group'), + 'label': d.get('supplier_group') + }, + 'supplier_type': { + 'name': d.get('supplier_type'), + 'label': d.get('supplier_type') + }, + 'country': { + 'name': d.get('country'), + 'label': d.get('country') + }, + 'modified': { + 'label': str(d.get('modified')), + 'timeAgo': str(d.get('modified')) + } + } + formatted_data.append(formatted_doc) + return formatted_data + + @frappe.whitelist() def get_data( doctype: str, @@ -220,7 +293,45 @@ def get_data( kanban_fields=[], view=None, default_filters=None, -): +): + if is_erpnext_doctype(doctype) and get_erpnext_enabled(): + # Get default data first to know what fields to fetch + default_data = get_erpnext_default_list_data(doctype) + fields_to_fetch = default_data.get("rows", []) + fields_to_fetch.extend(["image", "name"]) # Always fetch image and name + + # Get data from ERPNext with all needed fields + data = frappe.get_list( + doctype, + fields=fields_to_fetch, + filters=filters, + order_by=order_by or "modified desc", + page_length=page_length, + as_list=False + ) + + # Format data + formatted_data = format_erpnext_data(data, doctype) + + # Get columns definition + column_list = default_data.get("columns", []) + row_list = default_data.get("rows", []) + return { + "data": formatted_data, + "columns": column_list, + "rows": row_list, + "fields": get_fields_meta(doctype, as_array=True), + "total_count": len(frappe.get_all(doctype, filters=filters)), + "row_count": len(data), + "views": [], + "form_script": None, + "list_script": None, + "view_type": "list", + "page_length": page_length, + "page_length_count": page_length_count + } + + # Original implementation for non-ERPNext doctypes custom_view = False filters = frappe._dict(filters) rows = frappe.parse_json(rows or "[]") @@ -285,7 +396,12 @@ def get_data( is_default = False elif not custom_view or (is_default and hasattr(_list, "default_list_data")): rows = default_rows - columns = _list.default_list_data().get("columns") + if doctype in ["Customer", "Supplier"]: + default_data = get_erpnext_default_list_data(doctype) + if default_data: + columns = default_data.get("columns") + else: + columns = _list.default_list_data().get("columns") # check if rows has all keys from columns if not add them for column in columns: @@ -645,3 +761,226 @@ def getCounts(d, doctype): "FCRM Note", filters={"reference_doctype": doctype, "reference_docname": d.get("name")} ) return d + + +def _get_default_column(label, fieldtype, key, width): + return { + "label": label, + "type": fieldtype, + "key": key, + "width": width + } + +def get_erpnext_default_list_data(doctype): + """Get default list data configuration for ERPNext doctypes""" + if doctype == 'Customer': + return { + 'columns': [ + _get_default_column('Customer', 'Data', 'customer_name', '16rem'), + _get_default_column('Customer Group', 'Link', 'customer_group', '14rem'), + _get_default_column('Territory', 'Link', 'territory', '14rem'), + _get_default_column('Customer Type', 'Data', 'customer_type', '14rem'), + _get_default_column('Last Modified', 'Datetime', 'modified', '8rem') + ], + 'rows': [ + 'name', 'customer_name', 'customer_group', + 'territory', 'customer_type', 'modified' + ] + } + elif doctype == 'Supplier': + return { + 'columns': [ + _get_default_column('Supplier', 'Data', 'supplier_name', '16rem'), + _get_default_column('Supplier Group', 'Link', 'supplier_group', '14rem'), + _get_default_column('Supplier Type', 'Data', 'supplier_type', '14rem'), + _get_default_column('Country', 'Link', 'country', '14rem'), + _get_default_column('Last Modified', 'Datetime', 'modified', '8rem') + ], + 'rows': [ + 'name', 'supplier_name', 'supplier_group', + 'supplier_type', 'country', 'modified' + ] + } + return None + +@frappe.whitelist() +def get_timeline(doctype: str, name: str, for_contact: bool = False) -> list: + """Get timeline entries for a doctype""" + contacts = [frappe.get_doc("Contact", name)] if for_contact else get_contacts(doctype, name) + return _get_timeline(doctype, name, for_contact, contacts) + +@frappe.whitelist() +def get_contacts_count(doctype: str, name: str) -> int: + """Get count of contacts linked to a doctype""" + return frappe.db.count("Contact", filters=[ + ["Dynamic Link", "link_doctype", "=", doctype], + ["Dynamic Link", "link_name", "=", name] + ]) + +@frappe.whitelist() +def get_contacts(reference_doctype: str, reference_name: str) -> list: + """Get list of contacts linked to a reference doctype""" + return frappe.get_list( + "Contact", + filters=[ + ["Dynamic Link", "link_doctype", "=", reference_doctype], + ["Dynamic Link", "link_name", "=", reference_name] + ], + fields=["name", "email_id", "phone", "modified"] + ) + +@frappe.whitelist() +def create_communication_from_eml(file_url: str, contact_name: str) -> dict: + """Create a Communication from an uploaded .eml file.""" + try: + file = frappe.get_doc("File", {"file_url": file_url}) + file_path = frappe.get_site_path("public", file.file_url.lstrip("/")) + + with open(file_path, "rb") as f: + msg = BytesParser(policy=policy.default).parse(f) + + # Create Communication + comm = frappe.new_doc("Communication") + comm.update({ + "subject": msg["subject"], + "communication_medium": "Email", + "content": msg.get_body(preferencelist=("plain", "html")).get_content(), + "sender": msg["from"], + "recipients": msg["to"], + "communication_date": email.utils.parsedate_to_datetime(msg["date"]).replace(tzinfo=None), + "reference_doctype": "Contact", + "reference_name": contact_name, + "timeline_links": [{ + "link_doctype": "Contact", + "link_name": contact_name + }] + }) + comm.insert(ignore_permissions=True) + + # Handle attachments + for part in msg.iter_attachments(): + if file_name := part.get_filename(): + save_file( + file_name, + part.get_payload(decode=True), + "Communication", + comm.name, + "Home/Attachments" + ) + + return {"success": True, "communication": comm.name} + except Exception as e: + frappe.log_error(f"Failed to create communication from EML: {str(e)}") + return {"success": False, "error": str(e)} + +def _get_timeline(reference_doctype, reference_name, for_contact, contacts = None): + """Base timeline function that can be reused across doctypes""" + timeline = [] + if contacts: + _add_contact_entries(timeline, contacts) + + if not for_contact: + _add_reference_entries(timeline, reference_doctype, reference_name) + + timeline.sort(key=lambda x: x["timestamp"], reverse=True) + return timeline + +def _get_comments(reference_doctype, reference_name): + return frappe.get_list( + 'Comment', + filters={ + 'reference_doctype': reference_doctype, + 'reference_name': reference_name, + 'comment_type': 'Comment' + }, + fields=['name', 'content', 'comment_by', 'creation', 'owner'], + order_by='creation desc' + ) + +def _get_emails(reference_doctype, reference_name): + return frappe.get_list( + "Communication", + filters={ + 'reference_doctype': reference_doctype, + 'reference_name': reference_name + }, + fields=[ + "name", "subject", "content", "communication_type", + "communication_medium", "creation", "owner", "sender", + "sender_full_name", "recipients", "delivery_status", "communication_date" + ], + order_by="creation desc" + ) + +def _get_notes(reference_doctype, reference_name): + return frappe.get_list( + "FCRM Note", + fields=["name", "title", "content", "owner", "modified"], + filters={ + "reference_doctype": reference_doctype, + "reference_docname": reference_name + }, + order_by="modified desc" + ) + +def _add_reference_entries(timeline, reference_doctype, reference_name): + _add_comments(timeline, reference_doctype, reference_name) + _add_emails(timeline, reference_doctype, reference_name) + _add_notes(timeline, reference_doctype, reference_name) + +def _add_contact_entries(timeline, contacts): + for contact in contacts: + _add_reference_entries(timeline, "Contact", contact.name) + +def _add_comments(timeline, reference_doctype, reference_name): + for comment in _get_comments(reference_doctype, reference_name): + if reference_doctype == "Contact": + title = "Comment" + else: + title = "Comment regarding " + reference_name + + timeline.append({ + "type": "comment", + "title": title, + "description": comment.content, + "timestamp": comment.creation, + "owner": comment.owner, + "reference": { + "type": "Comment", + "name": comment.name + } + }) + +def _add_emails(timeline, reference_doctype, reference_name): + for email in _get_emails(reference_doctype, reference_name): + timeline.append({ + "type": "email", + "title": email.subject, + "description": email.content, + "reference": { + "type": "Communication", + "name": email.name + }, + "owner": email.sender, + "timestamp": email.communication_date, + "details": { + "sender": email.sender, + "sender_name": email.sender_full_name, + "recipients": email.recipients, + "contact": reference_name + } + }) + +def _add_notes(timeline, reference_doctype, reference_name): + for note in _get_notes(reference_doctype, reference_name): + timeline.append({ + "type": "note", + "title": note.title, + "description": note.content, + "reference": { + "type": "Note", + "name": note.name + }, + "owner": note.owner, + "timestamp": note.modified + }) \ No newline at end of file diff --git a/crm/api/supplier.py b/crm/api/supplier.py new file mode 100644 index 000000000..a23db4518 --- /dev/null +++ b/crm/api/supplier.py @@ -0,0 +1,77 @@ +import frappe +from frappe import _ + +@frappe.whitelist() +def get_supplier_sections(supplier): + """Get sections for supplier information""" + doc = frappe.get_doc("Supplier", supplier) + + basic_info = { + "title": "Basic Info", + "fields": [ + { + "label": "Supplier Name", + "value": doc.supplier_name, + "type": "Data" + }, + { + "label": "Supplier Group", + "value": doc.supplier_group, + "type": "Link" + }, + { + "label": "Supplier Type", + "value": doc.supplier_type, + "type": "Select" + } + ] + } + + contact_info = { + "title": "Contact Info", + "fields": [ + { + "label": "Email", + "value": doc.email_id, + "type": "Data" + }, + { + "label": "Phone", + "value": doc.phone, + "type": "Data" + }, + { + "label": "Mobile", + "value": doc.mobile_no, + "type": "Data" + }, + { + "label": "Website", + "value": doc.website, + "type": "Data" + } + ] + } + + financial_info = { + "title": "Financial Info", + "fields": [ + { + "label": "Tax ID", + "value": doc.tax_id, + "type": "Data" + }, + { + "label": "Default Currency", + "value": doc.default_currency, + "type": "Link" + }, + { + "label": "Payment Terms", + "value": doc.payment_terms, + "type": "Link" + } + ] + } + + return [basic_info, contact_info, financial_info] \ No newline at end of file diff --git a/frontend/src/components/Contact/ContactHeader.vue b/frontend/src/components/Contact/ContactHeader.vue new file mode 100644 index 000000000..ab3b0ab20 --- /dev/null +++ b/frontend/src/components/Contact/ContactHeader.vue @@ -0,0 +1,54 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/Contact/ContactProfile.vue b/frontend/src/components/Contact/ContactProfile.vue new file mode 100644 index 000000000..635e98477 --- /dev/null +++ b/frontend/src/components/Contact/ContactProfile.vue @@ -0,0 +1,85 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/components/Contact/ContactTabs.vue b/frontend/src/components/Contact/ContactTabs.vue new file mode 100644 index 000000000..933fd4b59 --- /dev/null +++ b/frontend/src/components/Contact/ContactTabs.vue @@ -0,0 +1,172 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/components/EmailDropZone.vue b/frontend/src/components/EmailDropZone.vue new file mode 100644 index 000000000..e76ec4279 --- /dev/null +++ b/frontend/src/components/EmailDropZone.vue @@ -0,0 +1,140 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/components/Icons/CustomerIcon.vue b/frontend/src/components/Icons/CustomerIcon.vue new file mode 100644 index 000000000..18ebc705e --- /dev/null +++ b/frontend/src/components/Icons/CustomerIcon.vue @@ -0,0 +1,19 @@ + \ No newline at end of file diff --git a/frontend/src/components/Icons/DealIcon.vue b/frontend/src/components/Icons/DealIcon.vue new file mode 100644 index 000000000..875a064a3 --- /dev/null +++ b/frontend/src/components/Icons/DealIcon.vue @@ -0,0 +1,17 @@ + \ No newline at end of file diff --git a/frontend/src/components/Icons/ImageIcon.vue b/frontend/src/components/Icons/ImageIcon.vue new file mode 100644 index 000000000..2588fd415 --- /dev/null +++ b/frontend/src/components/Icons/ImageIcon.vue @@ -0,0 +1,18 @@ + \ No newline at end of file diff --git a/frontend/src/components/Icons/PdfIcon.vue b/frontend/src/components/Icons/PdfIcon.vue new file mode 100644 index 000000000..6134e442b --- /dev/null +++ b/frontend/src/components/Icons/PdfIcon.vue @@ -0,0 +1,20 @@ + \ No newline at end of file diff --git a/frontend/src/components/Icons/SMSIcon.vue b/frontend/src/components/Icons/SMSIcon.vue new file mode 100644 index 000000000..ce32d9013 --- /dev/null +++ b/frontend/src/components/Icons/SMSIcon.vue @@ -0,0 +1,16 @@ + \ No newline at end of file diff --git a/frontend/src/components/Icons/SupplierIcon.vue b/frontend/src/components/Icons/SupplierIcon.vue new file mode 100644 index 000000000..640bfcf36 --- /dev/null +++ b/frontend/src/components/Icons/SupplierIcon.vue @@ -0,0 +1,19 @@ + \ No newline at end of file diff --git a/frontend/src/components/Layouts/AppSidebar.vue b/frontend/src/components/Layouts/AppSidebar.vue index d4906834b..8149a207b 100644 --- a/frontend/src/components/Layouts/AppSidebar.vue +++ b/frontend/src/components/Layouts/AppSidebar.vue @@ -117,55 +117,85 @@ import { } from '@/stores/notifications' import { FeatherIcon } from 'frappe-ui' import { useStorage } from '@vueuse/core' -import { computed, h } from 'vue' +import { computed, h, ref, onMounted } from 'vue' +import CustomerIcon from '@/components/Icons/CustomerIcon.vue' +import SupplierIcon from '@/components/Icons/SupplierIcon.vue' +import { checkERPNextEnabled } from '@/utils/erpnext' const { getPinnedViews, getPublicViews } = viewsStore() const { toggle: toggleNotificationPanel } = notificationsStore() const isSidebarCollapsed = useStorage('isSidebarCollapsed', false) -const links = [ - { - label: 'Leads', - icon: LeadsIcon, - to: 'Leads', - }, - { - label: 'Deals', - icon: DealsIcon, - to: 'Deals', - }, - { - label: 'Contacts', - icon: ContactsIcon, - to: 'Contacts', - }, - { - label: 'Organizations', - icon: OrganizationsIcon, - to: 'Organizations', - }, - { - label: 'Notes', - icon: NoteIcon, - to: 'Notes', - }, - { - label: 'Tasks', - icon: TaskIcon, - to: 'Tasks', - }, - { - label: 'Call Logs', - icon: PhoneIcon, - to: 'Call Logs', - }, - { - label: 'Email Templates', - icon: Email2Icon, - to: 'Email Templates', - }, -] +const isERPNextEnabled = ref(false) + +onMounted(async () => { + isERPNextEnabled.value = await checkERPNextEnabled() +}) + +const links = computed(() => { + let baseLinks = [ + { + label: 'Leads', + icon: LeadsIcon, + to: 'Leads', + }, + { + label: 'Deals', + icon: DealsIcon, + to: 'Deals', + }, + { + label: 'Contacts', + icon: ContactsIcon, + to: 'Contacts', + }, + { + label: 'Organizations', + icon: OrganizationsIcon, + to: 'Organizations', + } + ] + + if (isERPNextEnabled.value) { + baseLinks.push( + { + label: 'Customers', + icon: CustomerIcon, + to: 'Customers', + }, + { + label: 'Suppliers', + icon: SupplierIcon, + to: 'Suppliers', + } + ) + } + + return [ + ...baseLinks, + { + label: 'Notes', + icon: NoteIcon, + to: 'Notes', + }, + { + label: 'Tasks', + icon: TaskIcon, + to: 'Tasks', + }, + { + label: 'Call Logs', + icon: PhoneIcon, + to: 'Call Logs', + }, + { + label: 'Email Templates', + icon: Email2Icon, + to: 'Email Templates', + }, + ] +}) const allViews = computed(() => { let _views = [ @@ -173,7 +203,7 @@ const allViews = computed(() => { name: 'All Views', hideLabel: true, opened: true, - views: links, + views: links.value, }, ] if (getPublicViews().length) { @@ -220,6 +250,10 @@ function getIcon(routeName, icon) { return ContactsIcon case 'Organizations': return OrganizationsIcon + case 'Customers': + return CustomerIcon + case 'Suppliers': + return SupplierIcon case 'Notes': return NoteIcon case 'Call Logs': diff --git a/frontend/src/components/ListViews/BaseListView.vue b/frontend/src/components/ListViews/BaseListView.vue new file mode 100644 index 000000000..73c644121 --- /dev/null +++ b/frontend/src/components/ListViews/BaseListView.vue @@ -0,0 +1,225 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/ListViews/CustomersListView.vue b/frontend/src/components/ListViews/CustomersListView.vue new file mode 100644 index 000000000..fe0f9209a --- /dev/null +++ b/frontend/src/components/ListViews/CustomersListView.vue @@ -0,0 +1,55 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/ListViews/SuppliersListView.vue b/frontend/src/components/ListViews/SuppliersListView.vue new file mode 100644 index 000000000..37b2b6c20 --- /dev/null +++ b/frontend/src/components/ListViews/SuppliersListView.vue @@ -0,0 +1,55 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/Mobile/MobileHeader.vue b/frontend/src/components/Mobile/MobileHeader.vue new file mode 100644 index 000000000..ae12529d9 --- /dev/null +++ b/frontend/src/components/Mobile/MobileHeader.vue @@ -0,0 +1,15 @@ + \ No newline at end of file diff --git a/frontend/src/components/Modals/CommentModal.vue b/frontend/src/components/Modals/CommentModal.vue new file mode 100644 index 000000000..cd34de431 --- /dev/null +++ b/frontend/src/components/Modals/CommentModal.vue @@ -0,0 +1,172 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/components/Timeline/ActivityContent.vue b/frontend/src/components/Timeline/ActivityContent.vue new file mode 100644 index 000000000..b96eabe0c --- /dev/null +++ b/frontend/src/components/Timeline/ActivityContent.vue @@ -0,0 +1,21 @@ + + + diff --git a/frontend/src/components/Timeline/EmailContent.vue b/frontend/src/components/Timeline/EmailContent.vue new file mode 100644 index 000000000..741686455 --- /dev/null +++ b/frontend/src/components/Timeline/EmailContent.vue @@ -0,0 +1,111 @@ + + + + + diff --git a/frontend/src/components/Timeline/TimelineGroup.vue b/frontend/src/components/Timeline/TimelineGroup.vue new file mode 100644 index 000000000..bdb762f6b --- /dev/null +++ b/frontend/src/components/Timeline/TimelineGroup.vue @@ -0,0 +1,28 @@ + + + diff --git a/frontend/src/components/Timeline/TimelineItem.vue b/frontend/src/components/Timeline/TimelineItem.vue new file mode 100644 index 000000000..efeaef23a --- /dev/null +++ b/frontend/src/components/Timeline/TimelineItem.vue @@ -0,0 +1,39 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/Timeline/TimelineView.vue b/frontend/src/components/Timeline/TimelineView.vue new file mode 100644 index 000000000..150987759 --- /dev/null +++ b/frontend/src/components/Timeline/TimelineView.vue @@ -0,0 +1,97 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/pages/Contact.vue b/frontend/src/pages/Contact.vue index 92f919a1e..8b3eef0d1 100644 --- a/frontend/src/pages/Contact.vue +++ b/frontend/src/pages/Contact.vue @@ -1,185 +1,56 @@ + + diff --git a/frontend/src/pages/Customer.vue b/frontend/src/pages/Customer.vue new file mode 100644 index 000000000..c78c1402d --- /dev/null +++ b/frontend/src/pages/Customer.vue @@ -0,0 +1,400 @@ + + + \ No newline at end of file diff --git a/frontend/src/pages/Customers.vue b/frontend/src/pages/Customers.vue new file mode 100644 index 000000000..d001526b6 --- /dev/null +++ b/frontend/src/pages/Customers.vue @@ -0,0 +1,139 @@ + + \ No newline at end of file diff --git a/frontend/src/pages/MobileCustomer.vue b/frontend/src/pages/MobileCustomer.vue new file mode 100644 index 000000000..9e847ea2f --- /dev/null +++ b/frontend/src/pages/MobileCustomer.vue @@ -0,0 +1,132 @@ + + + \ No newline at end of file diff --git a/frontend/src/pages/MobileSupplier.vue b/frontend/src/pages/MobileSupplier.vue new file mode 100644 index 000000000..c88e97941 --- /dev/null +++ b/frontend/src/pages/MobileSupplier.vue @@ -0,0 +1,124 @@ + + + \ No newline at end of file diff --git a/frontend/src/pages/Supplier.vue b/frontend/src/pages/Supplier.vue new file mode 100644 index 000000000..0a241a6cc --- /dev/null +++ b/frontend/src/pages/Supplier.vue @@ -0,0 +1,392 @@ + + + \ No newline at end of file diff --git a/frontend/src/pages/Suppliers.vue b/frontend/src/pages/Suppliers.vue new file mode 100644 index 000000000..10039c6da --- /dev/null +++ b/frontend/src/pages/Suppliers.vue @@ -0,0 +1,167 @@ + + \ No newline at end of file diff --git a/frontend/src/router.js b/frontend/src/router.js index 7ce0f1105..13bb27e9e 100644 --- a/frontend/src/router.js +++ b/frontend/src/router.js @@ -91,6 +91,30 @@ const routes = [ component: () => import('@/pages/EmailTemplate.vue'), props: true, }, + { + alias: '/customers', + path: '/customers/view/:viewType?', + name: 'Customers', + component: () => import('@/pages/Customers.vue'), + }, + { + path: '/customers/:customerId', + name: 'Customer', + component: () => import(`@/pages/${handleMobileView('Customer')}.vue`), + props: true, + }, + { + alias: '/suppliers', + path: '/suppliers/view/:viewType?', + name: 'Suppliers', + component: () => import('@/pages/Suppliers.vue'), + }, + { + path: '/suppliers/:supplierId', + name: 'Supplier', + component: () => import(`@/pages/${handleMobileView('Supplier')}.vue`), + props: true, + }, { path: '/:invalidpath', name: 'Invalid Page', diff --git a/frontend/src/utils/erpnext.js b/frontend/src/utils/erpnext.js new file mode 100644 index 000000000..3f60e9fac --- /dev/null +++ b/frontend/src/utils/erpnext.js @@ -0,0 +1,22 @@ +import { call } from 'frappe-ui' + +let isERPNextEnabled = null + +export async function checkERPNextEnabled() { + if (isERPNextEnabled === null) { + try { + const result = await call('frappe.client.get_single_value', { + doctype: 'ERPNext CRM Settings', + field: 'enabled' + }) + isERPNextEnabled = !!result + } catch (error) { + isERPNextEnabled = false + } + } + return isERPNextEnabled +} + +export function resetERPNextCheck() { + isERPNextEnabled = null +} diff --git a/yarn.lock b/yarn.lock index 7b7baaa4d..3bc5732b2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3939,7 +3939,16 @@ sourcemap-codec@^1.4.8: resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4" integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA== -"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0: +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^4.1.0: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -4023,7 +4032,14 @@ stringify-object@^3.3.0: is-obj "^1.0.1" is-regexp "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==