first commit

This commit is contained in:
snehalathad@aissel.com 2024-09-02 10:28:52 +05:30
parent 55039e2613
commit 98646422e9
86 changed files with 4923 additions and 0 deletions

5
analystview/.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
node_modules
.DS_Store
dist
dist-ssr
*.local

View File

@ -0,0 +1,4 @@
{
"semi": false,
"singleQuote": true
}

50
analystview/README.md Normal file
View File

@ -0,0 +1,50 @@
# Frappe UI Starter
This template should help get you started developing custom frontend for Frappe
apps with Vue 3 and the Frappe UI package.
![Auth](https://user-images.githubusercontent.com/34810212/236846289-ac31c292-81ea-4456-be65-95773a4049be.png)
![Home](https://user-images.githubusercontent.com/34810212/236846299-fd534e2b-1c06-4f01-a4f2-91a27547cd55.png)
This boilerplate sets up Vue 3, Vue Router, TailwindCSS, and Frappe UI out of
the box. It also has basic authentication frontend.
## Docs
[Frappe UI Website](https://frappeui.com)
## Usage
This template is meant to be cloned inside an existing Frappe App. Assuming your
apps name is `todo`. Clone this template in the root folder of your app using `degit`.
```
cd apps/todo
npx degit NagariaHussain/doppio_frappeui_starter frontend
cd frontend
yarn
yarn dev
```
In a development environment, you need to put the below key-value pair in your `site_config.json` file:
```
"ignore_csrf": 1
```
This will prevent `CSRFToken` errors while using the vite dev server. In production environment, the `csrf_token` is attached to the `window` object in `index.html` for you.
The Vite dev server will start on the port `8080`. This can be changed from `vite.config.js`.
The development server is configured to proxy your frappe app (usually running on port `8000`). If you have a site named `todo.test`, open `http://todo.test:8080` in your browser. If you see a button named "Click to send 'ping' request", congratulations!
If you notice the browser URL is `/frontend`, this is the base URL where your frontend app will run in production.
To change this, open `src/router.js` and change the base URL passed to `createWebHistory`.
## Resources
- [Vue 3](https://v3.vuejs.org/guide/introduction.html)
- [Vue Router](https://next.router.vuejs.org/guide/)
- [Frappe UI](https://github.com/frappe/frappe-ui)
- [TailwindCSS](https://tailwindcss.com/docs/utility-first)
- [Vite](https://vitejs.dev/guide/)

19
analystview/index.html Normal file
View File

@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Frappe UI App</title>
</head>
<body>
<div id="app"></div>
<div id="modals"></div>
<div id="popovers"></div>
<script>
window.csrf_token = '{{ frappe.session.csrf_token }}'
</script>
<script type="module" src="/src/main.js"></script>
</body>
</html>

24
analystview/package.json Normal file
View File

@ -0,0 +1,24 @@
{
"name": "frappe-ui-frontend",
"private": true,
"version": "0.0.0",
"scripts": {
"dev": "vite",
"build": "vite build --base=/assets/kolanalystapp/analystview/ && yarn copy-html-entry",
"preview": "vite preview",
"copy-html-entry": "cp ../kolanalystapp/public/analystview/index.html ../kolanalystapp/www/analystview.html"
},
"dependencies": {
"feather-icons": "^4.28.0",
"frappe-ui": "^0.1.62",
"vue": "^3.4.29",
"vue-router": "^4.0.12"
},
"devDependencies": {
"@vitejs/plugin-vue": "^2.0.0",
"autoprefixer": "^10.4.2",
"postcss": "^8.4.5",
"tailwindcss": "^3.0.15",
"vite": "^2.7.2"
}
}

View File

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 440 B

5
analystview/src/App.vue Normal file
View File

@ -0,0 +1,5 @@
<template>
<div>
<router-view />
</div>
</template>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,152 @@
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 100;
font-display: swap;
src: url('Inter-Thin.woff2?v=3.12') format('woff2'),
url('Inter-Thin.woff?v=3.12') format('woff');
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 100;
font-display: swap;
src: url('Inter-ThinItalic.woff2?v=3.12') format('woff2'),
url('Inter-ThinItalic.woff?v=3.12') format('woff');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 200;
font-display: swap;
src: url('Inter-ExtraLight.woff2?v=3.12') format('woff2'),
url('Inter-ExtraLight.woff?v=3.12') format('woff');
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 200;
font-display: swap;
src: url('Inter-ExtraLightItalic.woff2?v=3.12') format('woff2'),
url('Inter-ExtraLightItalic.woff?v=3.12') format('woff');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 300;
font-display: swap;
src: url('Inter-Light.woff2?v=3.12') format('woff2'),
url('Inter-Light.woff?v=3.12') format('woff');
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 300;
font-display: swap;
src: url('Inter-LightItalic.woff2?v=3.12') format('woff2'),
url('Inter-LightItalic.woff?v=3.12') format('woff');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('Inter-Regular.woff2?v=3.12') format('woff2'),
url('Inter-Regular.woff?v=3.12') format('woff');
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 400;
font-display: swap;
src: url('Inter-Italic.woff2?v=3.12') format('woff2'),
url('Inter-Italic.woff?v=3.12') format('woff');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url('Inter-Medium.woff2?v=3.12') format('woff2'),
url('Inter-Medium.woff?v=3.12') format('woff');
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 500;
font-display: swap;
src: url('Inter-MediumItalic.woff2?v=3.12') format('woff2'),
url('Inter-MediumItalic.woff?v=3.12') format('woff');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url('Inter-SemiBold.woff2?v=3.12') format('woff2'),
url('Inter-SemiBold.woff?v=3.12') format('woff');
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 600;
font-display: swap;
src: url('Inter-SemiBoldItalic.woff2?v=3.12') format('woff2'),
url('Inter-SemiBoldItalic.woff?v=3.12') format('woff');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('Inter-Bold.woff2?v=3.12') format('woff2'),
url('Inter-Bold.woff?v=3.12') format('woff');
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 700;
font-display: swap;
src: url('Inter-BoldItalic.woff2?v=3.12') format('woff2'),
url('Inter-BoldItalic.woff?v=3.12') format('woff');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 800;
font-display: swap;
src: url('Inter-ExtraBold.woff2?v=3.12') format('woff2'),
url('Inter-ExtraBold.woff?v=3.12') format('woff');
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 800;
font-display: swap;
src: url('Inter-ExtraBoldItalic.woff2?v=3.12') format('woff2'),
url('Inter-ExtraBoldItalic.woff?v=3.12') format('woff');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 900;
font-display: swap;
src: url('Inter-Black.woff2?v=3.12') format('woff2'),
url('Inter-Black.woff?v=3.12') format('woff');
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 900;
font-display: swap;
src: url('Inter-BlackItalic.woff2?v=3.12') format('woff2'),
url('Inter-BlackItalic.woff?v=3.12') format('woff');
}

View File

@ -0,0 +1,22 @@
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="54" height="54px" viewBox="0 0 100 54" style="enable-background:new 0 0 300 54;" xml:space="preserve">
<g>
<circle fill="#F96731" cx="60.7" cy="48.6" r="3.5"/>
<circle fill="#F96731" cx="60.7" cy="17.8" r="3.5"/>
<path fill="#2B9AF3" d="M57.3,44.8c-5-1.5-8.6-6.1-8.6-11.6c0-5.4,3.6-10.1,8.6-11.6c-1.1-1-1.7-2.3-1.7-3.9c0-1,0.3-1.9,0.7-2.7
c0,0,0,0,0,0c-8.1,2-14.2,9.4-14.2,18.1c0,8.7,6,16.1,14.2,18.1c0,0,0,0,0,0c-0.5-0.8-0.7-1.7-0.7-2.7
C55.5,47.1,56.2,45.7,57.3,44.8z"/>
<path fill="#2B9AF3" d="M65.2,15.1c0.5,0.8,0.8,1.7,0.8,2.7c0,1.5-0.7,2.9-1.7,3.9c5,1.5,8.6,6.1,8.6,11.6
c0,5.4-3.6,10.1-8.6,11.6c1.1,1,1.7,2.3,1.7,3.9c0,1-0.3,1.9-0.8,2.7c8.1-2,14.2-9.4,14.2-18.1C79.4,24.5,73.3,17.1,65.2,15.1z
"/>
</g>
<g>
<path fill="#FFFFFF" d="M290,8c0,3.4-2.7,6.1-6.1,6.1c-3.4,0-6.2-2.7-6.2-6.1c0-3.3,2.7-6,6.2-6C287.3,2,290,4.7,290,8z M279.2,8
c0,2.7,2,4.8,4.7,4.8c2.6,0,4.6-2.1,4.6-4.8c0-2.7-1.9-4.8-4.6-4.8C281.2,3.2,279.2,5.4,279.2,8z M282.9,11.1h-1.4v-6
C282,5,282.8,5,283.8,5c1.1,0,1.6,0.2,2.1,0.4c0.3,0.3,0.6,0.7,0.6,1.3c0,0.7-0.5,1.2-1.2,1.4v0.1c0.6,0.2,0.9,0.7,1.1,1.5
c0.2,0.9,0.3,1.3,0.4,1.5h-1.5c-0.2-0.2-0.3-0.8-0.5-1.5c-0.1-0.7-0.5-1-1.2-1h-0.7V11.1z M282.9,7.7h0.7c0.8,0,1.4-0.3,1.4-0.9
c0-0.5-0.4-0.9-1.3-0.9c-0.4,0-0.6,0-0.8,0.1V7.7z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1,125 @@
<template>
<div
class="relative flex h-full flex-col justify-between transition-all duration-300 ease-in-out"
:class="isSidebarCollapsed ? 'w-12' : 'w-[220px]'"
>
<div class="items-center">
<UserDropdown class="p-2" :isCollapsed=false />
</div>
<div class="flex-1 overflow-y-auto">
<div class="mb-3 flex flex-col">
</div>
<div v-for="view in allViews" :key="view.label">
<!-- <div
v-if="!view.hideLabel && isSidebarCollapsed && view.views?.length"
class="mx-2 my-2 h-1 border-b"
/> -->
<Section
:label="view.name"
:hideLabel=false
:isOpened=true
>
<template #header="{ opened, hide, toggle }">
<div
v-if="!hide"
class="flex cursor-pointer gap-1.5 px-1 text-base font-medium text-gray-600 transition-all duration-300 ease-in-out"
:class="
isSidebarCollapsed
? 'ml-0 h-0 overflow-hidden opacity-0'
: 'ml-2 mt-4 h-7 w-auto opacity-100'
"
@click="toggle()"
>
<FeatherIcon
name="chevron-right"
class="h-4 text-gray-900 transition-all duration-300 ease-in-out"
:class="{ 'rotate-90': opened }"
/>
<span>{{ __(view.name) }}</span>
</div>
</template>
</Section>
</div>
</div>
<div class="m-2 flex flex-col gap-1">
</div>
</div>
</template>
<script setup>
import { FeatherIcon } from 'frappe-ui'
import { useStorage } from '@vueuse/core'
import { computed, h } from 'vue'
import UserDropdown from './UserDropdown.vue';
import { session } from '../data/session'
// const { getPinnedViews, getPublicViews } = viewsStore()
// const { toggle: toggleNotificationPanel } = notificationsStore()
const isSidebarCollapsed = false
const links = [
{
label: 'OptView',
to: '/home/OptView',
}
]
const allViews = computed(() => {
let _views = [
{
name: 'All Views',
hideLabel: true,
opened: true,
views: links,
},
]
return _views
})
function parseView(views) {
return views.map((view) => {
return {
label: view.label,
/// icon: getIcon(view.route_name, view.icon),
to: {
name: view.route_name,
// params: { viewType: view.type || 'list' },
// query: { view: view.name },
},
}
})
}
// function getIcon(routeName, icon) {
// if (icon) return h('div', { class: 'size-auto' }, icon)
// switch (routeName) {
// case 'Leads':
// return LeadsIcon
// case 'Deals':
// return DealsIcon
// case 'Contacts':
// return ContactsIcon
// case 'Organizations':
// return OrganizationsIcon
// case 'Notes':
// return NoteIcon
// case 'Call Logs':
// return PhoneIcon
// default:
// return PinIcon
// }
// }
</script>

View File

View File

@ -0,0 +1,21 @@
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-list-collapse"
>
<path d="m3 10 2.5-2.5L3 5" />
<path d="m3 19 2.5-2.5L3 14" />
<path d="M10 6h11" />
<path d="M10 12h11" />
<path d="M10 18h11" />
</svg>
</template>

View File

@ -0,0 +1,17 @@
<template>
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M2.5 4.5C2.5 3.39543 3.39543 2.5 4.5 2.5H8.5C8.77614 2.5 9 2.27614 9 2C9 1.72386 8.77614 1.5 8.5 1.5H4.5C2.84315 1.5 1.5 2.84315 1.5 4.5V11.5C1.5 13.1569 2.84315 14.5 4.5 14.5H11.5C13.1569 14.5 14.5 13.1569 14.5 11.5V7.5C14.5 7.22386 14.2761 7 14 7C13.7239 7 13.5 7.22386 13.5 7.5V11.5C13.5 12.6046 12.6046 13.5 11.5 13.5H4.5C3.39543 13.5 2.5 12.6046 2.5 11.5V4.5ZM14.1255 2.58446C14.3207 2.3892 14.3207 2.07261 14.1255 1.87735C13.9302 1.68209 13.6136 1.68209 13.4184 1.87735L6.68616 8.60954C6.4909 8.8048 6.4909 9.12139 6.68616 9.31665C6.88143 9.51191 7.19801 9.51191 7.39327 9.31665L14.1255 2.58446Z"
fill="currentColor"
/>
</svg>
</template>

View File

@ -0,0 +1,28 @@
<template>
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="54" height="54px" viewBox="0 0 100 54" style="enable-background:new 0 0 300 54;" xml:space="preserve">
<g>
<circle fill="#F96731" cx="60.7" cy="48.6" r="3.5"/>
<circle fill="#F96731" cx="60.7" cy="17.8" r="3.5"/>
<path fill="#2B9AF3" d="M57.3,44.8c-5-1.5-8.6-6.1-8.6-11.6c0-5.4,3.6-10.1,8.6-11.6c-1.1-1-1.7-2.3-1.7-3.9c0-1,0.3-1.9,0.7-2.7
c0,0,0,0,0,0c-8.1,2-14.2,9.4-14.2,18.1c0,8.7,6,16.1,14.2,18.1c0,0,0,0,0,0c-0.5-0.8-0.7-1.7-0.7-2.7
C55.5,47.1,56.2,45.7,57.3,44.8z"/>
<path fill="#2B9AF3" d="M65.2,15.1c0.5,0.8,0.8,1.7,0.8,2.7c0,1.5-0.7,2.9-1.7,3.9c5,1.5,8.6,6.1,8.6,11.6
c0,5.4-3.6,10.1-8.6,11.6c1.1,1,1.7,2.3,1.7,3.9c0,1-0.3,1.9-0.8,2.7c8.1-2,14.2-9.4,14.2-18.1C79.4,24.5,73.3,17.1,65.2,15.1z
"/>
</g>
<g>
<path fill="#FFFFFF" d="M290,8c0,3.4-2.7,6.1-6.1,6.1c-3.4,0-6.2-2.7-6.2-6.1c0-3.3,2.7-6,6.2-6C287.3,2,290,4.7,290,8z M279.2,8
c0,2.7,2,4.8,4.7,4.8c2.6,0,4.6-2.1,4.6-4.8c0-2.7-1.9-4.8-4.6-4.8C281.2,3.2,279.2,5.4,279.2,8z M282.9,11.1h-1.4v-6
C282,5,282.8,5,283.8,5c1.1,0,1.6,0.2,2.1,0.4c0.3,0.3,0.6,0.7,0.6,1.3c0,0.7-0.5,1.2-1.2,1.4v0.1c0.6,0.2,0.9,0.7,1.1,1.5
c0.2,0.9,0.3,1.3,0.4,1.5h-1.5c-0.2-0.2-0.3-0.8-0.5-1.5c-0.1-0.7-0.5-1-1.2-1h-0.7V11.1z M282.9,7.7h0.7c0.8,0,1.4-0.3,1.4-0.9
c0-0.5-0.4-0.9-1.3-0.9c-0.4,0-0.6,0-0.8,0.1V7.7z"/>
</g>
</svg>
</template>

View File

@ -0,0 +1,17 @@
<template>
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M6.17683 1C4.89138 1 3.81543 1.97482 3.68896 3.25405L3.19111 8.2899C3.16577 8.54615 3.0749 8.79156 2.92723 9.00252L1.98134 10.3538C1.51739 11.0166 1.99155 11.9273 2.80057 11.9273H7.53349V14.4999C7.53349 14.7761 7.75735 14.9999 8.03349 14.9999C8.30963 14.9999 8.53349 14.7761 8.53349 14.4999V11.9273H13.2016C14.0107 11.9273 14.4848 11.0166 14.0209 10.3538L13.068 8.99254C12.9236 8.78634 12.8335 8.54713 12.8059 8.29695L12.246 3.22567C12.1062 1.95882 11.0357 1 9.76113 1H6.17683ZM4.68411 3.35243C4.75999 2.5849 5.40556 2 6.17683 2H9.76113C10.5259 2 11.1682 2.57529 11.2521 3.3354L11.8119 8.40668C11.858 8.82365 12.0082 9.22233 12.2488 9.566L13.2016 10.9273H2.80057L3.74646 9.57598C3.99257 9.22439 4.14403 8.81536 4.18625 8.38828L4.68411 3.35243Z"
fill="currentColor"
/>
</svg>
</template>

View File

@ -0,0 +1,19 @@
<template>
<svg width="16" height="16" fill="none" viewBox="0 0 16 16">
<g class="es-line-reload" clip-path="url(#a)">
<path
fill="currentColor"
fill-rule="evenodd"
d="M9.743 2.189a6 6 0 0 0-6.558 2.596.5.5 0 0 0 .844.535 5 5 0 0 1 9.12 1.683l-1.187-.686a.5.5 0 0 0-.5.866l2.165 1.25a.5.5 0 0 0 .683-.183l1.25-2.165a.5.5 0 0 0-.866-.5l-.603 1.044a6 6 0 0 0-4.348-4.44ZM3.356 9.024l1.189.687a.5.5 0 0 0 .5-.866L2.88 7.595a.5.5 0 0 0-.683.183L.947 9.943a.5.5 0 1 0 .866.5l.603-1.044a6 6 0 0 0 10.9 1.816.5.5 0 0 0-.844-.536 5 5 0 0 1-9.116-1.655Z"
class="Union"
clip-rule="evenodd"
/>
</g>
<defs>
<clipPath id="a" class="a">
<path fill="currentColor" d="M.25 0h16v16h-16z" />
</clipPath>
</defs>
</svg>
</template>

View File

@ -0,0 +1,26 @@
<template>
<svg
width="15"
height="14"
viewBox="0 0 15 14"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M5.1783 0C3.89284 0 2.81689 0.974825 2.69042 2.25405L2.19257 7.2899C2.16724 7.54615 2.07636 7.79156 1.92869 8.00252L0.982801 9.35379C0.518857 10.0166 0.99301 10.9273 1.80203 10.9273H6.53506V13.4998C6.53506 13.776 6.75892 13.9998 7.03506 13.9998C7.31121 13.9998 7.53506 13.776 7.53506 13.4998V10.9273H12.2031C13.0121 10.9273 13.4863 10.0166 13.0223 9.35379L12.0695 7.99254C11.9251 7.78634 11.835 7.54713 11.8074 7.29695L11.2475 2.22567C11.1076 0.958818 10.0371 0 8.76259 0H5.1783ZM3.68557 2.35243C3.76145 1.5849 4.40702 1 5.1783 1H8.76259C9.52732 1 10.1696 1.57529 10.2535 2.3354L10.8134 7.40668C10.8594 7.82365 11.0097 8.22233 11.2502 8.566L12.2031 9.92725H1.80203L2.74793 8.57598C2.99404 8.22439 3.1455 7.81536 3.18772 7.38828L3.68557 2.35243Z"
fill="currentColor"
/>
<rect
x="0.792893"
width="2"
height="17"
rx="1"
transform="rotate(-45 0.792893 0)"
fill="currentColor"
stroke="white"
/>
</svg>
</template>

View File

@ -0,0 +1,25 @@
<template>
<Teleport to="#app-header" v-if="showHeader">
<slot>
<header class="flex h-12 items-center justify-between py-2.5 pl-5">
<div class="flex items-center gap-2">
<slot name="left-header" />
</div>
<div class="flex items-center gap-2">
<slot name="right-header" class="flex items-center gap-2" />
</div>
</header>
</slot>
</Teleport>
</template>
<script setup>
import { ref, nextTick } from 'vue'
const showHeader = ref(false)
nextTick(() => {
showHeader.value = true
})
</script>

View File

@ -0,0 +1,63 @@
<template>
<FormControl
v-if="filter.type == 'Check'"
:label="filter.label"
type="checkbox"
v-model="filter.value"
@change.stop="updateFilter(filter, $event.target.checked)"
/>
<FormControl
v-else-if="filter.type === 'Select'"
class="form-control cursor-pointer [&_select]:cursor-pointer"
type="select"
v-model="filter.value"
:options="filter.options"
:placeholder="filter.label"
@change.stop="updateFilter(filter, $event.target.value)"
/>
<Link
v-else-if="filter.type === 'Link'"
:value="filter.value"
:doctype="filter.options"
:placeholder="filter.label"
@change="(data) => updateFilter(filter, data)"
/>
<component
v-else-if="['Date', 'Datetime'].includes(filter.type)"
class="border-none"
:is="filter.type === 'Date' ? DatePicker : DateTimePicker"
:value="filter.value"
@change="(v) => updateFilter(filter, v)"
:placeholder="filter.label"
/>
<TextInput
v-else
v-model="filter.value"
type="text"
:placeholder="filter.label"
@input.stop="debouncedFn(filter, $event.target.value)"
/>
</template>
<script setup>
import Link from '@/components/Controls/Link.vue'
import { TextInput, FormControl, DatePicker, DateTimePicker } from 'frappe-ui'
import { useDebounceFn } from '@vueuse/core'
const props = defineProps({
filter: {
type: Object,
required: true,
},
})
const emit = defineEmits(['applyQuickFilter'])
const debouncedFn = useDebounceFn((f, value) => {
emit('applyQuickFilter', f, value)
}, 500)
function updateFilter(f, value) {
emit('applyQuickFilter', f, value)
}
</script>

View File

View File

@ -0,0 +1,106 @@
<template>
<Dropdown :options="dropdownOptions" v-bind="$attrs" class="color:F96731">
<template v-slot="{ open }">
<button
class="flex h-12 items-center rounded-md py-2 duration-300 ease-in-out"
:class="
'w-52 bg-white px-2 shadow-sm'
"
>
<KonectarLogo class="size-8 flex-shrink-0 rounded" />
<div
class="flex flex-1 flex-col text-left duration-300 ease-in-out"
:class="
isCollapsed
? 'ml-0 w-0 overflow-hidden opacity-0'
: 'ml-2 w-auto opacity-100'
"
>
<div class="text-base font-medium leading-none text-gray-900">
Konectar
</div>
<div class="mt-1 text-sm leading-none text-gray-700">
{{ session.user }}
</div>
</div>
<div
class="duration-300 ease-in-out"
:class="
isCollapsed
? 'ml-0 w-0 overflow-hidden opacity-0'
: 'ml-2 w-auto opacity-100'
"
>
<FeatherIcon
name="chevron-down"
class="size-4 text-gray-600"
aria-hidden="true"
/>
</div>
</button>
</template>
</Dropdown>
</template>
<script setup>
import { session } from '../data/session'
import Dropdown from 'frappe-ui/src/components/Dropdown.vue';
import { computed, ref } from 'vue'
import KonectarLogo from './Icons/KonectarLogo.vue';
const props = defineProps({
isCollapsed: {
type: Boolean,
default: false,
},
})
// const { logout } = sessionStore()
let dropdownOptions = ref([
{
group: 'Manage',
hideLabel: true,
items: [
{
icon: 'corner-up-left',
label: 'Switch to Desk',
onClick: () => window.location.replace('/app'),
},
{
icon: 'life-buoy',
label:'Support',
onClick: () => window.open('https://t.me/frappecrm', '_blank'),
},
{
icon: 'book-open',
label: 'Docs',
onClick: () => window.open('https://docs.frappe.io/crm', '_blank'),
},
],
},
{
group: 'Others',
hideLabel: true,
items: [
{
icon: 'settings',
label:'Settings',
// onClick: () => (showSettingsModal.value = true),
},
{
icon: 'log-out',
label:'Log out',
onClick: () => logout.submit(),
},
],
},
])
</script>

View File

@ -0,0 +1,877 @@
<!-- <template>
<div class="flex items-center justify-between gap-2 px-5 py-4">
<div class="flex items-center gap-2">
<Dropdown :options="viewsDropdownOptions">
<template #default="{ open }">
<Button :label="__(currentView.label)">
<template #prefix>
<div v-if="isEmoji(currentView.icon)">{{ currentView.icon }}</div>
<FeatherIcon
v-else-if="typeof currentView.icon == 'string'"
:name="currentView.icon"
class="h-4"
/>
<component v-else :is="currentView.icon" class="h-4" />
</template>
<template #suffix>
<FeatherIcon
:name="open ? 'chevron-up' : 'chevron-down'"
class="h-4 text-gray-600"
/>
</template>
</Button>
</template>
</Dropdown>
<Dropdown :options="viewActions">
<template #default>
<Button icon="more-horizontal" />
</template>
</Dropdown>
</div>
<div class="-mr-2 h-[70%] border-l" />
<FadedScrollableDiv
class="flex flex-1 items-center overflow-x-auto px-1"
orientation="horizontal"
>
<div
v-for="filter in quickFilterList"
:key="filter.name"
class="m-1 min-w-36"
>
<QuickFilterField
:filter="filter"
@applyQuickFilter="(f, v) => applyQuickFilter(f, v)"
/>
</div>
</FadedScrollableDiv>
<div class="-ml-2 h-[70%] border-l" />
<div class="flex items-center gap-2">
<div
v-if="viewUpdated && route.query.view && (!view.public || isManager())"
class="flex items-center gap-2 border-r pr-2"
>
<Button :label="__('Cancel')" @click="cancelChanges" />
<Button :label="__('Save Changes')" @click="saveView" />
</div>
<div class="flex items-center gap-2">
<Button :label="__('Refresh')" @click="reload()" :loading="isLoading">
<template #icon>
<RefreshIcon class="h-4 w-4" />
</template>
</Button>
<GroupBy
v-if="route.params.viewType === 'group_by'"
v-model="list"
:doctype="doctype"
@update="updateGroupBy"
/>
<Filter
v-model="list"
:doctype="doctype"
:default_filters="filters"
@update="updateFilter"
/>
<SortBy v-model="list" :doctype="doctype" @update="updateSort" />
<ColumnSettings
v-if="!options.hideColumnsButton"
v-model="list"
:doctype="doctype"
@update="(isDefault) => updateColumns(isDefault)"
/>
<Dropdown
v-if="!options.hideColumnsButton"
:options="[
{
group: __('Options'),
hideLabel: true,
items: [
{
label: __('Export'),
icon: () =>
h(FeatherIcon, { name: 'download', class: 'h-4 w-4' }),
onClick: () => (showExportDialog = true),
},
],
},
]"
>
<template #default>
<Button icon="more-horizontal" />
</template>
</Dropdown>
</div>
</div>
</div>
<ViewModal
v-model="showViewModal"
v-model:view="viewModalObj"
:doctype="doctype"
:options="{
afterCreate: async (v) => {
await reloadView()
viewUpdated = false
router.push({
name: route.name,
params: { viewType: v.type || 'list' },
query: { view: v.name },
})
},
afterUpdate: () => {
viewUpdated = false
reloadView()
},
}"
/>
<Dialog
v-model="showExportDialog"
:options="{
title: __('Export'),
actions: [
{
label: __('Download'),
variant: 'solid',
onClick: () => exportRows(),
},
],
}"
>
<template #body-content>
<FormControl
variant="outline"
:label="__('Export Type')"
type="select"
:options="[
{
label: __('Excel'),
value: 'Excel',
},
{
label: __('CSV'),
value: 'CSV',
},
]"
v-model="export_type"
:placeholder="__('Excel')"
/>
<div class="mt-3">
<FormControl
type="checkbox"
:label="__('Export All {0} Record(s)', [list.data.total_count])"
v-model="export_all"
/>
</div>
</template>
</Dialog>
</template>
<script setup>
import DetailsIcon from '@components/Icons/DetailsIcon.vue'
import QuickFilterField from '@/components/QuickFilterField.vue'
import RefreshIcon from '@/components/Icons/RefreshIcon.vue'
import EditIcon from '@/components/Icons/EditIcon.vue'
import DuplicateIcon from '@/components/Icons/DuplicateIcon.vue'
import PinIcon from '@/components/Icons/PinIcon.vue'
import UnpinIcon from '@/components/Icons/UnpinIcon.vue'
import SortBy from '@/components/SortBy.vue'
import Filter from '@/components/Filter.vue'
import FadedScrollableDiv from '@/components/FadedScrollableDiv.vue'
import ColumnSettings from '@/components/ColumnSettings.vue'
import { globalStore } from '@/stores/global'
import { viewsStore } from '@/stores/views'
import { usersStore } from '@/stores/users'
import { isEmoji } from '@/utils'
import { createResource, Dropdown, call, FeatherIcon } from 'frappe-ui'
import { computed, ref, onMounted, watch, h, markRaw } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useDebounceFn } from '@vueuse/core'
import _ from 'lodash'
const props = defineProps({
doctype: {
type: String,
required: true,
},
filters: {
type: Object,
default: {},
},
options: {
type: Object,
default: {
hideColumnsButton: false,
defaultViewName: '',
allowedViews: ['list'],
},
},
})
const { $dialog } = globalStore()
const { reload: reloadView, getView } = viewsStore()
const { isManager } = usersStore()
const list = defineModel()
const loadMore = defineModel('loadMore')
const resizeColumn = defineModel('resizeColumn')
const updatedPageCount = defineModel('updatedPageCount')
const route = useRoute()
const router = useRouter()
const defaultParams = ref('')
const viewUpdated = ref(false)
const showViewModal = ref(false)
function getViewType() {
let viewType = route.params.viewType || 'list'
let types = {
list: {
label: __('List View'),
icon: 'list',
},
group_by: {
label: __('Group By View'),
icon: markRaw(DetailsIcon),
},
}
return types[viewType]
}
const currentView = computed(() => {
let _view = getView(route.query.view, route.params.viewType, props.doctype)
return {
label:
_view?.label || props.options?.defaultViewName || getViewType().label,
icon: _view?.icon || getViewType().icon,
}
})
const view = ref({
name: '',
label: '',
type: 'list',
icon: '',
filters: {},
order_by: 'modified desc',
columns: '',
rows: '',
load_default_columns: false,
pinned: false,
public: false,
})
const pageLength = computed(() => list.value?.data?.page_length)
const pageLengthCount = computed(() => list.value?.data?.page_length_count)
watch(loadMore, (value) => {
if (!value) return
updatePageLength(value, true)
})
watch(resizeColumn, (value) => {
if (!value) return
updateColumns()
})
watch(updatedPageCount, (value) => {
if (!value) return
updatePageLength(value)
})
function getParams() {
let _view = getView(route.query.view, route.params.viewType, props.doctype)
const filters = (_view?.filters && JSON.parse(_view.filters)) || {}
const order_by = _view?.order_by || 'modified desc'
const columns = _view?.columns || ''
const rows = _view?.rows || ''
if (_view) {
view.value = {
name: _view.name,
label: _view.label,
type: _view.type || 'list',
icon: _view.icon,
filters: _view.filters,
order_by: _view.order_by,
columns: _view.columns,
rows: _view.rows,
route_name: _view.route_name,
load_default_columns: _view.row,
pinned: _view.pinned,
public: _view.public,
}
} else {
view.value = {
name: '',
label: getViewType().label,
type: 'list',
icon: '',
filters: {},
order_by: 'modified desc',
columns: '',
rows: '',
route_name: route.name,
load_default_columns: true,
pinned: false,
public: false,
}
}
return {
doctype: props.doctype,
filters: filters,
order_by: order_by,
columns: columns,
rows: rows,
page_length: pageLength.value,
page_length_count: pageLengthCount.value,
view: {
custom_view_name: _view?.name || '',
view_type: _view?.type || route.params.viewType || 'list',
group_by_field: _view?.group_by_field || 'owner',
},
default_filters: props.filters,
}
}
list.value = createResource({
url: 'crm.api.doc.get_list_data',
params: getParams(),
cache: [props.doctype, route.query.view, route.params.viewType],
onSuccess(data) {
let cv = getView(route.query.view, route.params.viewType, props.doctype)
let params = list.value.params ? list.value.params : getParams()
defaultParams.value = {
doctype: props.doctype,
filters: params.filters,
order_by: params.order_by,
page_length: params.page_length,
page_length_count: params.page_length_count,
columns: data.columns,
rows: data.rows,
view: {
custom_view_name: cv?.name || '',
view_type: cv?.type || route.params.viewType || 'list',
group_by_field: params?.view?.group_by_field || 'owner',
},
default_filters: props.filters,
}
},
})
onMounted(() => useDebounceFn(reload, 100)())
const isLoading = computed(() => list.value?.loading)
function reload() {
list.value.params = getParams()
list.value.reload()
}
const showExportDialog = ref(false)
const export_type = ref('Excel')
const export_all = ref(false)
async function exportRows() {
let fields = JSON.stringify(list.value.data.columns.map((f) => f.key))
let filters = JSON.stringify(list.value.params.filters)
let order_by = list.value.params.order_by
let page_length = list.value.params.page_length
if (export_all.value) {
page_length = list.value.data.total_count
}
window.location.href = `/api/method/frappe.desk.reportview.export_query?file_format_type=${export_type.value}&title=${props.doctype}&doctype=${props.doctype}&fields=${fields}&filters=${filters}&order_by=${order_by}&page_length=${page_length}&start=0&view=Report&with_comment_count=1`
showExportDialog.value = false
export_all.value = false
export_type.value = 'Excel'
}
let defaultViews = []
let allowedViews = props.options.allowedViews || ['list']
if (allowedViews.includes('list')) {
defaultViews.push({
label: __(props.options?.defaultViewName) || __('List View'),
icon: 'list',
onClick() {
viewUpdated.value = false
router.push({ name: route.name })
},
})
}
if (allowedViews.includes('group_by')) {
defaultViews.push({
label: __(props.options?.defaultViewName) || __('Group By View'),
icon: markRaw(DetailsIcon),
onClick() {
viewUpdated.value = false
router.push({ name: route.name, params: { viewType: 'group_by' } })
},
})
}
function getIcon(icon, type) {
if (isEmoji(icon)) {
return h('div', icon)
} else if (!icon && type === 'group_by') {
return markRaw(DetailsIcon)
}
return icon || 'list'
}
const viewsDropdownOptions = computed(() => {
let _views = [
{
group: __('Default Views'),
hideLabel: true,
items: defaultViews,
},
]
if (list.value?.data?.views) {
list.value.data.views.forEach((view) => {
view.label = __(view.label)
view.type = view.type || 'list'
view.icon = getIcon(view.icon, view.type)
view.filters =
typeof view.filters == 'string'
? JSON.parse(view.filters)
: view.filters
view.onClick = () => {
viewUpdated.value = false
router.push({
name: route.name,
params: { viewType: view.type },
query: { view: view.name },
})
}
})
let publicViews = list.value.data.views.filter((v) => v.public)
let savedViews = list.value.data.views.filter(
(v) => !v.pinned && !v.public && !v.is_default
)
let pinnedViews = list.value.data.views.filter((v) => v.pinned)
publicViews.length &&
_views.push({
group: __('Public Views'),
items: publicViews,
})
savedViews.length &&
_views.push({
group: __('Saved Views'),
items: savedViews,
})
pinnedViews.length &&
_views.push({
group: __('Pinned Views'),
items: pinnedViews,
})
}
return _views
})
const quickFilterList = computed(() => {
let filters = [{ name: 'name', label: __('ID') }]
if (quickFilters.data) {
filters.push(...quickFilters.data)
}
filters.forEach((filter) => {
filter['value'] = filter.type == 'Check' ? false : ''
if (list.value.params?.filters[filter.name]) {
let value = list.value.params.filters[filter.name]
if (Array.isArray(value)) {
if (
(['Check', 'Select', 'Link', 'Date', 'Datetime'].includes(
filter.type
) &&
value[0]?.toLowerCase() == 'like') ||
value[0]?.toLowerCase() != 'like'
)
return
filter['value'] = value[1]?.replace(/%/g, '')
} else {
filter['value'] = value.replace(/%/g, '')
}
}
})
return filters
})
const quickFilters = createResource({
url: 'crm.api.doc.get_quick_filters',
params: { doctype: props.doctype },
cache: ['Quick Filters', props.doctype],
auto: true,
})
function applyQuickFilter(filter, value) {
let filters = { ...list.value.params.filters }
let field = filter.name
if (value) {
if (['Check', 'Select', 'Link', 'Date', 'Datetime'].includes(filter.type)) {
filters[field] = value
} else {
filters[field] = ['LIKE', `%${value}%`]
}
filter['value'] = value
} else {
delete filters[field]
filter['value'] = ''
}
updateFilter(filters)
}
function updateFilter(filters) {
viewUpdated.value = true
if (!defaultParams.value) {
defaultParams.value = getParams()
}
list.value.params = defaultParams.value
list.value.params.filters = filters
view.value.filters = filters
list.value.reload()
if (!route.query.view) {
create_or_update_default_view()
}
}
function updateSort(order_by) {
viewUpdated.value = true
if (!defaultParams.value) {
defaultParams.value = getParams()
}
list.value.params = defaultParams.value
list.value.params.order_by = order_by
view.value.order_by = order_by
list.value.reload()
if (!route.query.view) {
create_or_update_default_view()
}
}
function updateGroupBy(group_by_field) {
viewUpdated.value = true
if (!defaultParams.value) {
defaultParams.value = getParams()
}
list.value.params = defaultParams.value
list.value.params.view.group_by_field = group_by_field
view.value.group_by_field = group_by_field
list.value.reload()
if (!route.query.view) {
create_or_update_default_view()
}
}
function updateColumns(obj) {
if (!obj) {
obj = {
columns: list.value.data.columns,
rows: list.value.data.rows,
isDefault: false,
}
}
if (!defaultParams.value) {
defaultParams.value = getParams()
}
defaultParams.value.columns = view.value.columns = obj.isDefault
? ''
: obj.columns
defaultParams.value.rows = view.value.rows = obj.isDefault ? '' : obj.rows
view.value.load_default_columns = obj.isDefault
if (obj.reset) {
defaultParams.value.columns = getParams().columns
defaultParams.value.rows = getParams().rows
}
if (obj.reload) {
list.value.params = defaultParams.value
list.value.reload()
}
viewUpdated.value = true
if (!route.query.view) {
create_or_update_default_view()
}
}
function create_or_update_default_view() {
if (route.query.view) return
view.value.doctype = props.doctype
call(
'crm.fcrm.doctype.crm_view_settings.crm_view_settings.create_or_update_default_view',
{
view: view.value,
}
).then(() => {
reloadView()
view.value = {
label: view.value.label,
type: view.value.type || 'list',
icon: view.value.icon,
name: view.value.name,
filters: defaultParams.value.filters,
order_by: defaultParams.value.order_by,
group_by_field: defaultParams.value.view.group_by_field,
columns: defaultParams.value.columns,
rows: defaultParams.value.rows,
route_name: route.name,
load_default_columns: view.value.load_default_columns,
}
viewUpdated.value = false
})
}
function updatePageLength(value, loadMore = false) {
if (!defaultParams.value) {
defaultParams.value = getParams()
}
list.value.params = defaultParams.value
if (loadMore) {
list.value.params.page_length += list.value.params.page_length_count
} else {
if (
value == list.value.params.page_length &&
value == list.value.params.page_length_count
)
return
list.value.params.page_length = value
list.value.params.page_length_count = value
}
list.value.reload()
}
// View Actions
const viewActions = computed(() => {
let actions = [
{
group: __('Default Views'),
hideLabel: true,
items: [
{
label: __('Duplicate'),
icon: () => h(DuplicateIcon, { class: 'h-4 w-4' }),
onClick: () => duplicateView(),
},
],
},
]
if (route.query.view && (!view.value.public || isManager())) {
actions[0].items.push({
label: __('Edit'),
icon: () => h(EditIcon, { class: 'h-4 w-4' }),
onClick: () => editView(),
})
if (!view.value.public) {
actions[0].items.push({
label: view.value.pinned ? __('Unpin View') : __('Pin View'),
icon: () =>
h(view.value.pinned ? UnpinIcon : PinIcon, { class: 'h-4 w-4' }),
onClick: () => pinView(),
})
}
if (isManager()) {
actions[0].items.push({
label: view.value.public ? __('Make Private') : __('Make Public'),
icon: () =>
h(FeatherIcon, {
name: view.value.public ? 'lock' : 'unlock',
class: 'h-4 w-4',
}),
onClick: () => publicView(),
})
}
actions.push({
group: __('Delete View'),
hideLabel: true,
items: [
{
label: __('Delete'),
icon: 'trash-2',
onClick: () =>
$dialog({
title: __('Delete View'),
message: __('Are you sure you want to delete this view?'),
variant: 'danger',
actions: [
{
label: __('Delete'),
variant: 'solid',
theme: 'red',
onClick: (close) => deleteView(close),
},
],
}),
},
],
})
}
return actions
})
const viewModalObj = ref({})
function duplicateView() {
let label =
__(
getView(route.query.view, route.params.viewType, props.doctype)?.label
) || getViewType().label
view.value.name = ''
view.value.label = label + __(' (New)')
viewModalObj.value = view.value
showViewModal.value = true
}
function editView() {
let cView = getView(route.query.view, route.params.viewType, props.doctype)
view.value.name = route.query.view
view.value.label = __(cView?.label) || getViewType().label
view.value.icon = cView?.icon || ''
viewModalObj.value = view.value
showViewModal.value = true
}
function publicView() {
call('crm.fcrm.doctype.crm_view_settings.crm_view_settings.public', {
name: route.query.view,
value: !view.value.public,
}).then(() => {
view.value.public = !view.value.public
reloadView()
})
}
function pinView() {
call('crm.fcrm.doctype.crm_view_settings.crm_view_settings.pin', {
name: route.query.view,
value: !view.value.pinned,
}).then(() => {
view.value.pinned = !view.value.pinned
reloadView()
})
}
function deleteView(close) {
call('crm.fcrm.doctype.crm_view_settings.crm_view_settings.delete', {
name: route.query.view,
}).then(() => {
router.push({ name: route.name })
reloadView()
})
close()
}
function cancelChanges() {
reload()
viewUpdated.value = false
}
function saveView() {
view.value = {
label: view.value.label,
type: view.value.type || 'list',
icon: view.value.icon,
name: view.value.name,
filters: defaultParams.value.filters,
order_by: defaultParams.value.order_by,
group_by_field: defaultParams.value.view.group_by_field,
columns: defaultParams.value.columns,
rows: defaultParams.value.rows,
route_name: route.name,
load_default_columns: view.value.load_default_columns,
}
viewModalObj.value = view.value
showViewModal.value = true
}
function applyFilter({ event, idx, column, item, firstColumn }) {
let restrictedFieldtypes = ['Duration', 'Datetime', 'Time']
if (restrictedFieldtypes.includes(column.type) || idx === 0) return
if (idx === 1 && firstColumn.key == '_liked_by') return
event.stopPropagation()
event.preventDefault()
let filters = { ...list.value.params.filters }
let value = item.name || item.label || item
if (value) {
filters[column.key] = value
} else {
delete filters[column.key]
}
if (column.key == '_assign') {
if (item.length > 1) {
let target = event.target.closest('.user-avatar')
if (target) {
let name = target.getAttribute('data-name')
filters['_assign'] = ['LIKE', `%${name}%`]
}
} else {
filters['_assign'] = ['LIKE', `%${item[0].name}%`]
}
}
updateFilter(filters)
}
function applyLikeFilter() {
let filters = { ...list.value.params.filters }
if (!filters._liked_by) {
filters['_liked_by'] = ['LIKE', '%@me%']
} else {
delete filters['_liked_by']
}
updateFilter(filters)
}
function likeDoc({ name, liked }) {
createResource({
url: 'frappe.desk.like.toggle_like',
params: { doctype: props.doctype, name: name, add: liked ? 'No' : 'Yes' },
auto: true,
onSuccess: () => reload(),
})
}
defineExpose({ applyFilter, applyLikeFilter, likeDoc })
// Watchers
watch(
() => getView(route.query.view, route.params.viewType, props.doctype),
(value, old_value) => {
if (_.isEqual(value, old_value)) return
reload()
},
{ deep: true }
)
watch([() => route, () => route.params.viewType], (value, old_value) => {
if (value[0] === old_value[0] && value[1] === value[0]) return
reload()
})
</script>
-->

View File

@ -0,0 +1,42 @@
import router from '@/router'
import { computed, reactive } from 'vue'
import { createResource } from 'frappe-ui'
import { userResource } from './user'
export function sessionUser() {
const cookies = new URLSearchParams(document.cookie.split('; ').join('&'))
let _sessionUser = cookies.get('user_id')
if (_sessionUser === 'Guest') {
_sessionUser = null
}
return _sessionUser
}
export const session = reactive({
login: createResource({
url: 'login',
makeParams({ email, password }) {
return {
usr: email,
pwd: password,
}
},
onSuccess(data) {
userResource.reload()
session.user = sessionUser()
session.login.reset()
router.replace(data.default_route || '/')
},
}),
logout: createResource({
url: 'logout',
onSuccess() {
userResource.reset()
session.user = sessionUser()
router.replace({ name: 'Login' })
},
}),
user: sessionUser(),
isLoggedIn: computed(() => !!session.user),
})

View File

@ -0,0 +1,12 @@
import router from '@/router'
import { createResource } from 'frappe-ui'
export const userResource = createResource({
url: 'frappe.auth.get_logged_user',
cache: 'User',
onError(error) {
if (error && error.exc_type === 'AuthenticationError') {
router.push({ name: 'LoginPage' })
}
},
})

View File

@ -0,0 +1,2 @@
@import './assets/Inter/inter.css';
@import 'frappe-ui/src/style.css';

31
analystview/src/main.js Normal file
View File

@ -0,0 +1,31 @@
import './index.css'
import { createApp } from 'vue'
import router from './router'
import App from './App.vue'
import {
Button,
Card,
Input,
setConfig,
frappeRequest,
resourcesPlugin,
} from 'frappe-ui'
let app = createApp(App)
setConfig('resourceFetcher', frappeRequest)
app.use(router)
app.use(resourcesPlugin)
app.component('Button', Button)
app.component('Card', Card)
app.component('Input', Input)
app.mount('#app')

View File

@ -0,0 +1,37 @@
<template>
<div class="max-w-3xl py-12 mx-auto">
<h2 class="font-bold text-lg text-gray-600 mb-4">
Welcome {{ session.user }}!
</h2>
<Button theme="gray" variant="solid" icon-left="code" @click="ping.fetch" :loading="ping.loading">
Click to send 'ping' request
</Button>
<div>
{{ ping.data }}
</div>
<pre>{{ ping }}</pre>
<div class="flex flex-row space-x-2 mt-4">
<Button @click="showDialog = true">Open Dialog</Button>
<Button @click="session.logout.submit()">Logout</Button>
</div>
<!-- Dialog -->
<Dialog title="Title" v-model="showDialog"> Dialog content </Dialog>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { Dialog } from 'frappe-ui'
import { createResource } from 'frappe-ui'
import { session } from '../data/session'
const ping = createResource({
url: 'ping',
auto: true,
})
const showDialog = ref(false)
</script>

View File

@ -0,0 +1,37 @@
<template>
<div class="m-3 flex flex-row items-center justify-center">
<Card title="Login to your FrappeUI App!" class="w-full max-w-md mt-4">
<form class="flex flex-col space-y-2 w-full" @submit.prevent="submit">
<Input
required
name="email"
type="text"
placeholder="johndoe@email.com"
label="User ID"
/>
<Input
required
name="password"
type="password"
placeholder="••••••"
label="Password"
/>
<Button :loading="session.login.loading" variant="solid"
>Login</Button
>
</form>
</Card>
</div>
</template>
<script lang="ts" setup>
import { session } from '../data/session'
function submit(e) {
let formData = new FormData(e.target)
session.login.submit({
email: formData.get('email'),
password: formData.get('password'),
})
}
</script>

View File

@ -0,0 +1,365 @@
<template>
<div class="flex h-screen w-screen">
<div class="h-full border-r bg-gray-50">
<AppSidebar />
</div>
<div class="flex-1 flex flex-col h-full overflow-auto">
<div class="flex flex-col justify-between gap-2 sm:pl-5 pl-2 py-2">
<div class="flex items-center justify-between pr-2">
<div class="flex items-center gap-2 overflow-x-auto ">
<Breadcrumbs :items="breadcrumbs" />
<Dropdown :options="[
{
group: 'Manage',
items: [
{
label: 'Edit Title',
icon: () => h(FeatherIcon, { name: 'edit' }),
},
{
label: 'Manage Members',
icon: () => h(FeatherIcon, { name: 'users' }),
},
],
},
{
group: 'Delete',
items: [
{
label: 'Delete users',
icon: () => h(FeatherIcon, { name: 'edit' }),
},
{
label: 'Delete this project',
icon: () => h(FeatherIcon, { name: 'trash' }),
},
],
},
]">
<Button>
<template #icon>
<FeatherIcon name="more-horizontal" class="h-4 w-4" />
</template>
</Button>
</Dropdown>
<div class="p-2">
<TextInput :type="'search'" size="sm" variant="subtle" placeholder="Search" :disabled="false"
modelValue="" />
</div>
</div>
<Dropdown :options="[
{
label: 'Download XLS',
icon: () => h(FeatherIcon, { name: 'download' }),
onClick: () => exportRows(),
},
{
label: 'Download CSV',
icon: () => h(FeatherIcon, { name: 'download' }),
onClick: () => {
},
},
{
label: 'Download PDF',
icon: () => h(FeatherIcon, { name: 'download' }),
onClick: () => {
},
},
]" :button="{
label: 'Export',
}" />
</div>
<ListView v-if="optdata.data" v-model="optdata.data.pageLengthCount" :class="$attrs.class" :columns="columns"
:rows="optdata.data" :options="{
selectable: false,
showTooltip: true,
resizeColumn: true,
rowCount: optdata.data.row_count,
totalCount: optdata.data.total_count,
emptyState: {
title: 'No records found',
description: 'Create a new record to get started',
button: {
label: 'New Record',
variant: 'solid',
onClick: () => console.log('New Record'),
},
},
}" row-key="id" />
<ListFooter class="border-t sm:px-5 px-3 py-2" v-model="pageLengthCount" :options="{
rowCount: 20,
totalCount: optdata.data.pageLengthCount,
}" @loadMore="emit('loadMore')" />
</div>
</div>
</div>
<ListBulkActions ref="listBulkActionsRef" v-model="optdata.data" doctype="optlistdata" />
</template>
<script lang="ts" setup>
import { ref, h, computed, watch } from 'vue'
import AppSidebar from '@/components/AppSidebar.vue'
import { Dialog, ListView, Breadcrumbs, ListFooter, FeatherIcon, TextInput } from 'frappe-ui'
import { createResource, createListResource } from 'frappe-ui'
import Dropdown from 'frappe-ui/src/components/Dropdown.vue';
let title = 'Dashboard'
// const breadcrumbs = [{ label: title, route: { name: 'Optview' } }] :class="index % 2 === 0 ? 'tdStyle' : 'tdGray'"
const breadcrumbs = [{ label: 'Opt In Data', route: { name: 'OptView' } }]
//const optdata = defineModel()
const loadMore = defineModel('loadMore')
const resizeColumn = defineModel('resizeColumn')
const updatedPageCount = defineModel('updatedPageCount')
const defaultParams = ref({
doctype: '',
order_by: 'modified desc',
columns: '',
rows: '',
page_length: '',
page_length_count: '',
filters: {},
})
let optdata = createListResource({
doctype: "optlistdata",
fields: ["name", "kol_id", "kol_name", "instance", "project_name", "user_name", "instance", "npi_id", "client_name", "status", "optin_recieived_date", "optin_approved_date", "client_poc"],
auto: true,
start: 0,
//pageLength: 20,
cache: 'optlistdata',
filters: { "status": ('IN', 'Opted Out') },
onSuccess(data) {
optdata.data = data
},
})
optdata.fetch()
optdata.update({
fields: ['*'],
filters: {
status: 'Opted out'
}
})
const columns = [{
label: 'Instance',
key: 'instance',
width: '80px',
},
{
label: 'NPI',
key: 'npi_id',
width: '100px',
},
{
label: 'Client Name',
key: 'client_name',
},
{
label: 'Project Name',
key: 'project_name',
},
{
label: 'Kol ID',
key: 'kol_id',
width: '80px',
},
{
label: 'Kol Name',
key: 'kol_name',
},
{
label: 'Client User Name',
key: 'user_name',
},
{
label: 'Status',
key: 'status',
width: '100px',
color: 'blue',
},
{
label: 'Requested Date',
key: 'optin_recieived_date',
width: '150px',
},
{
label: 'Approved Date',
key: 'optin_approved_date',
width: '150px',
},
{
label: 'Client POC',
key: 'client_poc',
},
]
function getParams() {
// let _view = getView(route.query.view, route.params.viewType, props.doctype)
// const filters = (_view?.filters && JSON.parse(_view.filters)) || {}
// const order_by = _view?.order_by || 'modified desc'
// const columns = _view?.columns || ''
// const rows = _view?.rows || ''
const filters = {}
const order_by = 'modified desc'
const columns = []
const rows = []
const data = []
return {
doctype: "optlistdata",
filters: filters,
order_by: order_by,
columns: columns,
rows: rows,
data: data,
page_length: pageLength.value,
page_length_count: pageLengthCount.value,
// default_filters: props.filters,
}
}
// optdata.value = createResource({
// url: 'kolanalystapp.api.get_list_data',
// params: getParams(),
// cache: ["optlistdata"],
// onSuccess(data) {
// // let cv = getView(route.query.view, route.params.viewType, props.doctype)
// let params = getParams()
// data = data
// defaultParams.value = {
// doctype: "optlistdata",
// filters: params.filters,
// order_by: params.order_by,
// page_length: params.page_length,
// page_length_count: params.page_length_count,
// columns: data.columns,
// rows: data.rows,
// // default_filters: props.filters,
// }
// },
// })
console.log(`check optdata : ${optdata.value}`)
const rows = computed(() => {
if (!optdata.value) return []
else {
return parseRows(optdata.value)
}
})
const ping = createResource({
url: 'ping',
auto: true,
})
function parseRows(rows) {
return rows.map((lead) => {
let _rows = {}
optdata.data?.forEach((row) => {
_rows[row] = lead[row]
if (row == 'status') {
_rows[row] = {
label: lead.status,
color: 'blue',
}
}
})
return rows;
})
}
const showDialog = ref(false)
const emit = defineEmits([
'loadMore',
'updatePageCount',
'columnWidthUpdated',
'applyFilter',
'applyLikeFilter',
'likeDoc',
])
// const isLikeFilterApplied = computed(() => {
// return list.value.params?.filters?._liked_by ? true : false
// })
// const { user } = sessionStore()
// function isLiked(item) {
// if (item) {
// let likedByMe = JSON.parse(item)
// return likedByMe.includes(user)
// }
// }
const pageLength = computed(() => optdata.list.data?.page_length)
const pageLengthCount = computed(() => optdata.list.data?.page_length_count)
async function exportRows() {
let fields = JSON.stringify(columns.map((f) => f.key))
console.log(`print: ${fields}`)
console.log(`print: ${optdata.value?.data}`)
let doctype = 'optlistdata'
let export_type = 'Excel'
// let filters = JSON.stringify(optdata.params.filters)
// let order_by = optdata.value.params.order_by
// ["instance","npi_id","client_name","project_name","kol_id","kol_name","user_name","status","optin_recieived_date","optin_approved_date","client_poc"]
window.location.href = `/api/method/kolanalystapp.reportquery.export_query?file_format_type=${export_type}&title=${doctype}&doctype=${doctype}&fields=${fields}&start=0&view=Report&with_comment_count=1`
// showExportDialog.value = false
// export_all.value = true
// export_type.value = 'Excel'
}
watch(pageLengthCount, (val, old_value) => {
if (val === old_value) return
emit('updatePageCount', val)
})
const listBulkActionsRef = ref(null)
defineExpose({
//customListActions: computed(() => listBulkActionsRef.value?.customListActions),
})
</script>

