-
404
- PAGE NOT FOUND
+
+
+
+
+
+
+
404
+
{{
+ $t('error.not_found_header')
+ }}
+
+
-
-
- {{ $t('notFoundPage.pageText') }}
-
-
+
+
+
{{ $t('error.not_found_description') }}
+
+
+
+
+
+
+
diff --git a/src/pages/help-page.vue b/src/pages/help-page.vue
index e69de29..4e3de2a 100644
--- a/src/pages/help-page.vue
+++ b/src/pages/help-page.vue
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/pages/login-page.vue b/src/pages/login-page.vue
new file mode 100644
index 0000000..3877f55
--- /dev/null
+++ b/src/pages/login-page.vue
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/pages/profile-page.vue b/src/pages/profile-page.vue
index f98522b..0598b8a 100644
--- a/src/pages/profile-page.vue
+++ b/src/pages/profile-page.vue
@@ -1,22 +1,31 @@
-
-
-
+
\ No newline at end of file
diff --git a/src/pages/supervisor-crew-page.vue b/src/pages/supervisor-crew-page.vue
deleted file mode 100644
index 5fe11a6..0000000
--- a/src/pages/supervisor-crew-page.vue
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
-
-
- {{ $t('employee_list.page_header') }}
-
-
-
-
\ No newline at end of file
diff --git a/src/pages/test-page.vue b/src/pages/test-page.vue
deleted file mode 100644
index d62f747..0000000
--- a/src/pages/test-page.vue
+++ /dev/null
@@ -1,70 +0,0 @@
-
-
-
-
-
-
-
- Welcome to App Targo!
-
-
-
-
- Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium,
- totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta
- sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia
- consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui
- dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora
- incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum
- exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem
- vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum
- qui
- dolorem eum fugiat quo voluptas nulla pariatur?
-
-
-
- At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum
- deleniti atque corrupti quos dolores et quas molestias excepturi sint occaecati cupiditate non
- provident, similique sunt in culpa qui officia deserunt mollitia animi, id est laborum et dolorum fuga.
- Et harum quidem rerum facilis est et expedita distinctio. Nam libero tempore, cum soluta nobis est
- eligendi optio cumque nihil impedit quo minus id quod maxime placeat facere possimus, omnis voluptas
- assumenda est, omnis dolor repellendus. Temporibus autem quibusdam et aut officiis debitis aut rerum
- necessitatibus saepe eveniet ut et voluptates repudiandae sint et molestiae non recusandae. Itaque earum
- rerum hic tenetur a sapiente delectus, ut aut reiciendis voluptatibus maiores alias consequatur aut
- perferendis doloribus asperiores repellat.
-
-
-
- Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et
- dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip
- ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu
- fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia
- deserunt mollit anim id est laborum.
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/src/pages/timesheet-approval-page.vue b/src/pages/timesheet-approval-page.vue
index 7ef554f..5677750 100644
--- a/src/pages/timesheet-approval-page.vue
+++ b/src/pages/timesheet-approval-page.vue
@@ -1,46 +1,65 @@
-
-
-
-
-
+
+
+
\ No newline at end of file
diff --git a/src/pages/timesheet-page.vue b/src/pages/timesheet-page.vue
index a91997a..38b8180 100644
--- a/src/pages/timesheet-page.vue
+++ b/src/pages/timesheet-page.vue
@@ -1,33 +1,24 @@
-
-
-
-
-
+
+
+
\ No newline at end of file
diff --git a/src/router/index.ts b/src/router/index.ts
index c1afbd1..0ddcc7d 100644
--- a/src/router/index.ts
+++ b/src/router/index.ts
@@ -2,6 +2,8 @@ import { defineRouter } from '#q-app/wrappers';
import { createMemoryHistory, createRouter, createWebHashHistory, createWebHistory, } from 'vue-router';
import routes from './routes';
import { useAuthStore } from 'src/stores/auth-store';
+import { RouteNames } from 'src/router/router-constants';
+import type { UserModuleAccess } from 'src/modules/shared/models/user.models';
/*
* If not building with SSR mode, you can
@@ -13,27 +15,34 @@ import { useAuthStore } from 'src/stores/auth-store';
*/
export default defineRouter(function (/* { store, ssrContext } */) {
- const createHistory = process.env.SERVER
- ? createMemoryHistory
- : (process.env.VUE_ROUTER_MODE === 'history' ? createWebHistory : createWebHashHistory);
+ const createHistory = process.env.SERVER
+ ? createMemoryHistory
+ : (process.env.VUE_ROUTER_MODE === 'history' ? createWebHistory : createWebHashHistory);
- const Router = createRouter({
- scrollBehavior: () => ({ left: 0, top: 0 }),
- routes,
+ const Router = createRouter({
+ scrollBehavior: () => ({ left: 0, top: 0 }),
+ routes,
- // Leave this as is and make changes in quasar.conf.js instead!
- // quasar.conf.js -> build -> vueRouterMode
- // quasar.conf.js -> build -> publicPath
- history: createHistory(process.env.VUE_ROUTER_BASE),
- });
+ // Leave this as is and make changes in quasar.conf.js instead!
+ // quasar.conf.js -> build -> vueRouterMode
+ // quasar.conf.js -> build -> publicPath
+ history: createHistory(process.env.VUE_ROUTER_BASE),
+ });
- Router.beforeEach((destinationPage) => {
- const authStore = useAuthStore();
+ Router.beforeEach(async (destination_page) => {
+ const auth_store = useAuthStore();
+ const result = await auth_store.getProfile() ?? { status: 400, message: 'unknown error occured' };
- if (destinationPage.meta.requiresAuth && !authStore.isAuthorizedUser) {
- return { name: 'login' };
- }
- })
+ if (destination_page.meta.requires_auth && !auth_store.user || (result.status >= 400 && destination_page.name !== RouteNames.LOGIN)) {
+ console.error('no user account found');
+ return { name: 'login' };
+ }
+
+ if (destination_page.meta.required_module && auth_store.user) {
+ if (!auth_store.user.user_module_access.includes(destination_page.meta.required_module as UserModuleAccess))
+ return {name: 'error'};
+ }
+ })
- return Router;
+ return Router;
});
diff --git a/src/router/router-constants.ts b/src/router/router-constants.ts
index b24aa80..2f1bc70 100644
--- a/src/router/router-constants.ts
+++ b/src/router/router-constants.ts
@@ -1,10 +1,12 @@
export enum RouteNames {
- /* eslint-disable */
LOGIN = 'login',
LOGIN_SUCCESS = 'login-success',
- DASHBOARD = 'dashboard',
+ DASHBOARD = '/',
TIMESHEET_APPROVALS = 'timesheet-approvals',
- EMPLOYEE_LIST = 'employee-list',
- PROFILE = 'user/profile',
- TIMESHEET_TEMP = 'timesheet-temp'
+ EMPLOYEE_LIST = 'employees',
+ PROFILE = 'profile',
+ TIMESHEET = 'timesheet',
+ HELP = 'help',
+
+ ERROR = 'error',
}
\ No newline at end of file
diff --git a/src/router/routes.ts b/src/router/routes.ts
index 7a21d96..5cc7111 100644
--- a/src/router/routes.ts
+++ b/src/router/routes.ts
@@ -1,60 +1,72 @@
import type { RouteRecordRaw } from 'vue-router';
import { RouteNames } from './router-constants';
+import { ModuleNames } from 'src/modules/shared/models/user.models';
const routes: RouteRecordRaw[] = [
{
- path: '/',
+ path: '/',
component: () => import('src/layouts/main-layout.vue'),
- meta: { requiresAuth: true },
+ meta: { requires_auth: true },
children: [
- {
- path: '',
+ {
+ path: '',
name: RouteNames.DASHBOARD,
- component: () => import('src/pages/test-page.vue'),
+ component: () => import('src/pages/dashboard-page.vue'),
+ meta: { required_module: ModuleNames.DASHBOARD }
},
{
path: 'timesheet-approvals',
name: RouteNames.TIMESHEET_APPROVALS,
component: () => import('src/pages/timesheet-approval-page.vue'),
+ meta: { required_module: ModuleNames.TIMESHEETS_APPROVAL }
},
{
path: 'employees',
name: RouteNames.EMPLOYEE_LIST,
- component: () => import('src/pages/supervisor-crew-page.vue'),
+ component: () => import('src/pages/employee-list-page.vue'),
+ meta: { required_module: ModuleNames.EMPLOYEE_LIST }
},
{
- path: 'timesheet-temp',
- name: RouteNames.TIMESHEET_TEMP,
- component: () => import('src/pages/timesheet-page.vue')
+ path: 'timesheet',
+ name: RouteNames.TIMESHEET,
+ component: () => import('src/pages/timesheet-page.vue'),
+ meta: { required_module: ModuleNames.TIMESHEETS },
},
{
- path: 'user/profile',
+ path: 'profile',
name: RouteNames.PROFILE,
component: () => import('src/pages/profile-page.vue'),
+ meta: { required_module: ModuleNames.PERSONAL_PROFILE },
+ },
+ {
+ path: 'help',
+ name: RouteNames.HELP,
+ component: () => import('src/pages/help-page.vue'),
},
],
},
{
- path: '/v1/login',
+ path: '/v1/login',
name: RouteNames.LOGIN,
- component: () => import('src/modules/auth/pages/auth-login.vue'),
- meta: { requiresAuth: false },
+ component: () => import('src/pages/login-page.vue'),
+ meta: { requires_auth: false },
},
{
- path: '/login-success',
+ path: '/login-success',
name: RouteNames.LOGIN_SUCCESS,
component: () => import('src/modules/auth/pages/auth-login-popup-success.vue'),
- meta: { requiresAuth: false },
+ meta: { requires_auth: false },
},
// Always leave this as last one,
// but you can also remove it
{
path: '/:catchAll(.*)*',
+ name: RouteNames.ERROR,
component: () => import('src/pages/error-page.vue'),
- meta: { requiresAuth: false },
+ meta: { requires_auth: false },
},
];
diff --git a/src/stores/auth-store.ts b/src/stores/auth-store.ts
index 603876f..3eecc6f 100644
--- a/src/stores/auth-store.ts
+++ b/src/stores/auth-store.ts
@@ -1,47 +1,73 @@
-import { computed, ref } from "vue";
+import { ref } from "vue";
+import { Notify } from "quasar";
import { defineStore } from "pinia";
import { AuthService } from "../modules/auth/services/services-auth";
+import { useRouter } from "vue-router";
import type { User } from "src/modules/shared/models/user.models";
-export type CompanyRole = 'guest' | 'supervisor' | 'accounting' | 'human_resources' | 'employee';
+export const useAuthStore = defineStore('auth', () => {
+ const user = ref
();
+ const authError = ref("");
+ const router = useRouter();
-const TestUsers: Record = {
- guest: { firstName: 'Unknown', lastName: 'Unknown', email: 'guest@guest.com', role: 'guest' },
- supervisor: { firstName: 'User', lastName: 'Test', email: 'user@targointernet.com', role: 'supervisor' },
- accounting: { firstName: 'Robin', lastName: 'Clark', email: 'user5@example.test', role: 'supervisor' },
- human_resources: { firstName: 'Robin', lastName: 'Clark', email: 'user5@example.test', role: 'supervisor' },
- employee: { firstName: 'Robin', lastName: 'Clark', email: 'user5@example.test', role: 'supervisor' },
-}
+ const login = () => {
+ //TODO: manage customer login process
+ };
-export const useAuthStore = defineStore('auth', () => {
- const user = ref(TestUsers.guest);
- const authError = ref("");
- const isAuthorizedUser = computed(() => user.value.role !== 'guest');
+ const oidcLogin = () => {
+ window.addEventListener('message', (event) => {
+ void handleAuthMessage(event);
+ });
- const login = () => {
- //TODO: manage customer login process
- };
+ const oidc_popup = window.open(`${import.meta.env.VITE_TARGO_BACKEND_URL}auth/v1/login`, 'authPopup', 'width=600,height=800');
- const oidcLogin = () => {
- const oidcPopup = AuthService.oidcLogin();
- if (!oidcPopup) {
- authError.value = "You have popups blocked on this website!";
+ if (!oidc_popup)
+ Notify.create({
+ message: "You have popups blocked on this website!",
+ color: 'negative',
+ textColor: 'white',
+ });
+ };
+
+ const logout = () => {
+ user.value = undefined;
+ };
+
+ const handleAuthMessage = async (event: MessageEvent) => {
+ if (event.data.type === 'authSuccess') {
+ try {
+ await getProfile();
+ await router.push('/');
+ } catch (error) {
+ console.error('failed to login: ', error);
+ }
+ } else {
+ Notify.create({
+ message: "You have popups blocked on this website!",
+ color: 'negative',
+ textColor: 'white',
+ });
+ }
+ };
+
+ const getProfile = async (): Promise<{ status: number, message: string }> => {
+ try {
+ const new_user = await AuthService.getProfile();
+ user.value = new_user;
+ return { status: 200, message: 'profile retrieved successfully' };
+ } catch (error) {
+ console.error('error while retrieving profile: ', error);
+ }
+ return { status: 400, message: 'unknown error occured' };
}
- };
- const logout = () => {
- user.value = TestUsers.guest;
- };
-
- const setUser = (bypassRole: string) => {
- if (bypassRole in TestUsers) {
- user.value = TestUsers[bypassRole as CompanyRole];
- }
- else {
- user.value = TestUsers.guest;
- }
- };
-
- return { user, authError, isAuthorizedUser, login, oidcLogin, logout, setUser };
+ return {
+ user,
+ authError,
+ login,
+ oidcLogin,
+ logout,
+ getProfile
+ };
});
diff --git a/src/stores/employee-store.ts b/src/stores/employee-store.ts
index 1010364..7a243f9 100644
--- a/src/stores/employee-store.ts
+++ b/src/stores/employee-store.ts
@@ -1,41 +1,102 @@
+/* eslint-disable */
import { ref } from "vue";
import { defineStore } from "pinia";
-import { EmployeeListService } from "src/modules/employee-list/services/services-employee-list";
-import { default_employee_profile, type EmployeeProfile } from "src/modules/employee-list/types/employee-profile-interface";
-import type { EmployeeListTableItem } from "src/modules/employee-list/types/employee-list-table-interface";
+import { EmployeeListService } from "src/modules/employee-list/services/employee-list-service";
+import { EmployeeProfile } from "src/modules/employee-list/models/employee-profile.models";
+import { Notify } from "quasar";
-export const useEmployeeStore = defineStore('employee', () => {
- const employee = ref( default_employee_profile );
- const employeeList = ref([]);
- const isShowingEmployeeAddModifyWindow = ref(false);
- const isLoadingEmployeeProfile = ref(false);
- const isLoadingEmployeeList = ref(false);
+export const useEmployeeStore = defineStore('employee', () => {
+ const employee = ref(new EmployeeProfile);
+ const employee_list = ref([]);
+ const is_add_modify_dialog_open = ref(false);
+ const management_mode = ref<'modify_employee' | 'add_employee'>('add_employee');
+ const is_loading = ref(false);
- const getEmployeeList = async () => {
- isLoadingEmployeeList.value = true;
- try {
- const response = await EmployeeListService.getEmployeeList();
- employeeList.value = response;
- } catch (error) {
- console.error("Ran into an error fetching employee list: ", error);
- //TODO: trigger an alert window with an error message here!
+ const openAddModifyDialog = async (employee_email?: string) => {
+
+ if (employee_email === undefined) {
+ management_mode.value = 'add_employee'
+ employee.value = new EmployeeProfile();
+ is_add_modify_dialog_open.value = true;
+ return;
}
- isLoadingEmployeeList.value = false;
+
+ is_loading.value = true;
+ management_mode.value = 'modify_employee';
+ await getEmployeeDetails(employee_email);
+ is_loading.value = false;
+ is_add_modify_dialog_open.value = true;
+ }
+
+ const closeAddModifyDialog = () => {
+ is_add_modify_dialog_open.value = false;
+ management_mode.value = 'add_employee';
+ employee.value = new EmployeeProfile;
};
- const getEmployeeDetails = async (email: string) => {
- isLoadingEmployeeProfile.value = true;
+ const getEmployeeList = async (): Promise => {
try {
- const response = await EmployeeListService.getEmployeeDetails(email);
- employee.value = response;
+ const response = await EmployeeListService.getEmployeeList();
+ if (response.success && response.data) employee_list.value = response.data;
+ return response.success;
+ } catch (error) {
+ console.error("Ran into an error fetching employee list: ", error);
+ return false;
+ }
+ };
+
+ const getEmployeeDetails = async (email?: string): Promise => {
+ try {
+ if (email === undefined) {
+ const response = await EmployeeListService.getEmployeeDetails();
+ if (response.success && response.data) employee.value = response.data;
+ return response.success;
+ } else {
+ const response = await EmployeeListService.getEmployeeDetailsWithEmployeeEmail(email);
+ if (response.success && response.data) employee.value = response.data;
+ return response.success;
+ }
} catch (error) {
console.error('There was an error retrieving employee info: ', error);
//TODO: trigger an alert window with an error message here!
}
- isLoadingEmployeeProfile.value = false;
+
+ return false;
};
- return { employee, employeeList, isShowingEmployeeAddModifyWindow, isLoadingEmployeeList, isLoadingEmployeeProfile, getEmployeeList, getEmployeeDetails };
+ const createOrUpdateEmployee = async (profile: EmployeeProfile) => {
+ let response;
+
+ if (management_mode.value === 'add_employee') {
+ const { birth_date, last_work_day, ...create_payload} = profile;
+ response = await EmployeeListService.createNewEmployee(create_payload);
+ } else {
+
+ response = await EmployeeListService.updateEmployee(profile);
+ }
+
+ closeAddModifyDialog();
+
+ if (response.success) await getEmployeeList();
+ else {
+ Notify.create({
+ message: 'failed to update or create employee',
+ color: 'negative',
+ })}
+ };
+
+ return {
+ employee,
+ employee_list,
+ is_add_modify_dialog_open,
+ management_mode,
+ is_loading,
+ getEmployeeList,
+ getEmployeeDetails,
+ openAddModifyDialog,
+ closeAddModifyDialog,
+ createOrUpdateEmployee,
+ };
});
diff --git a/src/stores/expense-store.ts b/src/stores/expense-store.ts
index b9bbfbd..1fdcf5c 100644
--- a/src/stores/expense-store.ts
+++ b/src/stores/expense-store.ts
@@ -1,109 +1,66 @@
+import { date } from "quasar";
import { computed, ref } from "vue";
import { defineStore } from "pinia";
import { useTimesheetStore } from "src/stores/timesheet-store";
-import { default_expense, default_pay_period_expenses, type UpsertExpense, type Expense, type PayPeriodExpenses } from "src/modules/timesheets/models/expense.models";
-import { timesheetService } from "src/modules/timesheets/services/timesheet-service";
-import { ExpensesApiError, type GenericApiError } from "src/modules/timesheets/models/expense.validation";
-import { computeExpenseTotals } from "src/modules/timesheets/utils/expense.util";
-import type { UpsertAction } from "src/modules/timesheets/models/shift.models";
-
-
+import { Expense } from "src/modules/timesheets/models/expense.models";
+import { ExpenseService } from "src/modules/timesheets/services/expense-service";
export const useExpensesStore = defineStore('expenses', () => {
const timesheet_store = useTimesheetStore();
const is_open = ref(false);
const is_loading = ref(false);
- const mode = ref('create');
- const pay_period_expenses = ref(default_pay_period_expenses);
- const pay_period_expenses_totals = computed(() => computeExpenseTotals(pay_period_expenses.value.expenses))
- const current_expense = ref(default_expense);
- const initial_expense = ref(default_expense);
- const error = ref(null);
+ const is_showing_create_form = ref(false);
+ const mode = ref<'create' | 'update' | 'delete'>('create');
+ const current_expense = ref(new Expense(date.formatDate(new Date(), 'YYYY-MM-DD')));
+ const initial_expense = ref(new Expense(date.formatDate(new Date(), 'YYYY-MM-DD')));
+ const is_save_disabled = computed(() => JSON.stringify(current_expense.value) === JSON.stringify(initial_expense.value))
- // const setErrorFrom = (err: unknown) => {
- // const e = err as any;
- // error.value = e?.message || 'Unknown error';
- // };
-
- const open = async (employee_email: string): Promise => {
+ const open = (): void => {
is_open.value = true;
- is_loading.value = true;
- error.value = null;
- current_expense.value = default_expense;
- initial_expense.value = default_expense;
-
- await getPayPeriodExpensesByEmployeeEmail(employee_email);
- is_loading.value = false;
+ is_showing_create_form.value = false;
+ if (timesheet_store.pay_period !== undefined) {
+ current_expense.value = new Expense(date.formatDate(new Date(), 'YYYY-MM-DD'));
+ initial_expense.value = new Expense(date.formatDate(new Date(), 'YYYY-MM-DD'));
+ }
+ mode.value = 'create';
}
const close = () => {
- error.value = null;
is_open.value = false;
+ is_showing_create_form.value = false;
};
- const getPayPeriodExpensesByEmployeeEmail = async (employee_email: string): Promise => {
- is_loading.value = true;
- error.value = null;
-
+ const upsertExpense = async (expense: Expense, email?: string): Promise => {
try {
- const expenses = await timesheetService.getExpensesByPayPeriodAndEmployeeEmail(
- encodeURIComponent(employee_email),
- encodeURIComponent(timesheet_store.pay_period.pay_year),
- encodeURIComponent(timesheet_store.pay_period.pay_period_no),
- );
- pay_period_expenses.value = expenses;
- } catch (err: unknown) {
- if (typeof err === 'object') {
- const error = err as GenericApiError;
- const status_code: number = error.status_code ?? 500;
- // const data = error.context ?? '';
- // error.value = data.message || data.error || err.message;
-
- throw new ExpensesApiError({
- status_code,
- // error_code: data.error_code,
- // message: data.message || data.error || err.message,
- // context: data.context,
- });
+ if (expense.id < 0) {
+ const data = await ExpenseService.createExpense(expense);
+ return data.success;
}
-
- } finally {
- is_loading.value = false;
- }
- };
-
- const upsertOrDeleteExpensesByEmployeeEmail = async (employee_email: string, date: string, expense: UpsertExpense): Promise => {
- is_loading.value = true;
- error.value = null;
-
- try {
- const updated_expenses = await timesheetService.upsertOrDeleteExpensesByPayPeriodAndEmployeeEmail(
- encodeURIComponent(employee_email),
- encodeURIComponent(date),
- expense,
- );
- console.log('updated expenses received: ', updated_expenses)
- pay_period_expenses.value.expenses = updated_expenses;
+ const data = await ExpenseService.updateExpense(expense, email);
+ return data.success;
} catch (err) {
// setErrorFrom(err);
- console.log('error doing some expense thing: ', err)
- } finally {
- is_loading.value = false;
+ console.error(err);
+ return false;
}
};
+ const deleteExpenseById = async (expense_id: number): Promise => {
+ const data = await ExpenseService.deleteExpenseById(expense_id);
+ return data.success;
+ }
+
return {
is_open,
is_loading,
+ is_showing_create_form,
mode,
- pay_period_expenses,
- pay_period_expenses_totals,
current_expense,
initial_expense,
- error,
+ is_save_disabled,
open,
- getPayPeriodExpensesByEmployeeEmail,
- upsertOrDeleteExpensesByEmployeeEmail,
+ upsertExpense,
+ deleteExpenseById,
close,
};
});
\ No newline at end of file
diff --git a/src/stores/help-store.ts b/src/stores/help-store.ts
new file mode 100644
index 0000000..df89732
--- /dev/null
+++ b/src/stores/help-store.ts
@@ -0,0 +1,37 @@
+import { defineStore } from "pinia";
+import { ref } from "vue";
+import { help_module_details } from "src/modules/help/models/help-module.model";
+import { useAuthStore } from "src/stores/auth-store";
+import type { HelpModuleOptions } from "src/modules/help/models/help-module.model";
+import type { UserModuleAccess } from "src/modules/shared/models/user.models";
+
+
+
+export const useHelpStore = defineStore('help', () => {
+ const is_loading = ref(false);
+ const user_modules = ref>();
+ const auth_store = useAuthStore();
+
+ const getFilteredModule = () => {
+ if(!auth_store.user) return;
+
+ const entries = auth_store.user.user_module_access
+ .map((key) => {
+ const options = help_module_details[key];
+ return options ? ([key, options] as const) : null;
+ })
+ .filter(
+ (entry): entry is readonly [UserModuleAccess, HelpModuleOptions[]] =>
+ entry !== null
+ );
+
+ user_modules.value = Object.fromEntries(entries) as Record;
+ }
+
+ return {
+ is_loading,
+ user_modules,
+ help_module_details,
+ getFilteredModule,
+ };
+});
\ No newline at end of file
diff --git a/src/stores/schedule-presets.store.ts b/src/stores/schedule-presets.store.ts
new file mode 100644
index 0000000..d924f16
--- /dev/null
+++ b/src/stores/schedule-presets.store.ts
@@ -0,0 +1,93 @@
+import { ref } from "vue";
+import { defineStore } from "pinia";
+import { SchedulePresetsService } from "src/modules/employee-list/services/schedule-presets-service";
+import { type PresetManagerMode, SchedulePreset, SchedulePresetFrontend } from "src/modules/employee-list/models/schedule-presets.models";
+
+
+export const useSchedulePresetsStore = defineStore('schedule_presets_store', () => {
+ const schedule_presets = ref([new SchedulePreset]);
+ const current_schedule_preset = ref(new SchedulePresetFrontend);
+ const schedule_preset_dialog_mode = ref('create');
+ const is_manager_open = ref(false);
+
+ const openSchedulePresetManager = (preset_id: number) => {
+ if (preset_id === -1)
+ current_schedule_preset.value = new SchedulePresetFrontend;
+ else if (schedule_preset_dialog_mode.value === 'copy') {
+ const preset = schedule_presets.value.find(preset => preset.id === preset_id)!;
+ const copied_preset = new SchedulePresetFrontend(preset);
+ copied_preset.id = -1;
+ copied_preset.name = "";
+ current_schedule_preset.value = copied_preset;
+ }
+ else
+ setCurrentSchedulePreset(preset_id);
+
+ is_manager_open.value = true;
+ };
+
+ const setCurrentSchedulePreset = (preset_id: number) => {
+ if (preset_id === -1) {
+ current_schedule_preset.value = new SchedulePresetFrontend;
+ return;
+ }
+ current_schedule_preset.value = new SchedulePresetFrontend(schedule_presets.value.find(preset => preset.id === preset_id))
+ };
+
+ const createSchedulePreset = async (preset: SchedulePreset): Promise => {
+ try {
+ const response = await SchedulePresetsService.createSchedulePresets(preset);
+ return response.success;
+ } catch (error) {
+ console.error('DEV ERROR || error while creating schedule preset: ', error);
+ return false;
+ }
+ };
+
+ const updateSchedulePreset = async (preset: SchedulePreset): Promise => {
+ try {
+ const response = await SchedulePresetsService.updateSchedulePresets(preset);
+ return response.success;
+ } catch (error) {
+ console.error('DEV ERROR || error while updating schedule preset: ', error);
+ return false;
+ }
+
+ };
+
+ const deleteSchedulePreset = async (preset_id: number): Promise => {
+ try {
+ const response = await SchedulePresetsService.deleteSchedulePresets(preset_id);
+ return response.success;
+ } catch (error) {
+ console.error('DEV ERROR || error while deleting schedule preset: ', error);
+ return false;
+ }
+ };
+
+ const findSchedulePresetList = async (): Promise => {
+ try {
+ const response = await SchedulePresetsService.getSchedulePresetsList();
+ if (response.success && response.data) schedule_presets.value = response.data;
+
+ return response.success;
+ } catch (error) {
+ console.error('DEV ERROR || error while searching for schedule presets: ', error);
+
+ return false
+ }
+ };
+
+ return {
+ schedule_presets,
+ current_schedule_preset,
+ schedule_preset_dialog_mode,
+ is_manager_open,
+ setCurrentSchedulePreset,
+ openSchedulePresetManager,
+ createSchedulePreset,
+ updateSchedulePreset,
+ deleteSchedulePreset,
+ findSchedulePresetList,
+ }
+})
\ No newline at end of file
diff --git a/src/stores/shift-store.ts b/src/stores/shift-store.ts
index 1dac9be..abcd25f 100644
--- a/src/stores/shift-store.ts
+++ b/src/stores/shift-store.ts
@@ -1,80 +1,71 @@
-import { ref } from "vue";
-import { defineStore } from "pinia";
-import { unwrapAndClone } from "src/utils/unwrap-and-clone";
-import { timesheetService } from "src/modules/timesheets/services/timesheet-service";
-import { useTimesheetStore } from "src/stores/timesheet-store";
-import { default_shift, type UpsertAction, type Shift, type UpsertShift } from "src/modules/timesheets/models/shift.models";
-
-export const useShiftStore = defineStore('shift', () => {
- const is_open = ref(false);
- const mode = ref('create');
- const date_iso = ref('');
- const current_shift = ref(default_shift);
- const initial_shift = ref(default_shift);
-
- const timesheet_store = useTimesheetStore();
-
- const open = (next_mode: UpsertAction, date: string, current: Shift, initial: Shift) => {
- mode.value = next_mode;
- date_iso.value = date;
- current_shift.value = current; // new shift
- initial_shift.value = initial; // old shift
- is_open.value = true;
- };
-
- const openCreate = (date: string) => {
- open('create', date, default_shift, default_shift);
- };
-
- const openUpdate = (date: string, shift: Shift) => {
- open('update', date, shift, unwrapAndClone(shift));
- };
-
- const openDelete = (date: string, shift: Shift) => {
- open('delete', date, default_shift, shift);
- }
-
- const close = () => {
- is_open.value = false;
- mode.value = 'create';
- date_iso.value = '';
- current_shift.value = default_shift;
- initial_shift.value = default_shift;
- };
-
- const upsertOrDeleteShiftByEmployeeEmail = async (employee_email: string, upsert_shift: UpsertShift) => {
- const encoded_email = encodeURIComponent(employee_email);
- const encoded_date = encodeURIComponent(current_shift.value.date);
-
- try {
- const result = await timesheetService.upsertOrDeleteShiftsByDateAndEmployeeEmail(encoded_email, [ upsert_shift, ], encoded_date);
- timesheet_store.pay_period_details = result;
- } catch (err) {
- console.log('error doing thing: ', err)
- // const status_code: number = err?.response?.status ?? 500;
- // const data = err?.response?.data ?? {};
- // throw new GenericApiError({
- // status_code,
- // error_code: data.error_code,
- // message: data.message || data.error || err.message,
- // context: data.context,
- // });
- } finally {
- close();
- }
- }
-
-
- return {
- is_open,
- mode,
- date_iso,
- current_shift,
- initial_shift,
- openCreate,
- openUpdate,
- openDelete,
- close,
- upsertOrDeleteShiftByEmployeeEmail,
- };
+/* eslint-disable */
+import { ref } from "vue";
+import { Notify } from "quasar";
+import { defineStore } from "pinia";
+import { ShiftService } from "src/modules/timesheets/services/shift-service";
+import { useTimesheetStore } from "src/stores/timesheet-store";
+
+export const useShiftStore = defineStore('shift_store', () => {
+ const timesheet_store = useTimesheetStore();
+ const shift_errors = ref([]);
+
+ const deleteShiftById = async (shift_id: number): Promise => {
+ try {
+ await ShiftService.deleteShiftById(shift_id);
+ return true;
+ } catch (error) {
+ console.error('DEV ERROR || error while deleting shift: ', error);
+ return false;
+ }
+ };
+
+ const createNewShifts = async (): Promise => {
+ if (timesheet_store.timesheets === undefined) return false;
+
+ try {
+ const days = timesheet_store.timesheets.flatMap(week => week.days);
+ const new_shifts = days.flatMap(day => day.shifts).filter(shift => shift.id < 0);
+
+ if (new_shifts?.length > 0) {
+ const response = await ShiftService.createNewShifts(new_shifts);
+ if (response.success) {
+ return true;
+ }
+ else { shift_errors.value.push(response.error!) }
+ }
+ return false;
+ } catch (error) {
+ Notify.create('Error creating new shifts');
+ return false;
+ }
+ };
+
+ const updateShifts = async (): Promise => {
+ if (timesheet_store.timesheets === undefined) return false;
+
+ try {
+ const existing_shifts = timesheet_store.timesheets.flatMap(week => week.days).flatMap(day => day.shifts).filter(shift => shift.id > 0);
+
+ if (existing_shifts?.length > 0) {
+ const response = await ShiftService.updateShifts(existing_shifts);
+
+ if (response.success) {
+ return true;
+ }
+ }
+
+ Notify.create('No shifts to update')
+ return false;
+ } catch (error) {
+ Notify.create('Error updating shifts');
+ return false;
+ }
+ }
+
+ return {
+ shift_errors,
+ deleteShiftById,
+ createNewShifts,
+ updateShifts,
+ }
})
\ No newline at end of file
diff --git a/src/stores/timesheet-store.ts b/src/stores/timesheet-store.ts
index b93a33d..64d9bad 100644
--- a/src/stores/timesheet-store.ts
+++ b/src/stores/timesheet-store.ts
@@ -1,118 +1,190 @@
-import { defineStore } from 'pinia';
import { computed, ref } from 'vue';
-import { withLoading } from 'src/utils/store-helpers';
+import { defineStore } from 'pinia';
+import { unwrapAndClone } from 'src/utils/unwrap-and-clone';
import { timesheetApprovalService } from 'src/modules/timesheet-approval/services/timesheet-approval-service';
import { timesheetService } from 'src/modules/timesheets/services/timesheet-service';
-import { default_pay_period_overview, type PayPeriodOverview } from "src/modules/timesheet-approval/models/pay-period-overview.models";
-import { default_pay_period, type PayPeriod } from 'src/modules/shared/models/pay-period.models';
-import { default_pay_period_details, type PayPeriodDetails } from 'src/modules/timesheets/models/pay-period-details.models';
+import type { PayPeriodOverviewResponse, TimesheetApprovalOverview } from "src/modules/timesheet-approval/models/timesheet-overview.models";
+import type { PayPeriod } from 'src/modules/shared/models/pay-period.models';
+import type { Timesheet } from 'src/modules/timesheets/models/timesheet.models';
import type { TimesheetApprovalCSVReportFilters } from 'src/modules/timesheet-approval/models/timesheet-approval-csv-report.models';
+
export const useTimesheetStore = defineStore('timesheet', () => {
const is_loading = ref(false);
- const pay_period = ref(default_pay_period);
- const pay_period_overviews = ref([default_pay_period_overview,]);
- const current_pay_period_overview = ref(default_pay_period_overview);
- const pay_period_details = ref(default_pay_period_details);
+ const pay_period = ref();
+ const timesheets = ref([]);
+ const all_current_shifts = computed(() => timesheets.value.flatMap(week => week.days.flatMap(day => day.shifts)) ?? []);
+ const initial_timesheets = ref([]);
+
+ const pay_period_overviews = ref([]);
+ const pay_period_infos = ref();
+ const is_report_dialog_open = ref(false);
+
+ const is_details_dialog_open = ref(false);
+ const selected_employee_name = ref();
+ const has_timesheet_preset = ref(false);
+ const current_pay_period_overview = ref();
const pay_period_report = ref();
- const is_calendar_limit = computed(() =>
- pay_period.value.pay_year === 2024 &&
- pay_period.value.pay_period_no <= 1
- );
- const getPayPeriodByDateOrYearAndNumber = async (date_or_year: string | number, period_number?: number): Promise => {
- is_loading.value = true;
+ const getNextOrPreviousPayPeriod = (direction: number) => {
+ if (!pay_period.value) return;
+ pay_period.value.pay_period_no += direction;
+
+ if (pay_period.value.pay_period_no > 26) {
+ pay_period.value.pay_period_no = 1;
+ pay_period.value.pay_year += direction;
+ }
+
+ if (pay_period.value.pay_period_no < 1) {
+ pay_period.value.pay_period_no = 26;
+ pay_period.value.pay_year += direction;
+ }
+ };
+
+ const getPayPeriodByDateOrYearAndNumber = async (date?: string): Promise => {
try {
- if (typeof date_or_year === 'string') {
- pay_period.value = await timesheetService.getPayPeriodByDate(date_or_year);
- }
- else if (typeof date_or_year === 'number' && period_number) {
- pay_period.value = await timesheetService.getPayPeriodByYearAndPeriodNumber(date_or_year, period_number);
- }
- else pay_period.value = default_pay_period;
- is_loading.value = false;
+ if (date !== undefined) {
+ pay_period.value = await timesheetService.getPayPeriodByDate(date);
+ } else if (pay_period.value !== undefined) {
+ pay_period.value = await timesheetService.getPayPeriodByYearAndPeriodNumber(pay_period.value.pay_year, pay_period.value.pay_period_no);
+ } else return false;
return true;
} catch (error) {
console.error('Could not get current pay period: ', error);
- pay_period.value = default_pay_period;
- pay_period_overviews.value = [default_pay_period_overview,];
+ pay_period.value = undefined;
+ pay_period_overviews.value = [];
//TODO: More in-depth error-handling here
- is_loading.value = false;
return false;
}
};
- const getPayPeriodOverviewsBySupervisorEmail = async (pay_year: number, period_number: number, supervisor_email: string): Promise => {
+ const getTimesheetOverviews = async (): Promise => {
is_loading.value = true;
try {
- const response = await timesheetApprovalService.getPayPeriodOverviewsBySupervisorEmail(pay_year, period_number, supervisor_email);
+ if (pay_period.value === undefined) {
+ is_loading.value = false;
+ return false;
+ }
+
+ const response = await timesheetApprovalService.getPayPeriodOverviews(pay_period.value.pay_year, pay_period.value.pay_period_no);
pay_period_overviews.value = response.employees_overview;
is_loading.value = false;
return true;
} catch (error) {
console.error('There was an error retrieving Employee Pay Period overviews: ', error);
- pay_period_overviews.value = [default_pay_period_overview,];
+ pay_period_overviews.value = [];
// TODO: More in-depth error-handling here
is_loading.value = false;
-
+
return false;
}
};
- const getPayPeriodDetailsByEmployeeEmail = async (employee_email: string) => {
+ const getTimesheetsByOptionalEmployeeEmail = async (employee_email?: string): Promise => {
+ if (pay_period.value === undefined) return false;
is_loading.value = true;
+ let response;
+
try {
- const response = await timesheetService.getPayPeriodDetailsByPayPeriodAndEmployeeEmail(
- pay_period.value.pay_year,
- pay_period.value.pay_period_no,
- employee_email
- );
- pay_period_details.value = response;
- console.log('pay period details: ', response, pay_period_details.value.employee_full_name)
+ if (employee_email) {
+ response = await timesheetService.getTimesheetsByPayPeriodAndOptionalEmail(pay_period.value.pay_year, pay_period.value.pay_period_no, employee_email);
+ } else {
+ response = await timesheetService.getTimesheetsByPayPeriodAndOptionalEmail(pay_period.value.pay_year, pay_period.value.pay_period_no);
+ }
+
+ if (response.success && response.data) {
+ has_timesheet_preset.value = response.data.has_preset_schedule;
+ selected_employee_name.value = response.data.employee_fullname;
+ timesheets.value = response.data.timesheets;
+ initial_timesheets.value = unwrapAndClone(timesheets.value);
+ } else {
+ selected_employee_name.value = '';
+ timesheets.value = [];
+ initial_timesheets.value = [];
+ }
+
is_loading.value = false;
+ return response.success;
} catch (error) {
console.error('There was an error retrieving timesheet details for this employee: ', error);
// TODO: More in-depth error-handling here
- pay_period_details.value = default_pay_period_details;
+ timesheets.value = [];
is_loading.value = false;
- }
+ return false;
+ }
};
- const getPayPeriodReportByYearAndPeriodNumber = async (year: number, period_number: number, report_filters?: TimesheetApprovalCSVReportFilters) => {
- return withLoading(is_loading.value, async () => {
- try {
- const response = await timesheetApprovalService.getPayPeriodReportByYearAndPeriodNumber(
- year,
- period_number,
- report_filters
- );
- pay_period_report.value = response;
+ const toggleTimesheetsApprovalByEmployeeEmail = async (email: string, approval_status: boolean): Promise => {
+ try {
+ const timesheet_ids = timesheets.value.map(timesheet => timesheet.timesheet_id);
+
+ // Backend returns the amount of shifts and expenses successfully updated, could be useful for error handling???
+ // const shift_expense_count = timesheets.value.reduce((timesheets_sum, timesheet) => {
+ // return timesheets_sum + timesheet.days.reduce((day_sum, day) => {
+ // return day_sum + day.shifts.length + day.expenses.length
+ // }, 0);
+ // }, 0);
- return true;
- } catch (error) {
- console.error('There was an error retrieving the report CSV: ', error);
- // TODO: More in-depth error-handling here
- }
+ const response = await timesheetApprovalService.updateTimesheetsApprovalStatus(email, timesheet_ids, approval_status);
+ return response.success;
+ } catch (error) {
+ console.error("couldn't approve timesheets for employee: ", error);
+ }
- return false;
- });
+ return false;
+ };
+
+ const getPayPeriodReport = async (report_filters: TimesheetApprovalCSVReportFilters) => {
+ try {
+ if (!pay_period.value) return false;
+ const response = await timesheetApprovalService.getPayPeriodReportByYearAndPeriodNumber(pay_period.value.pay_year, pay_period.value.pay_period_no, report_filters);
+ pay_period_report.value = response;
+ return response.data;
+ } catch (error) {
+ console.error('There was an error retrieving the report CSV: ', error);
+ // TODO: More in-depth error-handling here
+ }
+
+ return false;
+ };
+
+ const openReportDialog = () => {
+ is_report_dialog_open.value = true;
+ is_loading.value = true;
+
+
+ is_loading.value = false;
+ };
+
+ const closeReportDialog = () => {
+ is_report_dialog_open.value = false;
};
return {
is_loading,
- is_calendar_limit,
+ is_report_dialog_open,
+ is_details_dialog_open,
pay_period,
pay_period_overviews,
current_pay_period_overview,
- pay_period_details,
+ pay_period_infos,
+ selected_employee_name,
+ has_timesheet_preset,
+ timesheets,
+ all_current_shifts,
+ initial_timesheets,
+ getNextOrPreviousPayPeriod,
getPayPeriodByDateOrYearAndNumber,
- getPayPeriodOverviewsBySupervisorEmail,
- getPayPeriodDetailsByEmployeeEmail,
- getPayPeriodReportByYearAndPeriodNumber,
+ getTimesheetOverviews,
+ getTimesheetsByOptionalEmployeeEmail,
+ toggleTimesheetsApprovalByEmployeeEmail,
+ getPayPeriodReport,
+ openReportDialog,
+ closeReportDialog,
};
});
\ No newline at end of file
diff --git a/src/stores/ui-store.ts b/src/stores/ui-store.ts
index a47561a..96e75e2 100644
--- a/src/stores/ui-store.ts
+++ b/src/stores/ui-store.ts
@@ -1,12 +1,83 @@
+import { useI18n } from 'vue-i18n';
import { defineStore } from 'pinia';
-import { ref } from 'vue';
+import { computed, ref } from 'vue';
+import { LocalStorage, useQuasar, Dark } from 'quasar';
+import { Preferences } from 'src/modules/profile/models/preferences.models';
+import { ProfileService } from 'src/modules/profile/services/profile-service';
+import { RouteNames } from 'src/router/router-constants';
+
export const useUiStore = defineStore('ui', () => {
- const isRightDrawerOpen = ref(true);
-
+ const q = useQuasar();
+ const { locale } = useI18n();
+ const is_left_drawer_open = ref(true);
+ const focus_next_component = ref(false);
+ const is_mobile_mode = computed(() => q.screen.lt.md);
+ const user_preferences = ref(new Preferences);
+ const current_page = ref(RouteNames.DASHBOARD);
+
+
const toggleRightDrawer = () => {
- isRightDrawerOpen.value = !isRightDrawerOpen.value;
+ is_left_drawer_open.value = !is_left_drawer_open.value;
+ };
+
+ const getUserPreferences = async () => {
+ try {
+ const local_user_preferences = LocalStorage.getItem('user_preferences');
+
+ if (local_user_preferences !== null) {
+ if (local_user_preferences.id !== -1) {
+ Object.assign(user_preferences.value, local_user_preferences);
+ setPreferences();
+ return;
+ }
+ }
+
+ const response = await ProfileService.getUserPreferences();
+
+ if (response.success && response.data) {
+ LocalStorage.setItem('user_preferences', response.data);
+ Object.assign(user_preferences.value, response.data);
+ setPreferences();
+ }
+ } catch (error) {
+ user_preferences.value = new Preferences;
+ console.error('Could not retrieve user preferences: ', error);
+ }
+ };
+
+ const updateUserPreferences = async () => {
+ try {
+ if (user_preferences.value.id === -1) return;
+
+ const response = await ProfileService.updateUserPreferences(user_preferences.value);
+ if (response.success && response.data) {
+ Object.assign(user_preferences.value, response.data);
+ LocalStorage.setItem('user_preferences', response.data);
+ setPreferences();
+ return;
+ }
+ } catch (error) {
+ console.error('Could not update user preferences: ', error);
+ }
+ };
+
+ const setPreferences = () => {
+ if (user_preferences.value !== undefined) {
+ // if user_preferences.value.is_dark_mode === null
+ Dark.set(user_preferences.value.is_dark_mode ?? "auto");
+ locale.value = user_preferences.value.display_language;
+ }
}
- return { isRightDrawerOpen, toggleRightDrawer };
+ return {
+ current_page,
+ is_mobile_mode,
+ focus_next_component,
+ is_left_drawer_open,
+ user_preferences,
+ toggleRightDrawer,
+ getUserPreferences,
+ updateUserPreferences,
+ };
});
diff --git a/src/utils/boolean-utils.ts b/src/utils/boolean-utils.ts
new file mode 100644
index 0000000..8335e2c
--- /dev/null
+++ b/src/utils/boolean-utils.ts
@@ -0,0 +1,25 @@
+// export const createDefaultBooleanValue = (keys_list: PropertyKey[]): Record =>
+// keys_list.reduce((acc, mod) => {
+// acc[mod] = false;
+// return acc;
+// }, {} as Record);
+
+
+// export const toBooleanFromKeys = (keys_list: PropertyKey[], arr?: readonly PropertyKey[] | null): Record => {
+// const result = createDefaultBooleanValue(keys_list);
+// if (!arr || !Array.isArray(arr)) return result;
+// for (const item of arr) {
+// if (typeof item !== 'string') continue;
+// const trimmed = item.trim();
+// if ((keys_list as readonly PropertyKey[]).includes(trimmed)) {
+// result[trimmed as T] = true;
+// }
+// }
+// return result;
+// }
+
+export const toKeysFromBoolean = (boolean_values: Record): T[] => {
+ const values_array = Object.entries(boolean_values);
+ const values = values_array.filter(([_key, value]) => value === true);
+ return values.map(([key]) => key as T);
+}
\ No newline at end of file
diff --git a/src/utils/date-and-time-utils.ts b/src/utils/date-and-time-utils.ts
index 5ce223e..3360f77 100644
--- a/src/utils/date-and-time-utils.ts
+++ b/src/utils/date-and-time-utils.ts
@@ -3,14 +3,25 @@ import { date } from 'quasar';
const anchor_date: Date = new Date('2023-12-17');
export const getCurrentPayPeriod = (today = new Date()): number => {
- const period_length = 14; // days
- const periods_per_year = 26;
+ const period_length = 14; // days
+ const periods_per_year = 26;
- const days_since_anchor = date.getDateDiff(today, anchor_date, 'days');
- const periods_since_anchor = Math.floor(days_since_anchor / period_length);
+ const days_since_anchor = date.getDateDiff(today, anchor_date, 'days');
+ const periods_since_anchor = Math.floor(days_since_anchor / period_length);
- const current_period = (periods_since_anchor % periods_per_year) + 1;
+ const current_period = (periods_since_anchor % periods_per_year) + 1;
- console.log(current_period);
- return current_period;
+ return current_period;
+}
+
+export const getMinutes = (hours: number) => {
+ const minutes_percent = hours - Math.floor(hours);
+ const minutes = Math.round(minutes_percent * 60);
+ return minutes > 1 ? minutes.toString() : '0';
+}
+
+export const getHoursMinutesStringFromHoursFloat = (hours: number): string => {
+ const flat_hours = Math.floor(hours);
+ const minutes = Math.round((hours - flat_hours) * 60);
+ return `${flat_hours}h${minutes > 1 ? ' ' + minutes : ''}`
}
\ No newline at end of file
diff --git a/src/utils/deep-equal.ts b/src/utils/deep-equal.ts
deleted file mode 100644
index 3b575f3..0000000
--- a/src/utils/deep-equal.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-import { unwrapAndClone } from "src/utils/unwrap-and-clone";
-
-/**
- * Internal recursive function comparing two plain values.
- */
-const _deepEqualRecursive = (a: unknown, b: unknown): boolean => {
- if (a === b) return true;
-
- if (a == null || b == null || typeof a !== "object" || typeof b !== "object") {
- return false;
- }
-
- // Handle arrays
- if (Array.isArray(a) && Array.isArray(b)) {
- if (a.length !== b.length) return false;
- return a.every((val, i) => _deepEqualRecursive(val, b[i]));
- } else if (Array.isArray(a) || Array.isArray(b)) {
- return false; // one is array, other is not
- }
-
- const aKeys = Object.keys(a as Record);
- const bKeys = Object.keys(b as Record);
-
- if (aKeys.length !== bKeys.length) return false;
-
- return aKeys.every((key) =>
- _deepEqualRecursive(
- (a as Record)[key],
- (b as Record)[key]
- )
- );
-};
-
-/**
- * Deep equality check that normalizes reactive objects first.
- */
-export const deepEqual = (given: unknown, expected: unknown): boolean => {
- const a = unwrapAndClone(given as object);
- const b = unwrapAndClone(expected as object);
- return _deepEqualRecursive(a, b);
-};