Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .importlinter
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
23 changes: 23 additions & 0 deletions client/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
5 changes: 5 additions & 0 deletions client/src/app/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ export interface ModuleManifest {
}

export const MODULE_REGISTRY: Record<string, ModuleManifest> = {
leave_elm: {
code: "leave_elm",
basePath: "/leave",
load: () => import("../modules/leave_elm/routes"),
},
placement_cell: {
code: "placement_cell",
basePath: "/placement",
Expand Down
57 changes: 57 additions & 0 deletions client/src/modules/leave_elm/api.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown>[];
calendar?: Record<string, unknown>[];
}

export interface NewApplication {
leave_type: number;
start_date: string;
end_date: string;
purpose: string;
}

export async function listApplications(): Promise<LeaveApplication[]> {
const response = await http.get('/leave/applications');
return response.data as LeaveApplication[];
}

export async function listBalances(): Promise<LeaveBalance[]> {
const response = await http.get('/leave/balances');
return response.data as LeaveBalance[];
}

export async function listTasks(): Promise<LeaveTask[]> {
const response = await http.get('/leave/tasks');
return response.data as LeaveTask[];
}

export async function submitApplication(data: NewApplication): Promise<void> {
await http.post('/leave/applications', data);
}

export async function applicationAction(id: number, action: string, data: Record<string, unknown>): Promise<void> {
await http.post(`/leave/applications/${id}/action`, { action, data });
}

export async function getConfiguration(): Promise<Configuration> {
const response = await http.get('/leave/configuration');
return response.data as Configuration;
}

export async function saveConfiguration(data: Configuration): Promise<void> {
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[] };
}
144 changes: 144 additions & 0 deletions client/src/modules/leave_elm/pages/LeaveAdminPage.tsx
Original file line number Diff line number Diff line change
@@ -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<Record<string, any>[]>([]);
const [notifications, setNotifications] = useState<Record<string, any>[]>([]);

return (
<Container size="lg" mt={40}>
<PageHeader title="Leave Admin" subtitle="Manage leave policies and configurations." />

{/* Policy Editor */}
<Paper shadow="sm" p="md">
<Title order={3}>Policy</Title>
<Textarea value={policyJSON} onChange={(event) => setPolicyJSON(event.target.value)} rows={15} />
<Group justify="right" mt="xs">
<Button onClick={handleSavePolicyClick} color="blue">Save Policy</Button>
</Group>
</Paper>

{/* Calendar Editor */}
<Paper shadow="sm" p="md" mt={20}>
<Title order={3}>Calendar</Title>
<Textarea value={calendarJSON} onChange={(event) => setCalendarJSON(event.target.value)} rows={15} />
<Group justify="right" mt="xs">
<Button onClick={handleSaveCalendarClick} color="green">Save Calendar</Button>
</Group>
</Paper>

{isLoading || isLoadingRecords ? (
<LoadingOverlay visible={true} overlayProps={{ blur: 2 }} />
) : isError || isErrorRecords ? (
<Alert color="red">{errorMessage(error)}</Alert>
) : (
<>
<Title order={4} mt={20}>History</Title>
<Table striped highlightOnHover>
<Table.Thead>
<Table.Tr>
<Table.Th>Date</Table.Th>
<Table.Th>Details</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{history.map((record, index) => (
<Table.Tr key={index}>
<Table.Td>{new Date(record.date).toLocaleDateString()}</Table.Td>
<Table.Td>{record.details}</Table.Td>
</Table.Tr>
))}
</Table.Tbody>
</Table>

<Title order={4} mt={20}>Notifications</Title>
<Table striped highlightOnHover>
<Table.Thead>
<Table.Tr>
<Table.Th>Title</Table.Th>
<Table.Th>Description</Table.Th>
<Table.Th>Status</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{notifications.map((notification, index) => (
<Table.Tr key={index}>
<Table.Td>{notification.title}</Table.Td>
<Table.Td>{notification.description}</Table.Td>
<Table.Td>{notification.status}</Table.Td>
</Table.Tr>
))}
</Table.Tbody>
</Table>
</>
)}
</Container>
);
};

export default LeaveAdminPage;
60 changes: 60 additions & 0 deletions client/src/modules/leave_elm/pages/LeaveTasksPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { useEffect } from 'react';
import { Group, Stack, Text, Title, Button, LoadingOverlay, Alert, Container } from '@mantine/core';
import { useQuery, useMutation } from '@tanstack/react-query';
import { http } from '../../../lib/http';
import { listTasks } from '../api';
import { PageHeader } from '../../../ui/components/PageHeader';

export const LeaveTasksPage = () => {
const { mutate: approveTask } = useMutation({ mutationFn: (data: { id: number }) =>
http.post(`/leave/applications/${data.id}/action`, { action: 'approve', data })
});
const { mutate: rejectTask } = useMutation({ mutationFn: (data: { id: number }) =>
http.post(`/leave/applications/${data.id}/action`, { action: 'reject', data })
});
const { mutate: recommendTask } = useMutation({ mutationFn: (data: { id: number }) =>
http.post(`/leave/applications/${data.id}/action`, { action: 'recommend', data })
});
const { mutate: substituteTask } = useMutation({ mutationFn: (data: { id: number }) =>
http.post(`/leave/applications/${data.id}/action`, { action: 'substitute', data })
});

const { isLoading, isError, error, data: tasks } = useQuery({
queryKey: ['tasks'],
queryFn: listTasks,
});

useEffect(() => {
if (isError && error instanceof Error) {
console.error('Error fetching leave tasks:', error.message);
}
}, [isError, error]);

return (
<Container>
<LoadingOverlay visible={isLoading} />
{isError && (
<Alert title="Error" color="red">
{error?.message}
</Alert>
)}
<PageHeader title="Leave Tasks" subtitle="Manage pending leave applications" />

<Stack gap="lg">
{!isLoading && !isError && tasks!.map((task) => (
<Group key={task.id}>
<Title order={3}>{task.title}</Title>
<Text c="dimmed">{task.description}</Text>

<Button onClick={() => approveTask({ id: task.id })}>Approve</Button>
<Button onClick={() => rejectTask({ id: task.id })}>Reject</Button>
<Button onClick={() => recommendTask({ id: task.id })}>Recommend</Button>
<Button onClick={() => substituteTask({ id: task.id })}>Substitute</Button>
</Group>
))}
</Stack>
</Container>
);
};

export default LeaveTasksPage;
Loading