View File

@ -0,0 +1,108 @@
<template>
<ListView v-if="list.data" v-model="list.data.pageLengthCount" :class="$attrs.class" :columns="columns"
:rows="list.data" :options="{
selectable: false,
showTooltip: true,
resizeColumn: true,
rowCount: list.data.row_count,
totalCount: list.data.total_count,
emptyState: {
title: 'No records found',
description: 'Create a new record to get started',
button: {
label: 'New Record',
variant: 'solid',
onClick: () => console.log('New Record'),
},
},
}" row-key="id" />
<ListFooter class="border-t sm:px-5 px-3 py-2" v-model="pageLengthCount" :options="{
rowCount: 20,
totalCount: list.data.pageLengthCount,
}" @loadMore="emit('loadMore')" />
</template>
<script lang="ts" setup>
import { ListView, ListFooter } from 'frappe-ui'
import { createResource } from 'frappe-ui'
import { h, ref, computed, watch } from 'vue'
const list = defineModel()
list.value = createResource({
url: 'kolanalystapp.api.get_list_data',
cache: ["optlistdata"],
onSuccess(data) {
// let cv = getView(route.query.view, route.params.viewType, props.doctype)
data = data
//rows = rows
},
})
const columns = [{
label: 'Instance',
key: 'instance',
width: '80px',
},
{
label: 'NPI',
key: 'npi_id',
width: '100px',
},
{
label: 'Client Name',
key: 'client_name',
},
{
label: 'Project Name',
key: 'project_name',
},
{
label: 'Kol ID',
key: 'kol_id',
width: '80px',
},
{
label: 'Kol Name',
key: 'kol_name',
},
{
label: 'Client User Name',
key: 'user_name',
},
{
label: 'Status',
key: 'status',
width: '100px',
color: 'blue',
},
{
label: 'Requested Date',
key: 'optin_recieived_date',
width: '150px',
},
{
label: 'Approved Date',
key: 'optin_approved_date',
width: '150px',
},
{
label: 'Client POC',
key: 'client_poc',
},
]
</script>

