first commit
This commit is contained in:
parent
55039e2613
commit
98646422e9
|
@ -0,0 +1,5 @@
|
|||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"semi": false,
|
||||
"singleQuote": true
|
||||
}
|
|
@ -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/)
|
|
@ -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>
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 440 B |
|
@ -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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -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');
|
||||
}
|
|
@ -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 |
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
-->
|
|
@ -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),
|
||||
})
|
|
@ -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' })
|
||||
}
|
||||
},
|
||||
})
|
|
@ -0,0 +1,2 @@
|
|||
@import './assets/Inter/inter.css';
|
||||
@import 'frappe-ui/src/style.css';
|
|
@ -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')
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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
|
|
@ -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: [],
|
||||
}
|
|
@ -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'],
|
||||
},
|
||||
})
|
File diff suppressed because it is too large
Load Diff
|
@ -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"),
|
||||
}
|
|
@ -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'},]
|
|
@ -0,0 +1,8 @@
|
|||
// Copyright (c) 2024, snehalatha and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
// frappe.ui.form.on("optlistdata", {
|
||||
// refresh(frm) {
|
||||
|
||||
// },
|
||||
// });
|
|
@ -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": []
|
||||
}
|
|
@ -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"
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -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
|
|
@ -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)
|
||||
|
|
@ -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
|
|
@ -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"
|
||||
}
|
Loading…
Reference in New Issue