From ebd6a2eed2889adda55bbc4ce3b0e861946c237f Mon Sep 17 00:00:00 2001
From: Josh Traill <josh.traill@quartech.com>
Date: Mon, 13 Jan 2025 10:59:08 -0800
Subject: [PATCH 1/4] feat: add in snack bar exceptions feat: snackbar store
 feat: raise snacks globally

---
 web/src/components/shared/Snackbar.vue       | 23 +++++++++++++
 web/src/services/HttpService.ts              | 10 ++++++
 web/src/stores/SnackbarStore.ts              | 18 ++++++++++
 web/src/stores/index.ts                      |  1 +
 web/tests/components/shared/Snackbar.test.ts | 31 +++++++++++++++++
 web/tests/stores/SnackbarStore.test.ts       | 35 ++++++++++++++++++++
 6 files changed, 118 insertions(+)
 create mode 100644 web/src/components/shared/Snackbar.vue
 create mode 100644 web/src/stores/SnackbarStore.ts
 create mode 100644 web/tests/components/shared/Snackbar.test.ts
 create mode 100644 web/tests/stores/SnackbarStore.test.ts

diff --git a/web/src/components/shared/Snackbar.vue b/web/src/components/shared/Snackbar.vue
new file mode 100644
index 00000000..d2cac715
--- /dev/null
+++ b/web/src/components/shared/Snackbar.vue
@@ -0,0 +1,23 @@
+<template>
+  <v-snackbar
+    v-model="snackbarStore.isVisible"
+    :timeout="-1"
+    :color="snackbarStore.color"
+    location="bottom right"
+  >
+    <h3>{{ snackbarStore.title }}</h3>
+    <span>{{ snackbarStore.message }}</span>
+    <template v-slot:actions>
+      <v-icon class="mx-2" :icon="mdiCloseCircle" @click="close" />
+    </template>
+  </v-snackbar>
+</template>
+
+<script setup>
+  import { useSnackbarStore } from '@/stores/SnackBarStore';
+  import { mdiCloseCircle } from '@mdi/js';
+  const snackbarStore = useSnackbarStore();
+  const close = () => {
+    snackbarStore.isVisible = false;
+  };
+</script>
\ No newline at end of file
diff --git a/web/src/services/HttpService.ts b/web/src/services/HttpService.ts
index 03a45c18..98b883d9 100644
--- a/web/src/services/HttpService.ts
+++ b/web/src/services/HttpService.ts
@@ -4,6 +4,7 @@ import axios, {
   InternalAxiosRequestConfig,
 } from 'axios';
 import redirectHandlerService from './RedirectHandlerService';