55
analystview/src/router.js Normal file
View File

@ -0,0 +1,55 @@
import { createRouter, createWebHistory } from 'vue-router'
import { session } from './data/session'
import { userResource } from '@/data/user'
const routes = [
{
path: '/',
name: 'Home',
component: () => import('@/pages/Home.vue'),
},
{
name: 'Login',
path: '/account/login',
component: () => import('@/pages/Login.vue'),
},
{
name: 'OptView',
path: '/home/OptView',
component: () => import('@/pages/OptInView.vue'),
},
{
name: 'OptList',
path: '/home/OptList',
component: () => import('@/pages/OptList.vue'),
},
{
name: 'LayoutHeader',
path: '/LayoutHeader',
component: () => import('@/components/LayoutHeader.vue'),
},
]
let router = createRouter({
history: createWebHistory('/analystview'),
routes,
})
router.beforeEach(async (to, from, next) => {
let isLoggedIn = session.isLoggedIn
try {
await userResource.promise
} catch (error) {
isLoggedIn = false
}
if (to.name === 'Login' && isLoggedIn) {
next({ name: 'Home' })
} else if (to.name !== 'Login' && !isLoggedIn) {
next({ name: 'Login' })
} else {
next()
}
})
export default router

View File

@ -0,0 +1,12 @@
module.exports = {
presets: [require('frappe-ui/src/utils/tailwind.config')],
content: [
'./index.html',
'./src/**/*.{vue,js,ts,jsx,tsx}',
'./node_modules/frappe-ui/src/components/**/*.{vue,js,ts,jsx,tsx}',
],
theme: {
extend: {},
},
plugins: [],
}

