diff --git a/backend/felicity_lims/felicity/api/gql/analytics/types.py b/backend/felicity_lims/felicity/api/gql/analytics/types.py index 9ea06d22..ac26e184 100644 --- a/backend/felicity_lims/felicity/api/gql/analytics/types.py +++ b/backend/felicity_lims/felicity/api/gql/analytics/types.py @@ -1,7 +1,10 @@ from typing import List, Optional - +from datetime import datetime import strawberry # noqa +from felicity.api.gql.analysis.types.analysis import AnalysisType +from felicity.api.gql.user.types import UserType + @strawberry.type class Nothing: @@ -73,3 +76,23 @@ class LaggardData: @strawberry.type class LaggardStatistics: data: List[LaggardData] + + +@strawberry.type +class ReportMetaType: + uid: int + period_start: datetime + period_end: datetime + date_column: str + location: Optional[str] + sample_states: Optional[str] + report_type: str + status: Optional[str] + temp: Optional[str] + analyses: Optional[List[AnalysisType]] + created_at: Optional[datetime] + created_by_uid: Optional[int] + created_by: Optional[UserType] + updated_at: Optional[datetime] + updated_by_uid: Optional[int] + updated_by: Optional[UserType] \ No newline at end of file diff --git a/backend/felicity_lims/felicity/api/gql/notification/types.py b/backend/felicity_lims/felicity/api/gql/notification/types.py index 8527d56f..1906929c 100644 --- a/backend/felicity_lims/felicity/api/gql/notification/types.py +++ b/backend/felicity_lims/felicity/api/gql/notification/types.py @@ -4,11 +4,13 @@ from typing import List, Optional, Union import strawberry # noqa from felicity.api.gql.analysis.types.analysis import SampleType from felicity.api.gql.analysis.types.results import AnalysisResultType +from felicity.api.gql.analytics.types import ReportMetaType from felicity.api.gql.setup.types import DepartmentType from felicity.api.gql.user.types import GroupType, UserType from felicity.api.gql.worksheet.types import WorkSheetType from felicity.apps.analysis.models.analysis import Sample from felicity.apps.analysis.models.results import AnalysisResult +from felicity.apps.analytics.models import ReportMeta from felicity.apps.worksheet.models import WorkSheet @@ -19,7 +21,7 @@ class UnknownObjectType: actionObject = strawberry.union( "actionObject", - [WorkSheetType, SampleType, AnalysisResultType], + [WorkSheetType, SampleType, AnalysisResultType, ReportMetaType], description="Union of possible object types for streams", ) @@ -50,7 +52,7 @@ class ActivityStreamType: @strawberry.field async def action_object( self, info - ) -> Union[WorkSheetType, SampleType, AnalysisResultType, UnknownObjectType]: + ) -> Union[WorkSheetType, SampleType, AnalysisResultType, ReportMetaType, UnknownObjectType]: if self.action_object_type == "sample": sample = await Sample.get(uid=self.action_object_uid) return SampleType( @@ -70,6 +72,12 @@ class ActivityStreamType: exclude=["right", "left", "tree_id", "level", "worksheet"] ), parent=None) + if self.action_object_type == "report": + report = await ReportMeta.get(uid=self.action_object_uid) + return ReportMetaType(**report.marshal_simple( + exclude=[] + )) + return UnknownObjectType( message=f"Please provide a resolver for object of type {self.action_object_type}" ) diff --git a/backend/felicity_lims/felicity/apps/analytics/tasks.py b/backend/felicity_lims/felicity/apps/analytics/tasks.py index 553f2c45..53be4774 100644 --- a/backend/felicity_lims/felicity/apps/analytics/tasks.py +++ b/backend/felicity_lims/felicity/apps/analytics/tasks.py @@ -6,12 +6,13 @@ from felicity.apps.analysis.models.analysis import Sample from felicity.apps.analytics import SampleAnalyticsInit, conf, models from felicity.apps.job import conf as job_conf from felicity.apps.job import models as job_models -from felicity.apps.notification.utils import ReportNotifier +from felicity.apps.notification.utils import ReportNotifier, FelicityStreamer logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) report_notifier = ReportNotifier() +streamer = FelicityStreamer() async def generate_report(job_uid: str): @@ -67,4 +68,5 @@ async def generate_report(job_uid: str): f"Your {report.report_type} report was successfully generated", report.created_by, ) + await streamer.stream(report, report.created_by, "generated", "report") return True diff --git a/frontend/vite/src/auth.ts b/frontend/vite/src/auth.ts index 80d6a0b1..e31cbde6 100644 --- a/frontend/vite/src/auth.ts +++ b/frontend/vite/src/auth.ts @@ -37,8 +37,18 @@ const authFromStorage2 = (): { } const authLogout = () => { - localStorage.removeItem(STORAGE_AUTH_KEY); } -export { authToStorage, authFromStorage,authFromStorage2, authLogout } \ No newline at end of file + +const getAuthData = () => { + let data: any = {}; + if(localStorage.getItem(STORAGE_AUTH_KEY)){ + const auth = JSON.parse(localStorage.getItem(STORAGE_AUTH_KEY)!) + data = { auth } + } + return data; +} + + +export { authToStorage, authFromStorage,authFromStorage2, authLogout, getAuthData } \ No newline at end of file diff --git a/frontend/vite/src/axios/with-auth.ts b/frontend/vite/src/axios/with-auth.ts index 4e0a43b7..39322092 100644 --- a/frontend/vite/src/axios/with-auth.ts +++ b/frontend/vite/src/axios/with-auth.ts @@ -1,26 +1,25 @@ import axios from 'axios'; -import { useAuthStore } from "../stores" +import { getAuthData, authLogout } from "../auth" import { REST_BASE_URL } from '../conf' const getAuthHeaders = async () => { - const authStore = useAuthStore(); + const authData = getAuthData(); - if (authStore?.auth?.token) { + if (authData?.auth?.token) { return { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, PATCH, PUT, DELETE, OPTIONS', 'Access-Control-Allow-Headers': 'Origin, Content-Type, X-Auth-Token', - ...(authStore?.auth?.token && { + ...(authData?.auth?.token && { 'x-felicity-user-id': "felicity-user", 'x-felicity-role': "felicity-administrator", - 'Authorization': `Bearer ${authStore?.auth?.token}` + 'Authorization': `Bearer ${authData?.auth?.token}` }), } } - - authStore.logout(); + authLogout(); }; const axiosInstance = axios.create({ diff --git a/frontend/vite/src/components/Accordion.vue b/frontend/vite/src/components/Accordion.vue index 2db4c0ac..629eb2f8 100644 --- a/frontend/vite/src/components/Accordion.vue +++ b/frontend/vite/src/components/Accordion.vue @@ -25,6 +25,5 @@ import { ref } from 'vue'; let show = ref(false); - diff --git a/frontend/vite/src/composables/analytics.ts b/frontend/vite/src/composables/analytics.ts new file mode 100644 index 00000000..3170d8b2 --- /dev/null +++ b/frontend/vite/src/composables/analytics.ts @@ -0,0 +1,56 @@ +import { toRefs, reactive } from 'vue' +import axios from "../axios/with-auth"; + +import useNotifyToast from "./alert_toast"; +import { IReportListing } from '../models/reports'; + +const { toastSuccess, toastWarning } = useNotifyToast(); + +const state = reactive({ + reports: [], +} as { + reports: IReportListing[]; +}); + +export default function useAnalyticsComposable() { + + const fetchReports = async () => { + await axios.get("reports").then((resp) => { + state.reports = resp.data; + }) + } + + const generateReport = async (payload) => { + await axios.post("reports", payload).then((resp) => { + state.reports.push(resp.data); + }) + } + + const deleteReport = async (report: IReportListing) => { + await axios.delete("reports/" + report.uid).then((resp) => { + const data = resp.data; + const index = state.reports.findIndex((x) => x.uid === data.uid); + if (index > -1) { + state.reports.splice(index, 1); + toastSuccess(data.message); + } else { + toastWarning("Failed to remove report: Please refresh your page"); + } + }) + } + + const updateReport = (report: IReportListing) => { + const index = state.reports.findIndex((x) => x.uid === report.uid); + if (index > -1) { + state.reports[index] = {...state.reports[index], ...report } + } + } + + return { + ...toRefs(state), + fetchReports, + generateReport, + deleteReport, + updateReport + } +} diff --git a/frontend/vite/src/composables/index.ts b/frontend/vite/src/composables/index.ts index 31d25894..d5a7ca5f 100644 --- a/frontend/vite/src/composables/index.ts +++ b/frontend/vite/src/composables/index.ts @@ -5,6 +5,7 @@ import userPreferenceComposable from "./preferences" import useReportComposable from "./reports" import useSampleComposable from "./samples" import useWorkSheetComposable from "./worksheet" +// import useAnalyticsComposable from "./analytics" export { useNotifyToast, @@ -13,5 +14,6 @@ export { userPreferenceComposable, useReportComposable, useSampleComposable, - useWorkSheetComposable + useWorkSheetComposable, + // useAnalyticsComposable } \ No newline at end of file diff --git a/frontend/vite/src/graphql/stream.subscriptions.ts b/frontend/vite/src/graphql/stream.subscriptions.ts index 9ea90aa3..d4e8cc61 100644 --- a/frontend/vite/src/graphql/stream.subscriptions.ts +++ b/frontend/vite/src/graphql/stream.subscriptions.ts @@ -66,6 +66,11 @@ subscription getSystemActivity { result status } + ...on ReportMetaType { + uid + status + location + } } targetUid verb diff --git a/frontend/vite/src/main.ts b/frontend/vite/src/main.ts index fe23878b..a5cdf990 100644 --- a/frontend/vite/src/main.ts +++ b/frontend/vite/src/main.ts @@ -28,6 +28,7 @@ pinia.use(({ store }) => { }); const app = createApp(App) +app.use(pinia) app.component('font-awesome-icon', FontAwesomeIcon) app.component('default-layout', LayoutDashboard) app.component('empty-layout', LayoutEmpty) @@ -35,6 +36,5 @@ app.use(urql, urqlClient) app.use(VueSweetalert2) app.use(MotionPlugin) app.use(router) -app.use(pinia) app.mount('#app') diff --git a/frontend/vite/src/models/auth.ts b/frontend/vite/src/models/auth.ts index 5e774bab..ef5b66b7 100644 --- a/frontend/vite/src/models/auth.ts +++ b/frontend/vite/src/models/auth.ts @@ -14,6 +14,9 @@ export interface IUser { isSuperuser?: boolean; authUid?: number; auth?: IUserAuth; + // for API axios + first_name?: string; + last_name?: string; } export interface IUserAuth { diff --git a/frontend/vite/src/models/pagination.ts b/frontend/vite/src/models/pagination.ts index 1f373a5c..bf399125 100644 --- a/frontend/vite/src/models/pagination.ts +++ b/frontend/vite/src/models/pagination.ts @@ -1,4 +1,4 @@ -interface IPageInfo { +export interface IPageInfo { endCursor?: string, hasNextPage?: boolean, hasPreviousPage?: boolean, diff --git a/frontend/vite/src/stores/auth.ts b/frontend/vite/src/stores/auth.ts index cbb7d7c4..82fbbe7a 100644 --- a/frontend/vite/src/stores/auth.ts +++ b/frontend/vite/src/stores/auth.ts @@ -38,7 +38,6 @@ export const useAuthStore = defineStore('auth', () => { const logout = () => { toastInfo("Good bye " + auth.value.user?.firstName) - localStorage.removeItem(STORAGE_AUTH_KEY) reset() } diff --git a/frontend/vite/src/stores/client.ts b/frontend/vite/src/stores/client.ts index a9849282..01af5b5a 100644 --- a/frontend/vite/src/stores/client.ts +++ b/frontend/vite/src/stores/client.ts @@ -5,6 +5,7 @@ import { GET_ALL_CLIENTS, GET_CLIENT_BY_UID } from '../graphql/clients.queries'; import { addListsUnique } from '../utils'; import { IClient, IClientContact } from '../models/client' +import { IPageInfo } from '../models/pagination' import { useApiUtil } from '../composables' @@ -20,7 +21,7 @@ export const useClientStore = defineStore('client', { clientContacts: [], fetchingClientContacts: false, clientCount: 0, - clientPageInfo: undefined, + clientPageInfo: {}, } as { clients: IClient[]; fetchingClients: boolean; @@ -29,7 +30,7 @@ export const useClientStore = defineStore('client', { clientContacts: IClientContact[]; fetchingClientContacts: boolean; clientCount?: number; - clientPageInfo?: any; + clientPageInfo?: IPageInfo; } }, getters: { diff --git a/frontend/vite/src/stores/stream.ts b/frontend/vite/src/stores/stream.ts index 54d83118..2601a3a8 100644 --- a/frontend/vite/src/stores/stream.ts +++ b/frontend/vite/src/stores/stream.ts @@ -6,6 +6,7 @@ import { import { pipe, subscribe } from 'wonka'; import { useWorksheetStore } from './worksheet' import { useSampleStore } from './sample' +import useAnalyticsComposable from '../composables/analytics'; export const useStreamStore = defineStore('stream', { @@ -19,11 +20,12 @@ export const useStreamStore = defineStore('stream', { }, actions: { addStream(payload){ + const { updateReport } = useAnalyticsComposable() const wsStore = useWorksheetStore() const sampleStore = useSampleStore() this.streams?.unshift(payload); - + if(payload.actionObjectType === "sample"){ sampleStore.updateSampleStatus(payload.actionObject) } @@ -32,6 +34,10 @@ export const useStreamStore = defineStore('stream', { wsStore.updateWorksheetStatus(payload.actionObject) } + if(payload.actionObjectType === "report"){ + updateReport(payload.actionObject) + } + if(payload.actionObjectType === "result"){ sampleStore.updateAnalysesResultsStatus([payload.actionObject]) wsStore.updateAnalysesResults([payload.actionObject]) diff --git a/frontend/vite/src/urql.ts b/frontend/vite/src/urql.ts index 441326b9..1df2685b 100644 --- a/frontend/vite/src/urql.ts +++ b/frontend/vite/src/urql.ts @@ -16,7 +16,7 @@ import { authExchange } from '@urql/exchange-auth'; import { SubscriptionClient } from 'subscriptions-transport-ws' import { pipe, tap } from 'wonka' -import { useAuthStore } from "./stores" +import { getAuthData, authLogout } from "./auth" import { GQL_BASE_URL, WS_BASE_URL } from './conf' import { useNotifyToast } from './composables' @@ -27,13 +27,13 @@ const subscriptionClient = new SubscriptionClient( WS_BASE_URL, { reconnect: true, lazy: true, connectionParams: () => { - const authStore = useAuthStore(); + const authData = getAuthData(); return { headers: { - ...(authStore?.auth?.token && { + ...(authData?.auth?.token && { 'x-felicity-user-id': "felicity-user-x", 'x-felicity-role': "felicity-role-x", - 'Authorization': `Bearer ${authStore?.auth?.token}` + 'Authorization': `Bearer ${authData?.auth?.token}` }) }, } @@ -41,11 +41,11 @@ const subscriptionClient = new SubscriptionClient( WS_BASE_URL, { }); const getAuth = async ({ authState }) => { - const authStore = useAuthStore(); + const authData = getAuthData(); if (!authState) { - if (authStore?.auth?.token) { - return { token: authStore?.auth?.token }; + if (authData?.auth?.token) { + return { token: authData?.auth?.token }; } return null; } @@ -57,7 +57,7 @@ const getAuth = async ({ authState }) => { toastError("Faied to get Auth Data. Login"); - authStore.logout(); + authLogout(); return null; }; @@ -118,7 +118,7 @@ export const urqlClient = createClient({ } if (isAuthError) { toastError("Unknown Network Error Encountered") - useAuthStore().logout(); + authLogout(); } }, }), @@ -135,16 +135,16 @@ export const urqlClient = createClient({ }), ], fetchOptions: () => { - const authStore = useAuthStore(); + const authData = getAuthData(); return { headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, PATCH, PUT, DELETE, OPTIONS', 'Access-Control-Allow-Headers': 'Origin, Content-Type, X-Auth-Token', - ...(authStore?.auth?.token && { + ...(authData?.auth?.token && { 'x-felicity-user-id': "felicity-user-x", 'x-felicity-role': "felicity-role-x", - 'Authorization': `Bearer ${authStore?.auth?.token}` + 'Authorization': `Bearer ${authData?.auth?.token}` }), }, }; diff --git a/frontend/vite/src/views/client/Clients.vue b/frontend/vite/src/views/client/Clients.vue index 6c8d519e..09b1ef4f 100644 --- a/frontend/vite/src/views/client/Clients.vue +++ b/frontend/vite/src/views/client/Clients.vue @@ -82,7 +82,7 @@ function showMoreClients(): void { clientParams.first = +clientBatch.value; - clientParams.after = clientPageInfo?.value?.endCursor; + clientParams.after = clientPageInfo?.value?.endCursor!; clientParams.text = filterText.value; clientParams.filterAction = false; clientStore.fetchClients(clientParams); @@ -186,11 +186,11 @@