diff --git a/.importlinter b/.importlinter index d964dc3c..95276395 100644 --- a/.importlinter +++ b/.importlinter @@ -12,10 +12,15 @@ include_external_packages = True name = Modules may reach each other ONLY through contracts.py type = forbidden source_modules = + modules.leave_elm modules.placement modules.directory modules.accesscontrol forbidden_modules = + modules.leave_elm.models + modules.leave_elm.services + modules.leave_elm.api + modules.leave_elm.domain modules.placement.models modules.placement.services modules.placement.selectors @@ -38,6 +43,7 @@ type = forbidden source_modules = # Every module's domain, not just the first one written. modules.placement.domain + modules.leave_elm.domain forbidden_modules = django rest_framework @@ -66,3 +72,12 @@ layers = modules.placement.selectors modules.placement.models modules.placement.domain + +[importlinter:contract:elm-layers] +name = Layering inside ELM +type = layers +layers = + modules.leave_elm.api + modules.leave_elm.services + modules.leave_elm.models + modules.leave_elm.domain diff --git a/client/package-lock.json b/client/package-lock.json index 04403fdb..99073660 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -15,6 +15,7 @@ "dayjs": "^1.11.21", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-hook-form": "^7.90.0", "react-icons": "^5.3.0", "react-router-dom": "^6.27.0" }, @@ -3111,6 +3112,28 @@ "react": "^18.3.1" } }, + "node_modules/react-hook-form": { + "version": "7.90.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.90.0.tgz", + "integrity": "sha512-+NdG0FLzCPhFOp2NNxaUYLF9fyZhNjx9FTBBSYWF71/l6BBlmIc/j6xlp3DbaHWLJbIG7Zd8oznA19xJClU97w==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17 || ^18 || ^19" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/react-icons": { "version": "5.7.0", "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.7.0.tgz", diff --git a/client/package.json b/client/package.json index 5d4d3a9a..9b9e6ea7 100644 --- a/client/package.json +++ b/client/package.json @@ -21,6 +21,7 @@ "dayjs": "^1.11.21", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-hook-form": "^7.90.0", "react-icons": "^5.3.0", "react-router-dom": "^6.27.0" }, diff --git a/client/src/app/registry.ts b/client/src/app/registry.ts index e76d32bf..7a4e519f 100644 --- a/client/src/app/registry.ts +++ b/client/src/app/registry.ts @@ -12,6 +12,11 @@ export interface ModuleManifest { } export const MODULE_REGISTRY: Record = { + leave_elm: { + code: "leave_elm", + basePath: "/leave", + load: () => import("../modules/leave_elm/routes"), + }, placement_cell: { code: "placement_cell", basePath: "/placement", diff --git a/client/src/modules/leave_elm/api.ts b/client/src/modules/leave_elm/api.ts new file mode 100644 index 00000000..eb152021 --- /dev/null +++ b/client/src/modules/leave_elm/api.ts @@ -0,0 +1,57 @@ +import type { LeaveApplication, LeaveBalance, LeaveTask, LeaveHistory, LeaveNotification } from "./types"; +export type { LeaveApplication, LeaveBalance, LeaveTask, LeaveHistory, LeaveNotification } from "./types"; + + +import { http } from '../../lib/http'; + + + + +export interface Configuration { + policy?: Record[]; + calendar?: Record[]; +} + +export interface NewApplication { + leave_type: number; + start_date: string; + end_date: string; + purpose: string; +} + +export async function listApplications(): Promise { + const response = await http.get('/leave/applications'); + return response.data as LeaveApplication[]; +} + +export async function listBalances(): Promise { + const response = await http.get('/leave/balances'); + return response.data as LeaveBalance[]; +} + +export async function listTasks(): Promise { + const response = await http.get('/leave/tasks'); + return response.data as LeaveTask[]; +} + +export async function submitApplication(data: NewApplication): Promise { + await http.post('/leave/applications', data); +} + +export async function applicationAction(id: number, action: string, data: Record): Promise { + await http.post(`/leave/applications/${id}/action`, { action, data }); +} + +export async function getConfiguration(): Promise { + const response = await http.get('/leave/configuration'); + return response.data as Configuration; +} + +export async function saveConfiguration(data: Configuration): Promise { + await http.post('/leave/configuration', data); +} + +export async function getRecords(): Promise<{ history?: LeaveHistory[], notifications?: LeaveNotification[] }> { + const response = await http.get('/leave/records'); + return response.data as { history?: LeaveHistory[], notifications?: LeaveNotification[] }; +} diff --git a/client/src/modules/leave_elm/pages/LeaveAdminPage.tsx b/client/src/modules/leave_elm/pages/LeaveAdminPage.tsx new file mode 100644 index 00000000..72a75fa1 --- /dev/null +++ b/client/src/modules/leave_elm/pages/LeaveAdminPage.tsx @@ -0,0 +1,144 @@ +import { useEffect, useState } from 'react'; +import { useMutation, useQuery } from '@tanstack/react-query'; +import { Button, Group, Title, Textarea, LoadingOverlay, Alert, Container, Paper, Table } from "@mantine/core"; +import { errorMessage } from "../../../lib/http"; +import { PageHeader } from "../../../ui/components/PageHeader"; +import { getConfiguration, saveConfiguration, getRecords, type Configuration } from "../api"; + +const LeaveAdminPage = () => { + const [policyJSON, setPolicyJSON] = useState(''); + const [calendarJSON, setCalendarJSON] = useState(''); + + // Fetch configuration data initially + const { isLoading, isError, error, data } = useQuery({ queryKey: ['configuration'], queryFn: async () => { + return await getConfiguration(); + } }); + + useEffect(() => { + if (data) { + setPolicyJSON(JSON.stringify(data.policy || [], null, 2)); + setCalendarJSON(JSON.stringify(data.calendar || [], null, 2)); + } + }, [data]); + + const saveConfigurationMutation = useMutation({ + mutationFn: async (newData: Configuration) => { + return await saveConfiguration(newData); + }, + onSuccess: () => { + alert('Configuration saved successfully!'); + }, + onError: (error) => { + console.error(error); + alert('Failed to save configuration.'); + }, + }); + + const handleSavePolicyClick = () => { + try { + const parsedPolicy = JSON.parse(policyJSON); + saveConfigurationMutation.mutate({ policy: parsedPolicy }); + } catch (e) { + alert('Invalid JSON format for policy'); + } + }; + + const handleSaveCalendarClick = () => { + try { + const parsedCalendar = JSON.parse(calendarJSON); + saveConfigurationMutation.mutate({ calendar: parsedCalendar }); + } catch (e) { + alert('Invalid JSON format for calendar'); + } + }; + + // Fetch records + const { isLoading: isLoadingRecords, isError: isErrorRecords, data: dataRecords } = useQuery({ queryKey: ['records'], queryFn: async () => { + return await getRecords(); + } }); + + useEffect(() => { + if (dataRecords && dataRecords.history) { + setHistory(dataRecords.history); + } + + if (dataRecords && dataRecords.notifications) { + setNotifications(dataRecords.notifications); + } + }, [dataRecords]); + + const [history, setHistory] = useState[]>([]); + const [notifications, setNotifications] = useState[]>([]); + + return ( + + + + {/* Policy Editor */} + + Policy +