View File

@ -0,0 +1,22 @@
import path from 'path'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import frappeui from 'frappe-ui/vite'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [frappeui(), vue()],
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
},
},
build: {
outDir: `../${path.basename(path.resolve('..'))}/public/analystview`,
emptyOutDir: true,
target: 'es2015',
},
optimizeDeps: {
include: ['frappe-ui > feather-icons', 'showdown', 'engine.io-client'],
},
})

1856
analystview/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

206
kolanalystapp/api.py Normal file
View File

@ -0,0 +1,206 @@
import frappe
@frappe.whitelist(allow_guest=True)
def fetch_data(
doctype : str,
fields= None,
filters=None,
order_by=None,
page_length=20):
records = frappe.get_list('optlistdata',
filters=filters,
limit_page_length = page_length)
return [{
"kol_id" : str(client["kol_id"]),
"kol_name" : str(client["kol_name"]),
"project_name" : str(client["project_name"]),
"client_name" : str(client["client_name"]),
"client_poc" : str(client["client_poc"]),
"optin_received_date" : str(client["optin_recieived_date"]),
"optin_approved_date" : str(client["optin_approved_date"]),
"status" : str(client["status"]),
"instance" : str(client["instance"]),
"npi_id": str(client["npi_id"]),
"country" : str(client["country"]),
"data_processed_date" : str(client["data_processed_date"]),
"user_name": str(client["user_name"]),
}
for client in records]
@frappe.whitelist()
def get_list_data(
doctype: str,
filters: dict,
order_by: str,
page_length=20,
page_length_count=20,
columns=None,
rows=None,
view=None,
default_filters=None,
):
custom_view = False
filters = frappe._dict(filters)
custom_view_name = view.get('custom_view_name') if view else None
view_type = view.get('view_type') if view else None
group_by_field = view.get('group_by_field') if view else None
for key in filters:
value = filters[key]
if isinstance(value, list):
if "@me" in value:
value[value.index("@me")] = frappe.session.user
elif "%@me%" in value:
index = [i for i, v in enumerate(value) if v == "%@me%"]
for i in index:
value[i] = "%" + frappe.session.user + "%"
elif value == "@me":
filters[key] = frappe.session.user
if default_filters:
default_filters = frappe.parse_json(default_filters)
filters.update(default_filters)
is_default = True
if columns or rows:
custom_view = True
is_default = False
columns = frappe.parse_json(columns)
rows = frappe.parse_json(rows)
if not columns:
columns = [
{"label": "Name", "type": "Data", "key": "name", "width": "16rem"},
{"label": "Last Modified", "type": "Datetime", "key": "modified", "width": "8rem"},
]
if not rows:
rows = ["name"]
default_view_filters = {
"dt": doctype,
"type": view_type or 'list',
"is_default": 1,
"user": frappe.session.user,
}
_list = get_controller(doctype)
if not custom_view :
list_view_settings = frappe.get_doc("optlistdata", default_view_filters)
columns = frappe.parse_json(list_view_settings.columns)
rows = frappe.parse_json(list_view_settings.rows)
is_default = False
elif not custom_view or is_default and hasattr(_list, "default_list_data"):
columns = _list.default_list_data().get("columns")
if hasattr(_list, "default_list_data"):
rows = _list.default_list_data().get("rows")
# check if rows has all keys from columns if not add them
for column in columns:
if column.get("key") not in rows:
rows.append(column.get("key"))
column["label"] = _(column.get("label"))
if column.get("key") == "_liked_by" and column.get("width") == "10rem":
column["width"] = "50px"
# check if rows has group_by_field if not add it
if group_by_field and group_by_field not in rows:
rows.append(group_by_field)
data = fetch_data(
doctype,
fields=rows,
filters=filters,
order_by=order_by,
page_length=page_length,
) or []
fields = frappe.get_meta(doctype).fields
fields = [field for field in fields if field.fieldtype not in no_value_fields]
fields = [
{
"label": _(field.label),
"type": field.fieldtype,
"value": field.fieldname,
"options": field.options,
}
for field in fields
if field.label and field.fieldname
]
std_fields = [
{"label": "Name", "type": "Data", "value": "name"},
{"label": "Created On", "type": "Datetime", "value": "creation"},
{"label": "Last Modified", "type": "Datetime", "value": "modified"},
{
"label": "Modified By",
"type": "Link",
"value": "modified_by",
"options": "User",
},
{"label": "Assigned To", "type": "Text", "value": "_assign"},
{"label": "Owner", "type": "Link", "value": "owner", "options": "User"},
{"label": "Like", "type": "Data", "value": "_liked_by"},
]
for field in std_fields:
if field.get('value') not in rows:
rows.append(field.get('value'))
if field not in fields:
field["label"] = _(field["label"])
fields.append(field)
if not is_default and custom_view_name:
is_default = frappe.db.get_value("optlistdata", custom_view_name, "load_default_columns")
if group_by_field and view_type == "group_by":
def get_options(type, options):
if type == "Select":
return [option for option in options.split("\n")]
else:
has_empty_values = any([not d.get(group_by_field) for d in data])
options = list(set([d.get(group_by_field) for d in data]))
options = [u for u in options if u]
if has_empty_values:
options.append("")
if order_by and group_by_field in order_by:
order_by_fields = order_by.split(",")
order_by_fields = [(field.split(" ")[0], field.split(" ")[1]) for field in order_by_fields]
if (group_by_field, "asc") in order_by_fields:
options.sort()
elif (group_by_field, "desc") in order_by_fields:
options.sort(reverse=True)
else:
options.sort()
return options
for field in fields:
if field.get("value") == group_by_field:
group_by_field = {
"label": field.get("label"),
"name": field.get("value"),
"type": field.get("type"),
"options": get_options(field.get("type"), field.get("options")),
}
return {
# "data": data,
"columns": columns,
"rows": rows,
"fields": fields,
"group_by_field": group_by_field,
"page_length": page_length,
"page_length_count": page_length_count,
"is_default": is_default,
"views": get_views(doctype),
"total_count": len(frappe.get_list(doctype, filters=filters)),
"row_count": len(data),
"form_script": get_form_script(doctype),
"list_script": get_form_script(doctype, "List"),
}

