diff --git a/README.md b/README.md index a5d94ac54..05ae0c4c9 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,10 @@ # Fusion Frontend ## Overview + This Project is the frontend of the Fusion - IIITDMJ's ERP Portal. We've migrated the frontend of fusion from Django templates to a modern React-based architecture. -## ## Tech Stack +## Tech Stack - [ReactJS](https://react.dev/learn) as the main frontend library - [Mantine UI](https://mantine.dev/getting-started/) for UI components @@ -14,16 +15,128 @@ This Project is the frontend of the Fusion - IIITDMJ's ERP Portal. We've migrate Check the `package.json` file for more information about all the libraries being used. This project is using Eslint and Prettier for linting and formatting the code. -## Setting up the project 🛠️ +## Module-Wise Sync Targets + +For production synchronization targets, refer to: [Fusion-README](https://github.com/FusionIIIT/Fusion-README) + +## Full-Stack Module-Wise Git Workflow Guide + +This section outlines the repository setup, branch management, and contribution workflow for teams working on the Fusion ERP Frontend. + +### Phase 1: Team Lead Setup + +1. **Fork the Main Repository:** + + * Go to [https://github.com/FusionIIIT/Fusion-client](https://github.com/FusionIIIT/Fusion-client) and click **Fork, **Uncheck** **the ****Checkbox,** **andclick ****Create fork.**** +2. **Share:** + + * Distribute your forked repository URL to your team members + +### Phase 2: Team Member Setup + +1. **Fork the Team Lead's Repository:** + + * Navigate to your Team Lead's fork and fork it to your own GitHub account +2. **Clone Locally:** + + ```sh + git clone https://github.com//Fusion-client.git + ``` +3. **Set Upstream:** + + ```sh + cd Fusion-client + git remote add upstream https://github.com//Fusion-client.git + ``` + +### Phase 3: Module-Wise Branch Switching + +**Note:** v1 (MANUAL : Work on Existing Codebase) and v2 (AI : Work from Scracth according to documnets and Fusion README) - If you are assigned to the AI group, replace `v1` with `v2` in the commands below. + +Fetch upstream data first: -1. Fork the repository +```sh +cd Fusion-client +git fetch upstream +``` + +Then run your specific module command: + +* **Examination:** `git checkout -b examination-v1 upstream/examination-v1` +* **LMS:** `git checkout -b lms-v1 upstream/lms-v1` +* **Award & Scholarship:** `git checkout -b scholarships-v1 upstream/scholarships-v1` +* **Department:** `git checkout -b department-v1 upstream/department-v1` +* **Other Academic Procedure:** `git checkout -b academic-procedures-v1 upstream/academic-procedures-v1` +* **Announcements:** `git checkout -b announcements-v1 upstream/announcements-v1` +* **Placement Cell + PBI:** `git checkout -b placement-pbi-v1 upstream/placement-pbi-v1` +* **Gymkhana:** `git checkout -b gymkhana-v1 upstream/gymkhana-v1` +* **Primary Health Center:** `git checkout -b health-center-v1 upstream/health-center-v1` +* **Hostel Management:** `git checkout -b hostel-management-v1 upstream/hostel-management-v1` +* **Mess Management:** `git checkout -b mess-management-v1 upstream/mess-management-v1` +* **Visitor Hostel:** `git checkout -b visitor-hostel-v1 upstream/visitor-hostel-v1` +* **Visitor Management System:** `git checkout -b visitor-management-v1 upstream/visitor-management-v1` +* **Dashboards:** `git checkout -b dashboards-v1 upstream/dashboards-v1` +* **File Tracking System:** `git checkout -b file-tracking-v1 upstream/file-tracking-v1` +* **RSPC:** `git checkout -b rspc-v1 upstream/rspc-v1` +* **P&S Management:** `git checkout -b ps-management-v1 upstream/ps-management-v1` +* **HR (EIS):** `git checkout -b hr-eis-v1 upstream/hr-eis-v1` +* **Patent Management System:** `git checkout -b patent-management-v1 upstream/patent-management-v1` +* **Institute Works Department:** `git checkout -b institute-works-v1 upstream/institute-works-v1` +* **Internal Audit and Accounts:** `git checkout -b audit-accounts-v1 upstream/audit-accounts-v1` +* **Complaint Management:** `git checkout -b complaint-management-v1 upstream/complaint-management-v1` + +## Setting up the project + +1. Fork the repository (as described above) 2. Clone **your forked** repository -3. Change directory to the project folder(`cd path/to/project`) -4. Run `npm install` to install all the dependencies -5. Run `npm run dev` to start the development server. +3. Change directory to the project folder: `cd Fusion-client` +4. Install all dependencies: + ```sh + npm install + ``` +5. Run the development server: + ```sh + npm run dev + ``` + The development server will start at `http://localhost:5173/` -Make sure that your backend server is running properly before starting the frontend server. +**Important:** Make sure that your backend server is running properly before starting the frontend server. + +## Phase 4: Syncing, Committing, and PRs + +### Production Sync Targets + +* **Frontend Production Branch:** `acad-main` + +### Workflow + +1. **Sync with Production:** + + * Frequently pull the latest changes from your specific module's production branch to avoid merge conflicts later + * Team lead syncs first, then team members sync from the team lead's fork + + ```sh + git pull upstream + ``` +2. **Make Changes:** + + * Make your code changes and commit them locally to your active module branch +3. **Committing:** + + ```sh + git add . + git commit -m "Your descriptive commit message" + ``` +4. **Pushing:** + + ```sh + git push origin + ``` +5. **Create Pull Request:** + + * Go to `https://github.com//Fusion-client/tree/` and create a Pull Request + * **Important:** Target the Team Lead's fork, NOT the main FusionIIIT repository ## Project Structure and important information @@ -35,7 +148,7 @@ Make sure that your backend server is running properly before starting the front 6. All the web pages related to a a **module** are in `src/modules/` folder. 7. All the components related to a **module** are in the `src/modules//components` folder. 8. All the styles related to a **module** are in the `src/modules//styles` folder. -9. All the state management related code is in the `src/redux` folder. The `src/redux/userSlice.jsx` file contains user-related states. +9. All the state management related code is in the `src/redux` folder. The `src/redux/userSlice.jsx` file contains user-related states. - Note: You can access the username and role of the user using the `useSelector` hook. @@ -63,4 +176,3 @@ const ExampleComponent = () => { - All the constants should be in UPPERCASE. **Note**: Please make sure to follow the project structure and naming conventions while adding new files or folders to the project. - diff --git a/src/Modules/FileTracking/FeatureOne.jsx b/src/Modules/FileTracking/FeatureOne.jsx new file mode 100644 index 000000000..81f033c08 --- /dev/null +++ b/src/Modules/FileTracking/FeatureOne.jsx @@ -0,0 +1,7 @@ +import React from "react"; + +import SectionNavigation from "./components/SectionNavigation"; + +export default function FeatureOne() { + return ; +} diff --git a/src/Modules/FileTracking/FeatureTwo.jsx b/src/Modules/FileTracking/FeatureTwo.jsx new file mode 100644 index 000000000..afab2c0aa --- /dev/null +++ b/src/Modules/FileTracking/FeatureTwo.jsx @@ -0,0 +1,7 @@ +import React from "react"; + +import AdminManagement from "./components/AdminManagement"; + +export default function FeatureTwo() { + return ; +} diff --git a/src/Modules/FileTracking/api.js b/src/Modules/FileTracking/api.js new file mode 100644 index 000000000..e7f351712 --- /dev/null +++ b/src/Modules/FileTracking/api.js @@ -0,0 +1,188 @@ +import axios from "axios"; + +import { + newAdminAuditLogsRoute, + newAdminPoliciesRoute, + newAdminUserDetailRoute, + newAdminUsersRoute, + newAmendRoute, + newApproveRoute, + newArchiveListRoute, + newArchiveRoute, + newCloseRoute, + newDeleteDraftRoute, + newDesignationsRoute, + newDraftsRoute, + newFileDetailRoute, + newFilesRoute, + newFileTypesRoute, + newForwardRoute, + newHistoryRoute, + newInboxRoute, + newOutboxRoute, + newPendingRoute, + newRejectRoute, + newReturnRoute, + newSendRoute, +} from "../../routes/filetrackingRoutes"; + +axios.defaults.withCredentials = true; + +const authConfig = (token) => ({ + withCredentials: true, + headers: { + Authorization: `Token ${token}`, + }, +}); + +const listPayload = (payload) => { + if (Array.isArray(payload)) { + return payload; + } + + if (payload && Array.isArray(payload.results)) { + return payload.results; + } + + return []; +}; + +export const listNewFiles = async (token, params = {}) => { + const response = await axios.get(newFilesRoute, { ...authConfig(token), params }); + return listPayload(response.data); +}; + +export const createNewFile = async (payload, token) => { + const response = await axios.post(newFilesRoute, payload, authConfig(token)); + return response.data; +}; + +export const getNewFileDetail = async (id, token) => { + const response = await axios.get(newFileDetailRoute(id), authConfig(token)); + return response.data; +}; + +export const updateNewFileDetail = async (id, payload, token) => { + const response = await axios.put(newFileDetailRoute(id), payload, authConfig(token)); + return response.data; +}; + +export const sendFile = async (id, payload, token) => { + const response = await axios.post(newSendRoute(id), payload, authConfig(token)); + return response.data; +}; + +export const forwardFile = async (id, payload, token) => { + const response = await axios.post(newForwardRoute(id), payload, authConfig(token)); + return response.data; +}; + +export const returnFile = async (id, payload, token) => { + const response = await axios.post(newReturnRoute(id), payload, authConfig(token)); + return response.data; +}; + +export const amendFile = async (id, payload, token) => { + const response = await axios.post(newAmendRoute(id), payload, authConfig(token)); + return response.data; +}; + +export const approveFile = async (id, payload, token) => { + const response = await axios.post(newApproveRoute(id), payload, authConfig(token)); + return response.data; +}; + +export const rejectFile = async (id, payload, token) => { + const response = await axios.post(newRejectRoute(id), payload, authConfig(token)); + return response.data; +}; + +export const closeFile = async (id, payload, token) => { + const response = await axios.post(newCloseRoute(id), payload, authConfig(token)); + return response.data; +}; + +export const archiveFile = async (id, payload, token) => { + const response = await axios.post(newArchiveRoute(id), payload, authConfig(token)); + return response.data; +}; + +export const getFileHistory = async (id, token) => { + const response = await axios.get(newHistoryRoute(id), authConfig(token)); + return response.data; +}; + +export const listDrafts = async (token) => { + const response = await axios.get(newDraftsRoute, authConfig(token)); + return listPayload(response.data); +}; + +export const deleteDraft = async (id, token) => { + const response = await axios.delete(newDeleteDraftRoute(id), authConfig(token)); + return response.data; +}; + +export const listInbox = async (token, params = {}) => { + const response = await axios.get(newInboxRoute, { ...authConfig(token), params }); + return listPayload(response.data); +}; + +export const listOutbox = async (token, params = {}) => { + const response = await axios.get(newOutboxRoute, { ...authConfig(token), params }); + return listPayload(response.data); +}; + +export const listPending = async (token, params = {}) => { + const response = await axios.get(newPendingRoute, { ...authConfig(token), params }); + return listPayload(response.data); +}; + +export const listArchive = async (token, params = {}) => { + const response = await axios.get(newArchiveListRoute, { ...authConfig(token), params }); + return listPayload(response.data); +}; + +export const listFileTypes = async (token) => { + const response = await axios.get(newFileTypesRoute, authConfig(token)); + return response.data; +}; + +export const listDesignations = async (token) => { + const response = await axios.get(newDesignationsRoute, authConfig(token)); + return listPayload(response.data); +}; + +export const listAdminUsers = async (token, params = {}) => { + const response = await axios.get(newAdminUsersRoute, { ...authConfig(token), params }); + return listPayload(response.data); +}; + +export const createAdminUser = async (payload, token) => { + const response = await axios.post(newAdminUsersRoute, payload, authConfig(token)); + return response.data; +}; + +export const updateAdminUser = async (id, payload, token) => { + const response = await axios.put(newAdminUserDetailRoute(id), payload, authConfig(token)); + return response.data; +}; + +export const deactivateAdminUser = async (id, token) => { + const response = await axios.delete(newAdminUserDetailRoute(id), authConfig(token)); + return response.data; +}; + +export const listAdminPolicies = async (token) => { + const response = await axios.get(newAdminPoliciesRoute, authConfig(token)); + return listPayload(response.data); +}; + +export const updateAdminPolicies = async (payload, token) => { + const response = await axios.put(newAdminPoliciesRoute, payload, authConfig(token)); + return response.data; +}; + +export const listAdminAuditLogs = async (token, params = {}) => { + const response = await axios.get(newAdminAuditLogsRoute, { ...authConfig(token), params }); + return listPayload(response.data); +}; diff --git a/src/Modules/FileTracking/components/AdminManagement.jsx b/src/Modules/FileTracking/components/AdminManagement.jsx new file mode 100644 index 000000000..2f4ad610a --- /dev/null +++ b/src/Modules/FileTracking/components/AdminManagement.jsx @@ -0,0 +1,459 @@ +import { useEffect, useMemo, useState } from "react"; +import { + Box, + Button, + Card, + Divider, + Group, + Modal, + Select, + Stack, + Table, + Tabs, + Text, + TextInput, + Textarea, + Title, + Switch, +} from "@mantine/core"; +import { notifications } from "@mantine/notifications"; +import { + createAdminUser, + deactivateAdminUser, + listAdminAuditLogs, + listAdminPolicies, + listAdminUsers, + listDesignations, + updateAdminPolicies, + updateAdminUser, +} from "../api"; +import { getApiErrorMessage } from "../utils/apiErrors"; + +const DEFAULT_CREATE_FORM = { + username: "", + password: "", + first_name: "", + last_name: "", + email: "", + designation_names: [], +}; + +export default function AdminManagement() { + const token = localStorage.getItem("authToken"); + + const [users, setUsers] = useState([]); + const [designations, setDesignations] = useState([]); + const [logs, setLogs] = useState([]); + const [policies, setPolicies] = useState([]); + + const [createForm, setCreateForm] = useState(DEFAULT_CREATE_FORM); + const [newPolicyKey, setNewPolicyKey] = useState(""); + const [newPolicyValue, setNewPolicyValue] = useState("{}"); + const [selectedUser, setSelectedUser] = useState(null); + const [editRoles, setEditRoles] = useState([]); + const [editIsStaff, setEditIsStaff] = useState(false); + const [editIsActive, setEditIsActive] = useState(true); + const [showUserModal, setShowUserModal] = useState(false); + + const designationOptions = useMemo( + () => designations.map((d) => ({ value: d.name, label: d.full_name || d.name })), + [designations], + ); + + const loadUsers = async () => { + const data = await listAdminUsers(token); + setUsers(data || []); + }; + + const loadDesignations = async () => { + const data = await listDesignations(token); + setDesignations(data || []); + }; + + const loadPolicies = async () => { + const data = await listAdminPolicies(token); + setPolicies(data || []); + }; + + const loadLogs = async () => { + const data = await listAdminAuditLogs(token, { limit: 200 }); + setLogs(data || []); + }; + + const loadAll = async () => { + try { + await Promise.all([loadUsers(), loadDesignations(), loadPolicies(), loadLogs()]); + } catch (err) { + notifications.show({ + title: "Admin console unavailable", + message: getApiErrorMessage(err, "Failed to load admin data."), + color: "red", + }); + } + }; + + useEffect(() => { + loadAll(); + }, []); + + const handleCreateUser = async () => { + if (!createForm.username || !createForm.password) { + notifications.show({ + title: "Missing required fields", + message: "Username and password are required.", + color: "red", + }); + return; + } + + try { + await createAdminUser(createForm, token); + notifications.show({ + title: "User created", + message: `User ${createForm.username} created successfully.`, + color: "green", + }); + setCreateForm(DEFAULT_CREATE_FORM); + loadUsers(); + loadLogs(); + } catch (err) { + notifications.show({ + title: "Create failed", + message: getApiErrorMessage(err, "Could not create user."), + color: "red", + }); + } + }; + + const openEditUser = (user) => { + setSelectedUser(user); + setEditRoles(user.roles || []); + setEditIsStaff(Boolean(user.is_staff)); + setEditIsActive(Boolean(user.is_active)); + setShowUserModal(true); + }; + + const handleUpdateUser = async () => { + if (!selectedUser) return; + + try { + await updateAdminUser( + selectedUser.id, + { + designation_names: editRoles, + is_staff: editIsStaff, + is_active: editIsActive, + }, + token, + ); + + notifications.show({ + title: "User updated", + message: `Updated roles for ${selectedUser.username}.`, + color: "green", + }); + setShowUserModal(false); + loadUsers(); + loadLogs(); + } catch (err) { + notifications.show({ + title: "Update failed", + message: getApiErrorMessage(err, "Could not update user."), + color: "red", + }); + } + }; + + const handleDeactivateUser = async (user) => { + try { + await deactivateAdminUser(user.id, token); + notifications.show({ + title: "User deactivated", + message: `${user.username} has been deactivated.`, + color: "green", + }); + loadUsers(); + loadLogs(); + } catch (err) { + notifications.show({ + title: "Deactivate failed", + message: getApiErrorMessage(err, "Could not deactivate user."), + color: "red", + }); + } + }; + + const handleSavePolicy = async () => { + if (!newPolicyKey) { + notifications.show({ + title: "Policy key required", + message: "Enter a policy key.", + color: "red", + }); + return; + } + + let parsedValue = {}; + try { + parsedValue = JSON.parse(newPolicyValue || "{}"); + } catch (err) { + notifications.show({ + title: "Invalid JSON", + message: "Policy value must be valid JSON.", + color: "red", + }); + return; + } + + try { + await updateAdminPolicies( + { + key: newPolicyKey, + value: parsedValue, + }, + token, + ); + notifications.show({ + title: "Policy updated", + message: `${newPolicyKey} saved successfully.`, + color: "green", + }); + setNewPolicyKey(""); + setNewPolicyValue("{}"); + loadPolicies(); + loadLogs(); + } catch (err) { + notifications.show({ + title: "Policy update failed", + message: getApiErrorMessage(err, "Could not update policy."), + color: "red", + }); + } + }; + + return ( + + + FT Admin User and Role Management + + + + + + Users and Roles + Policies + Audit Logs + + + + + + Create User + + setCreateForm((s) => ({ ...s, username: e.currentTarget.value }))} + /> + setCreateForm((s) => ({ ...s, password: e.currentTarget.value }))} + /> + setCreateForm((s) => ({ ...s, first_name: e.currentTarget.value }))} + /> + setCreateForm((s) => ({ ...s, last_name: e.currentTarget.value }))} + /> + + + setCreateForm((s) => ({ ...s, email: e.currentTarget.value }))} + /> +