+import { useSnackbarStore } from '@/stores/SnackBarStore';
 
 export interface IHttpService {
   get<T>(resource: string, queryParams?: Record<string, any>): Promise<T>;
@@ -17,6 +18,7 @@ export interface IHttpService {
 
 export class HttpService implements IHttpService {
   readonly client: AxiosInstance;
+  snackBarStore = useSnackbarStore();
 
   constructor(baseURL: string) {
     this.client = axios.create({
@@ -47,8 +49,16 @@ export class HttpService implements IHttpService {
   private handleAuthError(error: any) {
     console.error(error);
     console.log('User unauthenticated.');
+    // todo: check for a 403 and handle it
     if (error.response && error.response.status === 401) {
       redirectHandlerService.handleUnauthorized(window.location.href);
+    } else {
+      // The user should be notified about unhandled server exceptions.
+      this.snackBarStore.showSnackbar(
+        'Something went wrong, please contact your Administrator.',
+        '#b84157',
+        'Error'
+      );
     }
     return Promise.reject(new Error(error));
   }
diff --git a/web/src/stores/SnackbarStore.ts b/web/src/stores/SnackbarStore.ts
new file mode 100644
index 00000000..820262f5
--- /dev/null
+++ b/web/src/stores/SnackbarStore.ts
@@ -0,0 +1,18 @@
+import { defineStore } from 'pinia';
+import { ref } from 'vue';
+
+export const useSnackbarStore = defineStore('snackbar', () => {
+  const isVisible = ref(false);
+  const message = ref('');
+  const color = ref('success');
+  const title = ref('');
+
+  const showSnackbar = (msg = '', col = 'success', ti = '') => {
+    message.value = msg;
+    color.value = col;
+    title.value = ti;
+    isVisible.value = true;
+  };
+
+  return { isVisible, message, color, showSnackbar, title };
+});
\ No newline at end of file
diff --git a/web/src/stores/index.ts b/web/src/stores/index.ts
index 79dd7b44..4d11ce15 100644
--- a/web/src/stores/index.ts
+++ b/web/src/stores/index.ts
@@ -14,3 +14,4 @@ export { useCommonStore } from './CommonStore';
 export { useCourtFileSearchStore } from './CourtFileSearchStore';
 export { useCourtListStore } from './CourtListStore';
 export { useCriminalFileStore } from './CriminalFileStore';
+export { useSnackbarStore } from './SnackBarStore';
\ No newline at end of file
diff --git a/web/tests/components/shared/Snackbar.test.ts b/web/tests/components/shared/Snackbar.test.ts
new file mode 100644
index 00000000..6c8e14b1
--- /dev/null
+++ b/web/tests/components/shared/Snackbar.test.ts
@@ -0,0 +1,31 @@
+import { mount } from '@vue/test-utils';
+import { useSnackbarStore } from '@/stores/SnackBarStore';
+import { describe, it, expect, beforeEach } from 'vitest';
+import Snackbar from '@/components/shared/Snackbar.vue';
+import { setActivePinia, createPinia } from 'pinia'
+
+describe('Snackbar.vue', () => {
+    let store: ReturnType<typeof useSnackbarStore>;
+
+    beforeEach(() => {
+        setActivePinia(createPinia());
+        store = useSnackbarStore();
+    });
+
+  it('renders snackbar with correct props', () => {
+    store.showSnackbar('Test message', 'error', 'Test title');
+    const wrapper = mount(Snackbar);
+
+    expect(wrapper.find('h3').text()).toBe('Test title');
+    expect(wrapper.text()).toContain('Test message');
+    expect(store.isVisible).toBe(true);
+  });
+
+  it('closes snackbar when close button is clicked', async () => {
+    const wrapper = mount(Snackbar);
+
+    await wrapper.find('v-snackbar__actions v-icon').trigger('click');
+
+    expect(store.isVisible).toBe(false);
+  });
+});
\ No newline at end of file
diff --git a/web/tests/stores/SnackbarStore.test.ts b/web/tests/stores/SnackbarStore.test.ts
new file mode 100644
index 00000000..fe3588f7
--- /dev/null
+++ b/web/tests/stores/SnackbarStore.test.ts
@@ -0,0 +1,35 @@
+import { setActivePinia, createPinia } from 'pinia'
+import { useSnackbarStore } from '@/stores/SnackBarStore';
+import { beforeEach, describe, expect, it } from 'vitest';
+
+describe('SnackBarStore', () => {
+  let store: ReturnType<typeof useSnackbarStore>;
+
+  beforeEach(() => {
+    setActivePinia(createPinia())
+    store = useSnackbarStore();
+  });
+
+  it('initializes with default values', () => {
+    expect(store.isVisible).toBe(false);
+    expect(store.message).toBe('');
+    expect(store.color).toBe('success');
+    expect(store.title).toBe('');
+  });
+
+  it('shows snackbar with given message, color, and title', () => {
+    store.showSnackbar('Test message', 'error', 'Test title');
+    expect(store.isVisible).toBe(true);
+    expect(store.message).toBe('Test message');
+    expect(store.color).toBe('error');
+    expect(store.title).toBe('Test title');
+  });
+
+  it('shows snackbar with default values when no arguments are passed', () => {
+    store.showSnackbar();
+    expect(store.isVisible).toBe(true);
+    expect(store.message).toBe('');
+    expect(store.color).toBe('success');
+    expect(store.title).toBe('');
+  });
+});
\ No newline at end of file

From f521887e4d542b972df6baed393683265dfbe695 Mon Sep 17 00:00:00 2001
From: Josh Traill <josh.traill@quartech.com>
Date: Mon, 13 Jan 2025 11:22:01 -0800
Subject: [PATCH 2/4] fix: incorrect casing

---
 web/src/App.vue                        | 2 ++
 web/src/components/shared/Snackbar.vue | 2 +-
 web/src/services/HttpService.ts        | 2 +-
 web/src/stores/index.ts                | 2 +-
 4 files changed, 5 insertions(+), 3 deletions(-)

diff --git a/web/src/App.vue b/web/src/App.vue
index 35f78f7f..4cad85dc 100644
--- a/web/src/App.vue
+++ b/web/src/App.vue
@@ -26,6 +26,7 @@
       <v-main>
         <router-view />
       </v-main>
+      <snackbar />
     </v-app>
   </v-theme-provider>
 </template>
@@ -34,6 +35,7 @@
   import { mdiAccountCircle } from '@mdi/js';
   import { ref } from 'vue';
   import ProfileOffCanvas from './components/shared/ProfileOffCanvas.vue';
+  import Snackbar from './components/shared/Snackbar.vue';
   import { useThemeStore } from './stores/ThemeStore';
 
   const themeStore = useThemeStore();
diff --git a/web/src/components/shared/Snackbar.vue b/web/src/components/shared/Snackbar.vue
index d2cac715..13c0b7bc 100644
--- a/web/src/components/shared/Snackbar.vue
+++ b/web/src/components/shared/Snackbar.vue
@@ -14,7 +14,7 @@
 </template>
 
 <script setup>
-  import { useSnackbarStore } from '@/stores/SnackBarStore';
+  import { useSnackbarStore } from '@/stores/SnackbarStore';
   import { mdiCloseCircle } from '@mdi/js';
   const snackbarStore = useSnackbarStore();
   const close = () => {
diff --git a/web/src/services/HttpService.ts b/web/src/services/HttpService.ts
index 98b883d9..e000498b 100644
--- a/web/src/services/HttpService.ts
+++ b/web/src/services/HttpService.ts
@@ -1,10 +1,10 @@
+import { useSnackbarStore } from '@/stores/SnackbarStore';
 import axios, {
   AxiosInstance,
   AxiosRequestConfig,
   InternalAxiosRequestConfig,
 } from 'axios';
 import redirectHandlerService from './RedirectHandlerService';
-import { useSnackbarStore } from '@/stores/SnackBarStore';
 
 export interface IHttpService {
   get<T>(resource: string, queryParams?: Record<string, any>): Promise<T>;
diff --git a/web/src/stores/index.ts b/web/src/stores/index.ts
index 4d11ce15..c6c90970 100644
--- a/web/src/stores/index.ts
+++ b/web/src/stores/index.ts
@@ -14,4 +14,4 @@ export { useCommonStore } from './CommonStore';
 export { useCourtFileSearchStore } from './CourtFileSearchStore';
 export { useCourtListStore } from './CourtListStore';
 export { useCriminalFileStore } from './CriminalFileStore';
-export { useSnackbarStore } from './SnackBarStore';
\ No newline at end of file
+export { useSnackbarStore } from './SnackbarStore';
\ No newline at end of file

From 224897f9a06b4fc251f43bc37d7abe6e717bba8d Mon Sep 17 00:00:00 2001
From: Josh Traill <josh.traill@quartech.com>
Date: Mon, 13 Jan 2025 11:25:35 -0800
Subject: [PATCH 3/4] test: fix casing

---
 web/tests/components/shared/Snackbar.test.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/web/tests/components/shared/Snackbar.test.ts b/web/tests/components/shared/Snackbar.test.ts
index 6c8e14b1..13edb7f7 100644
--- a/web/tests/components/shared/Snackbar.test.ts
+++ b/web/tests/components/shared/Snackbar.test.ts
@@ -1,5 +1,5 @@
 import { mount } from '@vue/test-utils';
-import { useSnackbarStore } from '@/stores/SnackBarStore';
+import { useSnackbarStore } from '@/stores/SnackbarStore';
 import { describe, it, expect, beforeEach } from 'vitest';
 import Snackbar from '@/components/shared/Snackbar.vue';
 import { setActivePinia, createPinia } from 'pinia'

From 154a98439f25cf1e05a7288ad60082a225fbcd91 Mon Sep 17 00:00:00 2001
From: Josh Traill <josh.traill@quartech.com>
Date: Mon, 13 Jan 2025 11:26:18 -0800
Subject: [PATCH 4/4] test: fix casing

---
 web/tests/stores/SnackbarStore.test.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/web/tests/stores/SnackbarStore.test.ts b/web/tests/stores/SnackbarStore.test.ts
index fe3588f7..67ee2707 100644
--- a/web/tests/stores/SnackbarStore.test.ts
+++ b/web/tests/stores/SnackbarStore.test.ts
@@ -1,5 +1,5 @@
 import { setActivePinia, createPinia } from 'pinia'
-import { useSnackbarStore } from '@/stores/SnackBarStore';
+import { useSnackbarStore } from '@/stores/SnackbarStore';
 import { beforeEach, describe, expect, it } from 'vitest';
 
 describe('SnackBarStore', () => {