View File

@ -227,3 +227,5 @@ app_license = "mit"
# "Logging DocType Name": 30 # days to retain logs
# }
website_route_rules = [{'from_route': '/analystview/<path:app_path>', 'to_route': 'analystview'},]

View File

@ -0,0 +1,8 @@
// Copyright (c) 2024, snehalatha and contributors
// For license information, please see license.txt
// frappe.ui.form.on("optlistdata", {
// refresh(frm) {
// },
// });

View File

@ -0,0 +1,131 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "field:kol_id",
"creation": "2024-07-03 18:39:37.119865",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"kol_id",
"kol_name",
"client_name",
"project_name",
"client_poc",
"optin_recieived_date",
"optin_approved_date",
"status",
"user_name",
"optin_type",
"data_processed_date",
"instance",
"npi_id",
"country"
],
"fields": [
{
"fieldname": "kol_id",
"fieldtype": "Data",
"in_preview": 1,
"in_standard_filter": 1,
"label": "KOL ID",
"unique": 1
},
{
"fieldname": "kol_name",
"fieldtype": "Data",
"label": "KOL Name"
},
{
"fieldname": "client_name",
"fieldtype": "Data",
"label": "Client Name"
},
{
"fieldname": "project_name",
"fieldtype": "Data",
"label": "Project Name"
},
{
"fieldname": "client_poc",
"fieldtype": "Data",
"label": "Client POC"
},
{
"fieldname": "optin_recieived_date",
"fieldtype": "Data",
"label": "Optin recieived date"
},
{
"fieldname": "optin_approved_date",
"fieldtype": "Data",
"label": "Optin approved date"
},
{
"fieldname": "status",
"fieldtype": "Data",
"label": "status"
},
{
"fieldname": "user_name",
"fieldtype": "Data",
"label": "User Name"
},
{
"fieldname": "optin_type",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Optin Type"
},
{
"fieldname": "data_processed_date",
"fieldtype": "Data",
"label": "Data Processed Date"
},
{
"fieldname": "instance",
"fieldtype": "Data",
"in_list_view": 1,
"in_preview": 1,
"label": "instance"
},
{
"fieldname": "npi_id",
"fieldtype": "Data",
"in_list_view": 1,
"in_preview": 1,
"in_standard_filter": 1,
"label": "NPI ID"
},
{
"fieldname": "country",
"fieldtype": "Data",
"label": "Country"
}
],
"index_web_pages_for_search": 1,
"is_virtual": 1,
"links": [],
"modified": "2024-07-04 17:47:38.089823",
"modified_by": "Administrator",
"module": "Opt in analyst app",
"name": "optlistdata",
"naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}

