Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions crm/api/report.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import frappe
from frappe.desk.query_report import run

@frappe.whitelist(allow_guest=True)
def get_report(report_name, filters=None):
print("Fetching report data...")
if isinstance(filters, str):
import json
filters = json.loads(filters)

# Run the report
try:
report_output = run(report_name, filters or {})
return {
"columns": report_output.get("columns"),
"data": report_output.get("result")
}
except Exception as e:
frappe.log_error(frappe.get_traceback(), "Error fetching report data")
return {"error": str(e)}
19 changes: 19 additions & 0 deletions crm/fcrm/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@

import frappe
from frappe import _

@frappe.whitelist(allow_guest=True)
def get_fcrm_reports():
"""Fetch all standard and custom reports for FCRM module (Redsoft CRM)."""
reports = frappe.get_all(
"Report",
filters={"module": "Redsoft CRM"},
fields=["name", "ref_doctype", "report_type", "is_standard", "modified"]
)

return {
"status": "success",
"module": "Redsoft CRM",
"count": len(reports),
"data": reports
}
12 changes: 11 additions & 1 deletion crm/fcrm/doctype/crm_lead/crm_lead.json
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@
"image_field": "image",
"index_web_pages_for_search": 1,
"links": [],
"modified": "2025-01-02 22:14:01.991054",
"modified": "2025-04-21 11:56:49.339554",
"modified_by": "Administrator",
"module": "FCRM",
"name": "CRM Lead",
Expand Down Expand Up @@ -329,8 +329,18 @@
"report": 1,
"role": "All",
"share": 1
},
{
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Guest",
"share": 1
}
],
"row_format": "Dynamic",
"sender_field": "email",
"sender_name_field": "first_name",
"show_title_field_in_link": 1,
Expand Down
2 changes: 2 additions & 0 deletions frontend/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,8 @@ declare module 'vue' {
ReloadIcon: typeof import('./src/components/Icons/ReloadIcon.vue')['default']
ReplyAllIcon: typeof import('./src/components/Icons/ReplyAllIcon.vue')['default']
ReplyIcon: typeof import('./src/components/Icons/ReplyIcon.vue')['default']
ReportDetailModal: typeof import('./src/components/Modals/ReportDetailModal.vue')['default']
ReportsListView: typeof import('./src/components/ListViews/ReportsListView.vue')['default']
Resizer: typeof import('./src/components/Resizer.vue')['default']
RightSideLayoutIcon: typeof import('./src/components/Icons/RightSideLayoutIcon.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
Expand Down
32 changes: 20 additions & 12 deletions frontend/src/components/Layouts/AppSidebar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ import SidebarLink from '@/components/SidebarLink.vue'
import Notifications from '@/components/Notifications.vue'
import Settings from '@/components/Settings/Settings.vue'
import { viewsStore } from '@/stores/views'
import DashboardIcon from '@/components/Icons/DashboardIcon.vue'
import {
unreadNotificationsCount,
notificationsStore,
Expand Down Expand Up @@ -195,14 +196,14 @@ const isDemoSite = ref(window.is_demo_site)

const links = [
{
label: 'Leads',
label: 'Patients',
icon: LeadsIcon,
to: 'Leads',
to: 'Patients',
},
{
label: 'Deals',
label: 'Bookings',
icon: DealsIcon,
to: 'Deals',
to: 'Bookings',
},
{
label: 'Contacts',
Expand Down Expand Up @@ -234,6 +235,11 @@ const links = [
icon: Email2Icon,
to: 'Email Templates',
},
{
label: 'Reports',
icon: DashboardIcon,
to: 'Reports',
},
]

const allViews = computed(() => {
Expand Down Expand Up @@ -281,9 +287,9 @@ function getIcon(routeName, icon) {
if (icon) return h('div', { class: 'size-auto' }, icon)

switch (routeName) {
case 'Leads':
case 'Patients':
return LeadsIcon
case 'Deals':
case 'Bookings':
return DealsIcon
case 'Contacts':
return ContactsIcon
Expand All @@ -292,7 +298,9 @@ function getIcon(routeName, icon) {
case 'Notes':
return NoteIcon
case 'Call Logs':
return PhoneIcon
return
case 'Dashboard':
return DashboardIcon
default:
return PinIcon
}
Expand Down Expand Up @@ -324,7 +332,7 @@ const steps = reactive([
completed: false,
onClick: () => {
minimize.value = true
router.push({ name: 'Leads' })
router.push({ name: 'Patients' })
},
},
{
Expand Down Expand Up @@ -358,7 +366,7 @@ const steps = reactive([
if (lead) {
router.push({ name: 'Lead', params: { leadId: lead } })
} else {
router.push({ name: 'Leads' })
router.push({ name: 'Patients' })
}
},
}
Expand Down Expand Up @@ -421,7 +429,7 @@ const steps = reactive([
hash: '#comments',
})
} else {
router.push({ name: 'Leads' })
router.push({ name: 'Patients' })
}
},
},
Expand All @@ -441,7 +449,7 @@ const steps = reactive([
hash: '#emails',
})
} else {
router.push({ name: 'Leads' })
router.push({ name: 'Patients' })
}
},
},
Expand Down Expand Up @@ -469,7 +477,7 @@ const steps = reactive([
hash: '#activity',
})
} else {
router.push({ name: 'Leads' })
router.push({ name: 'Patients' })
}
},
}
Expand Down
207 changes: 207 additions & 0 deletions frontend/src/components/ListViews/ReportsListView.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
<template>
<ListView
:class="$attrs.class"
:columns="columns"
:rows="rows"
:options="{
getRowRoute: (row) => ({
name: 'Report',
params: { reportName: row.name },
query: { view: route.query.view, viewType: route.params.viewType },
}),
selectable: options.selectable,
showTooltip: options.showTooltip,
resizeColumn: options.resizeColumn,
rowCount: options.rowCount,
totalCount: options.totalCount,
}"
row-key="name"
>
<ListHeader
class="mx-3 sm:mx-5"
@columnWidthUpdated="emit('columnWidthUpdated')"
>
<ListHeaderItem
v-for="column in columns"
:key="column.key"
:item="column"
@columnWidthUpdated="emit('columnWidthUpdated', column)"
>
<Button
v-if="column.key === '_liked_by'"
variant="ghosted"
class="!h-4"
:class="isLikeFilterApplied ? 'fill-red-500' : 'fill-white'"
@click="() => emit('applyLikeFilter')"
>
<HeartIcon class="h-4 w-4" />
</Button>
</ListHeaderItem>
</ListHeader>