View File

@ -0,0 +1,173 @@
# Copyright (c) 2024, snehalatha and contributors
# For license information, please see license.txt
import frappe
from frappe.model.document import Document
from kolanalystapp.opt_in_analyst_app.kol_databases import get_records
class optlistdata(Document):
def db_insert(self, *args, **kwargs):
raise NotImplementedError
def load_from_db(self):
clients_array = get_records()
clientlist = []
d = {}
for client in clients_array :
clientlist.append(
{
"name": client["id"],
"kol_id" : str(client["id"]),
"kol_name" : str(client["kol_name"]),
"project_name" : str(client["project_name"]),
"client_name" : str(client["client_name"]),
"client_poc" : str(client["client_poc"]),
"status" : str(client["status"]),
"instance" : str(client["instance"]),
"npi_id": str(client["npi_num"]),
"data_processed_date": str(client["data_processed_date"]),
"country": str(client["user_country"]),
"user_name": str(client["user_name"]),
"optin_recieived_date" : optinReceived(str(client["project_details"]),"Opt-in Received"),
"optin_approved_date" : optinReceived(str(client["project_details"]),"Opt-in Approved"),
}
)
print(f"$$name: {self.name}")
for client in clientlist :
if client["kol_id"] == self.name :
d = client
super(Document, self).__init__(d)
def db_update(self):
raise NotImplementedError
def delete(self):
raise NotImplementedError
@staticmethod
def get_list(filters=None, page_length=20, **kwargs):
clientlist = []
clientlist = getValues()
# page_length = 20
# for client in clients_array :
# clientlist.append({
# **client,
# # "_id": str(client["id"]),
# # "kol_id" : str(client["id"]),
# # "kol_name" : str(client["kol_name"]),
# # "client_poc" : str(client["client_name"]),
# # "project_name" : str(client["project_details"]),
# # "cs_assigned_poc" : str(client["user_name"]),
# })
# {'id': 457002, 'kol_name': 'Ferdinand Douglas', 'project_name': None, 'client_name': 'Abbott EMEA', 'user_name': 'Abbott EMEAGeneric', 'project_details': 'New-2024-04-18,Opt-in Requested-2024-04-18'}
# print(f"################################check person {clientlist}")
return paginate(clientlist,page_length,1)
@staticmethod
def get_count(filters=None, **kwargs):
clientlist = []
clientlist = getValues()
return len(clientlist)
@staticmethod
def get_stats(**kwargs):
pass
# def get_virtual_data(start=0, page_length=20):
# data = [{"name": f"Doc {i+1}", "value": i+1}
# for i in range(start, start + page_length)]
# total_records = 1000
# return data, total_records
# def get_paginated_data(start=0, page_length=20):
# start = int(start)
# page_length = int(page_length)
# data, total_records = get_virtual_data(start, page_length)
# return {
# "data": data,
# "total_records": total_records,
# }
def paginate(items, page_size, page_number):
start_index = (page_number - 1) * page_size
end_index = start_index + page_size
return items[start_index:end_index]
def getValues():
records = get_records()
# print(f"rec########{records}")
return[
{
"name": client["id"],
"kol_id" : str(client["id"]),
"kol_name" : str(client["kol_name"]),
"project_name" : str(client["project_name"]),
"client_name" : str(client["client_name"]),
"client_poc" : str(client["client_poc"]),
"status" : str(client["status"]),
"instance" : str(client["instance"]),
"npi_id": str(client["npi_num"]),
"data_processed_date": str(client["data_processed_date"]),
"country": str(client["user_country"]),
"user_name": str(client["user_name"]),
"optin_recieived_date" : optinReceived(str(client["project_details"]),"Opt-in Received"),
"optin_approved_date" : optinReceived(str(client["project_details"]),"Opt-in Approved"),
}
for client in records
]
def optinReceived(value,key):
if not value:
return "no date"
else:
dictionary = dict(subString.split(":") for subString in value.split(","))
if(key in dictionary):
return dictionary[key]
else:
return ' '
def optinStatus(str):
if not str:
return str("")
else:
dictionary = dict(subString.split(":") for subString in str.split(","))
if("Opt-in" in dictionary):
if(len(dictionary) == 1 ):
return "Opted In"
else:
if("Opt-out" in dictionary):
return "Opted Out"