<ListRows
class="mx-3 sm:mx-5"
:rows="rows"
v-slot="{ idx, column, item, row }"
doctype="Report"
>
<ListRowItem :item="item" :align="column.align">
<template #default="{ label }">
<div
v-if="['modified', 'creation'].includes(column.key)"
class="truncate text-base"
@click="(event) =>
emit('applyFilter', {
event,
idx,
column,
item,
firstColumn: columns[0],
})"
>
<Tooltip :text="item.label">
<div>{{ item.timeAgo }}</div>
</Tooltip>
</div>
<div v-else-if="column.type === 'Check'">
<FormControl
type="checkbox"
:modelValue="item"
:disabled="true"
class="text-ink-gray-9"
/>
</div>
<div v-else-if="column.key === '_liked_by'">
<Button
variant="ghosted"
:class="isLiked(item) ? 'fill-red-500' : 'fill-white'"
@click.stop.prevent="
() =>
emit('likeDoc', { name: row.name, liked: isLiked(item) })
"
>
<HeartIcon class="h-4 w-4" />
</Button>
</div>
<div
v-else
class="truncate text-base"
@click="(event) =>
emit('applyFilter', {
event,
idx,
column,
item,
firstColumn: columns[0],
})"
>
{{ label }}
</div>
</template>
</ListRowItem>
</ListRows>

<ListSelectBanner>
<template #actions="{ selections, unselectAll }">
<Dropdown
:options="listBulkActionsRef.bulkActions(selections, unselectAll)"
>
<Button icon="more-horizontal" variant="ghost" />
</Dropdown>
</template>
</ListSelectBanner>
</ListView>

<ListFooter
v-if="pageLengthCount"
class="border-t px-3 py-2 sm:px-5"
v-model="pageLengthCount"
:options="{
rowCount: options.rowCount,
totalCount: options.totalCount,
}"
@loadMore="emit('loadMore')"
/>

<ListBulkActions
ref="listBulkActionsRef"
v-model="list"
doctype="Report"
:options="{ hideAssign: true }"
/>
</template>

<script setup>
import HeartIcon from '@/components/Icons/HeartIcon.vue'
import ListBulkActions from '@/components/ListBulkActions.vue'
import ListRows from '@/components/ListViews/ListRows.vue'
import {
ListView,
ListHeader,
ListHeaderItem,
ListSelectBanner,
ListRowItem,
ListFooter,
Tooltip,
Dropdown,
FormControl,
Button,
} from 'frappe-ui'
import { sessionStore } from '@/stores/session'
import { ref, computed, watch } from 'vue'
import { useRoute } from 'vue-router'

const props = defineProps({
rows: { type: Array, required: true },
columns: { type: Array, required: true },
options: {
type: Object,
default: () => ({
selectable: true,
showTooltip: true,
resizeColumn: false,
rowCount: 0,
totalCount: 0,
}),
},
})

const emit = defineEmits([
'loadMore',
'updatePageCount',
'columnWidthUpdated',
'applyFilter',
'applyLikeFilter',
'likeDoc',
])

const route = useRoute()

const pageLengthCount = defineModel()
const list = defineModel('list')
const listBulkActionsRef = ref(null)

const { user } = sessionStore()

const isLikeFilterApplied = computed(() => {
return list.value?.params?.filters?._liked_by ? true : false
})

function isLiked(item) {
if (item) {
let likedByMe = JSON.parse(item)
return likedByMe.includes(user)
}
}

watch(pageLengthCount, (val, old_value) => {
if (val === old_value) return
emit('updatePageCount', val)
})

defineExpose({
customListActions: computed(
() => listBulkActionsRef.value?.customListActions,
),
})
</script>
Loading