View File

@ -0,0 +1,9 @@
# Copyright (c) 2024, snehalatha and Contributors
# See license.txt
# import frappe
from frappe.tests.utils import FrappeTestCase
class Testoptlistdata(FrappeTestCase):
pass

View File

@ -0,0 +1,68 @@
import mysql.connector
from mysql.connector import Error
# Define the database connection details
db_configs = [
{
'user': 'snehalatha',
'password': 'paSsWord@#654',
'host': 'konectar-readreplica-rds.konectar.io',
'database': 'kolm_lite_cardio',
'instance':'cardio'
},
{
'user': 'snehalatha',
'password': 'paSsWord@#654',
'host': 'konectar-readreplica-rds.konectar.io',
'database': 'kolm_lite_veterinary',
'instance':'veterinary'
},
{
'user': 'snehalatha',
'password': 'paSsWord@#654',
'host': 'konectar-readreplica-rds.konectar.io',
'database': 'kolm_lite_oralhealth',
'instance':'oralhealth'
}
]
def fetch_records(config):
try:
# Connect to the database
connection = mysql.connector.connect(
host=config['host'],
user=config['user'],
password=config['password'],
database=config['database']
)
if connection.is_connected():
cursor = connection.cursor(dictionary=True)
cursor.execute(f"SELECT '{ config['instance'] }' as instance, kols.npi_num,kols.id,CONCAT_WS(' ',kols.first_name,kols.middle_name,kols.last_name) as kol_name, projects.name as project_name, clients.name as client_name,CONCAT(client_users.first_name,' ',client_users.last_name) as user_name, GROUP_CONCAT(DISTINCT log_activities.transaction_name,':',date(log_activities.created_on)) as project_details,case when user_kols.opt_in_out_status = 3 then 'Opted Out' when user_kols.opt_in_out_status = 4 then 'Opted In' else '' end as status , date(kols.data_updated) as data_processed_date,countries.Country as user_country,CONCAT(poc.first_name,' ',poc.last_name) as client_poc FROM kols left join user_kols on user_kols.kol_id = kols.id left join countries on countries.CountryId = kols.country_id inner join opt_inout_statuses on opt_inout_statuses.id = user_kols.opt_in_out_status left join log_activities on log_activities.miscellaneous1 = user_kols.kol_id left join client_users on client_users.id = user_kols.user_id left join clients on client_users.client_id = clients.id left join client_users as poc on poc.id = clients.created_by left join project_clients on project_clients.client_id = clients.id left join projects on project_clients.project_id = projects.id where log_activities.module ='opt_in_out' and log_activities.transaction_name in ('New','Opt-in Requested','Opt-out','Opt-in Approved','Opt-in Expired','Opt-in Received','Opt-in') group by kols.id;")
records = cursor.fetchall()
return records
except Error as e:
print(f"Error while connecting to MySQL: {e}")
return None
finally:
if connection.is_connected():
cursor.close()
connection.close()
# Loop through the databases and fetch records
def get_records():
all_records = []
for config in db_configs:
records = fetch_records(config)
if records:
all_records.extend(records)
return all_records
# Print or process the fetched records
# for record in all_records:
# print(record)

View File

@ -0,0 +1,121 @@
import frappe
import json
from functools import lru_cache
from sql_metadata import Parser
import frappe
import frappe.permissions
from frappe import _
from frappe.core.doctype.access_log.access_log import make_access_log
from frappe.model import child_table_fields, default_fields, get_permitted_fields, optional_fields
from frappe.model.base_document import get_controller
from frappe.model.db_query import DatabaseQuery
from frappe.model.utils import is_virtual_doctype
from frappe.utils import add_user_info, cint, format_duration
from frappe.utils.data import sbool
@frappe.whitelist()
@frappe.read_only()
def export_query():
"""export from report builder"""
from frappe.desk.utils import get_csv_bytes, pop_csv_params, provide_binary_file
form_params = get_form_params()
form_params["limit_page_length"] = None
form_params["as_list"] = True
doctype = form_params.pop("doctype")
file_format_type = form_params.pop("file_format_type")
title = form_params.pop("title", doctype)
csv_params = pop_csv_params(form_params)
add_totals_row = 1 if form_params.pop("add_totals_row", None) == "1" else None
frappe.permissions.can_export(doctype, raise_exception=True)
if selection := form_params.pop("selected_items", None):
form_params["filters"] = {"name": ("in", json.loads(selection))}
make_access_log(
doctype=doctype,
file_type=file_format_type,
report_name=form_params.report_name,
filters=form_params.filters,
)
db_query = DatabaseQuery(doctype)
ret = frappe.get_list(doctype)
if add_totals_row:
ret = append_totals_row(ret)
# //data = [[_("Sr"), *get_labels(fields, doctype)]]
data.extend([i + 1, *list(row)] for i, row in enumerate(ret.value))
data = handle_duration_fieldtype_values(doctype, data, db_query.fields)
if file_format_type == "CSV":
from frappe.utils.xlsxutils import handle_html
file_extension = "csv"
content = get_csv_bytes(
[[handle_html(frappe.as_unicode(v)) if isinstance(v, str) else v for v in r] for r in data],
csv_params,
)
elif file_format_type == "Excel":
from frappe.utils.xlsxutils import make_xlsx
file_extension = "xlsx"
content = make_xlsx(data, doctype).getvalue()
provide_binary_file(title, file_extension, content)
def get_form_params():
"""parse GET request parameters."""
data = frappe._dict(frappe.local.form_dict)
# clean_params(data)
# validate_args(data)
return data
def get_labels(fields, doctype):
"""get column labels based on column names"""
labels = []
for key in fields:
try:
parenttype, fieldname = parse_field(key)
except ValueError:
continue
parenttype = parenttype or doctype
if parenttype == doctype and fieldname == "name":
label = _("ID", context="Label of name column in report")
else:
df = frappe.get_meta(parenttype).get_field(fieldname)
label = _(df.label if df else fieldname.title())
if parenttype != doctype:
# If the column is from a child table, append the child doctype.
# For example, "Item Code (Sales Invoice Item)".
label += f" ({ _(parenttype) })"
labels.append(label)
return labels
def handle_duration_fieldtype_values(doctype, data, fields):
for field in fields:
try:
parenttype, fieldname = parse_field(field)
except ValueError:
continue
parenttype = parenttype or doctype
df = frappe.get_meta(parenttype).get_field(fieldname)
if df and df.fieldtype == "Duration":
index = fields.index(field) + 1
for i in range(1, len(data)):
val_in_seconds = data[i][index]
if val_in_seconds:
duration_val = format_duration(val_in_seconds, df.hide_days)
data[i][index] = duration_val
return data

15
package.json Normal file
View File

@ -0,0 +1,15 @@
{
"name": "kolanalystapp",
"version": "1.0.0",
"description": "Opt in analyst app",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"postinstall": "cd analystview && yarn install",
"dev": "cd analystview && yarn dev",
"build": "cd analystview && yarn build"
},
"keywords": [],
"author": "",
"license": "ISC"
}