diff --git a/client/package-lock.json b/client/package-lock.json index 46f7ac1..4df6af7 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -20,6 +20,7 @@ "next": "15.4.7", "react": "19.1.0", "react-dom": "19.1.0", + "swr": "^2.3.8", "tailwind-merge": "^3.3.1", "tailwindcss-animate": "^1.0.7" }, @@ -2574,6 +2575,15 @@ "node": ">=0.4.0" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/detect-libc": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", @@ -6592,6 +6602,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/swr": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/swr/-/swr-2.3.8.tgz", + "integrity": "sha512-gaCPRVoMq8WGDcWj9p4YWzCMPHzE0WNl6W8ADIx9c3JBEIdMkJGMzW+uzXvxHMltwcYACr9jP+32H8/hgwMR7w==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/tailwind-merge": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.1.tgz", @@ -6937,6 +6960,15 @@ "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/client/package.json b/client/package.json index ce70203..ad72587 100644 --- a/client/package.json +++ b/client/package.json @@ -27,6 +27,7 @@ "next": "15.4.7", "react": "19.1.0", "react-dom": "19.1.0", + "swr": "^2.3.8", "tailwind-merge": "^3.3.1", "tailwindcss-animate": "^1.0.7" }, diff --git a/client/src/components/ui/backend/organization_call_backend.ts b/client/src/components/ui/backend/organization_call_backend.ts new file mode 100644 index 0000000..695ee3b --- /dev/null +++ b/client/src/components/ui/backend/organization_call_backend.ts @@ -0,0 +1,159 @@ +// src/hooks/organization_call_backend.ts +// Change this URL to match your backend API endpoint + +import { Inventory_Details_Interface } from "@/components/ui/card_organization_inventory_details_modal"; +import { Member_Details_Interface } from "@/components/ui/card_organization_member_details_modal"; +import { generateRandomMockInventoryDetails } from "@/mocks/Inventory_Details_Interface_Mocks"; +import { generateMockMember } from "@/mocks/Members_Details_Interface_Mocks"; + +// tha main URL +export const BASE_URL = "http://localhost:8000/api/activities/"; + +// change when backend is ready +const isDev: boolean = true; + +// --- GET: Fetch all items --- +export const getItems = async ( + filterType: string, +): Promise => { + if (isDev) { + // Mock data for development + const mockData: Inventory_Details_Interface[] = []; + for (let i = 0; i < 10; i++) { + mockData.push(generateRandomMockInventoryDetails()); + } + console.log("Mock data generated:", mockData); + return mockData; + } else { + // 1. Fetch from Django + // fetch() is used to get the data from backend using URL + const response = await fetch(`${BASE_URL}?type=${filterType}`); + + // 2. Check if the request was successful + if (!response.ok) throw new Error("Failed to fetch"); + + // 3. Parse the JSON data + // because fetcah returns a response, it isnt the data yet, + // its just the response header, + // you will need to use .json() to get the data + // it converts raw bytes to Javascript objects + return await response.json(); // Returns the list from Django + } +}; + +// --- POST: Create a new item --- +// .stringify, flattens the object +// headers: { 'Content-Type': 'application/json' } determine, how the data will be dealt with by the server +// possible types: text/plain, text/html application/json, images/jpeg, application/x-www-form-urlencoded ( form data ), +// if you dont have this, might get error 400 +export const createItem = async ( + newData: Inventory_Details_Interface, +): Promise => { + if (isDev) { + console.log("Mock create item called with data:", newData); + return true; // Simulate successful creation + } else { + const response = await fetch(BASE_URL, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(newData), + }); + + // must return true when coding backend + return await response.json(); + } +}; + +// --- PATCH/PUT: Update an existing item --- +export const updateItem = async ( + id: number, + newData: Inventory_Details_Interface, +) => { + // Django usually expects a trailing slash after the ID + const response = await fetch(`${BASE_URL}${id}/`, { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(newData), + }); + return await response.json(); +}; + +// --- DELETE: Remove an item --- +export const deleteItem = async (id: number) => { + const response = await fetch(`${BASE_URL}${id}/`, { + method: "DELETE", + }); + // DELETE usually returns a 204 No Content status, so we don't always .json() it + return response.ok; +}; + +// MEMBERS SECTION + +export const getMembers = async ( + filterType: string, +): Promise => { + if (isDev) { + // Mock data for development + const mockData: Member_Details_Interface[] = []; + for (let i = 0; i < 10; i++) { + mockData.push(generateMockMember()); + } + console.log("Mock data generated:", mockData); + return mockData; + } else { + // 1. Fetch from Django + // fetch() is used to get the data from backend using URL + const response = await fetch(`${BASE_URL}?type=${filterType}`); + + // 2. Check if the request was successful + if (!response.ok) throw new Error("Failed to fetch"); + + // 3. Parse the JSON data + // because fetcah returns a response, it isnt the data yet, + // its just the response header, + // you will need to use .json() to get the data + // it converts raw bytes to Javascript objects + return await response.json(); // Returns the list from Django + } +}; + +export const createMember = async ( + newData: Member_Details_Interface, +): Promise => { + if (isDev) { + console.log("Mock create Member called with data:", newData); + return true; // Simulate successful creation + } else { + const response = await fetch(BASE_URL, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(newData), + }); + + // must return true when coding backend + return await response.json(); + } +}; + +// --- PATCH/PUT: Update an existing Member --- +export const updateMember = async ( + id: number, + newData: Member_Details_Interface, +) => { + // Django usually expects a trailing slash after the ID + const response = await fetch(`${BASE_URL}${id}/`, { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(newData), + }); + return await response.json(); +}; + +// --- DELETE: Remove an Member --- +export const deleteMember = async (id: number) => { + const response = await fetch(`${BASE_URL}${id}/`, { + method: "DELETE", + }); + // DELETE usually returns a 204 No Content status, so we don't always .json() it + return response.ok; +}; diff --git a/client/src/components/ui/backend/organization_clean_backend_calls.ts b/client/src/components/ui/backend/organization_clean_backend_calls.ts new file mode 100644 index 0000000..efa1225 --- /dev/null +++ b/client/src/components/ui/backend/organization_clean_backend_calls.ts @@ -0,0 +1,265 @@ +// src/hooks/organization_clean_backend_calls.ts +import SWR, { KeyedMutator } from "swr"; + +import { Member_Details_Interface } from "@/components/ui/card_organization_member_details_modal"; + +import type { Inventory_Details_Interface } from "../card_organization_inventory_details_modal"; +import { + BASE_URL, + createItem, + createMember, + deleteItem, + getItems, + getMembers, + updateItem, + updateMember, +} from "./organization_call_backend"; + +// remember to +// cd intermediate_team_4 +// cd client +// npm install swr + +// the interface for the return +export interface organization_clean_backend_calls_return_interface { + data: Inventory_Details_Interface[]; + loading: boolean; + error: Error | null; + isSuccess: boolean; + refresh: KeyedMutator; // Optional refresh function +} + +export interface organization_clean_backend_calls_return_get_members_interface { + data: Member_Details_Interface[]; + loading: boolean; + error: Error | null; + isSuccess: boolean; + refresh: KeyedMutator; // Optional refresh function +} + +export interface organization_clean_backend_calls_return_boolean_members_interface { + data: boolean; + loading: boolean; + error: Error | null; + isSuccess: boolean; + refresh: KeyedMutator; // Optional refresh function +} + +/* +This is the class that will call the backend to get the item data / set the item data / update the item data/ delete the item data + +Remember, this will only be invoked once, when its mounted, if you want to call it peridocally, you need to set up a timer to call it periodically +*/ + +// 1. Define a simple fetcher function (standard for SWR) +// const fetcher = (url: string) => fetch(url).then(res => res.json()); + +// this is the one that will work with the clean function from api call +const fetcher = () => getItems("all"); + +/* +Even if we are calling mocks, we need to SWR + +filterType: d to add to backend url call +isDev: true if we want to mock data +*/ +export const useOrganizationBackendGetItems = ( + filterType: string, +): organization_clean_backend_calls_return_interface => { + // 2. SWR handles the state, the effect, and the async logic + // SWR(key, fetcher, options) + // why it? shows cache data while it fetches new data + // key: API url + // fetcher: a function that returns a promise + // options: common options are: refreshInterval, revalidateOnFocus, revalidateOnReconnect, dedumpingInterval + + // more on fetcher + // This is your fetcher normally + // const fetcher = (url) => fetch(url).then(res => res.json()); + // SWR is like UberEats app + // fetcher is delivery driver that fetches the data + // const { data } = SWR('http://localhost:8000/api/data/', fetcher); + + // returns: data, error, isLoading, isValidating, mutate + // data: the data returned by the fetcher function + // error: the error returned by the fetcher function + // isLoading: true if the data is being fetched, false otherwise + // isValidating: true if the data is being validated, false otherwise + // mutate: a function that triggers a new fetch + + /* + // old + const { data, error, isLoading } = SWR( + `/api/data?type=${filterType}`, + fetcher, + { refreshInterval: 10000 } // This handles the "polling" automatically! + ); + + return { + data: data || [], + loading: isLoading, + error + }; + */ + + // SWR syntax: SWR(key, fetcher, options) + // why do we need an array ? and not just filter type? + // beca this array will become a key ( cache collision prevention ) + // lets say we dont have the base URL + // and we have data and inventory in the same filter type which is Electronics + // const { data } = SWR('Electronics', () => getActivities('Electronics')); + // const { data } = SWR('Electronics', () => getInventory('Electronics')); + // lets say i pull data from data first, it will cache Electronics data + // SWR will think oh, the keys are the same, so lets just the cache + // this is cache collission + + // mutate? what is that, so lets say we do polling every 30seconds and + // ther is an inventory update that happened in between the 30seconds + // SWR will immediately + const { data, error, isLoading, mutate } = SWR( + [`${BASE_URL}`, filterType], // The "Key" (Unique identifier) + fetcher, // The "Fetcher" (Your function) + { + refreshInterval: 30, // Poll every 30 seconds 30000ms + revalidateOnFocus: true, // Refresh when r clicks back into the tab + }, + ); + + const res: organization_clean_backend_calls_return_interface = { + data: data || [], + loading: isLoading, + error: error, + isSuccess: !isLoading && !error, + refresh: mutate, // SWR calls its refresh function "mutate" + }; + + return res; +}; + +// This is now a standard function, NOT a hook +// if it is a hook, it will not work, a hook means swr +export const createItemClean = async ( + itemData: Partial | Inventory_Details_Interface, +): Promise => { + itemData.organization = "Demo Organization"; // Temporary hardcoded value + + // 1. Send the data to Django using your existing createItem function + const response = await createItem(itemData as Inventory_Details_Interface); + + // 2. Return the result + return response; +}; + +export const updateItemClean = async ( + itemData: Partial | Inventory_Details_Interface, +): Promise => { + const id = itemData.id ? itemData.id : 0; + + // 1. Send the data to Django using your existing updateItem function + const response = await updateItem( + id, + itemData as Inventory_Details_Interface, + ); + + // 2. Return the result + return response; +}; + +export const deleteItemClean = async (itemId: number): Promise => { + console.log("Deleting item with ID:", itemId); + // Replace with your actual delete backend call: + return await deleteItem(itemId); + return Promise.resolve(true); +}; + +// MMEBERS section +const fetcher2 = () => getMembers("all"); + +/* +Even if we are calling mocks, we need to SWR + +filterType: d to add to backend url call +isDev: true if we want to mock data +*/ +export const useOrganizationBackendGetMembers = ( + filterType: string, +): organization_clean_backend_calls_return_get_members_interface => { + const { data, error, isLoading, mutate } = SWR( + [`${BASE_URL}`, filterType], // The "Key" (Unique identifier) + fetcher2, // The "Fetcher" (Your function) + { + refreshInterval: 30, // Poll every 30 seconds 30000ms + revalidateOnFocus: true, // Refresh when r clicks back into the tab + }, + ); + + const res: organization_clean_backend_calls_return_get_members_interface = { + data: data || [], + loading: isLoading, + error: error, + isSuccess: !isLoading && !error, + refresh: mutate, // SWR calls its refresh function "mutate" + }; + + return res; +}; +/** + * Standard function to create a new member. + */ +export const createMemberClean = async ( + memberData: Partial | Member_Details_Interface, +): Promise => { + return await createMember(memberData as Member_Details_Interface); +}; + +/** + * Standard function to update an existing member. + */ +export const updateMemberClean = async ( + memberData: Partial | Member_Details_Interface, +): Promise => { + const id = memberData.id ? memberData.id : 0; + return await updateMember(id, memberData as Member_Details_Interface); +}; + +/** + * Standard function to delete a member. + */ +export const deleteMemberClean = async (memberId: number): Promise => { + console.log("Deleting member with ID:", memberId); + // Replace with your actual delete backend call: + // return await deleteMember(memberId); + return Promise.resolve(true); +}; + +// export interface organization_clean_backend_calls_input_interface { +// data: Inventory_Details_Interface[]; +// loading: boolean; +// error: Error | null; +// isSuccess: boolean; // +// refresh: KeyedMutator; // Optional refresh function +// } + +// export const createNewItemClean = ( +// itemData: Inventory_Details_Interface, +// ): organization_clean_backend_calls_return_interface => { + +// // to SWR for PUT +// const { data, error, isLoading, mutate } = SWR( +// [`${BASE_URL}`, "newItem"], +// () => createItem(itemData), +// { +// revalidateOnFocus: true, +// }, +// ); + +// const res: organization_clean_backend_calls_return_interface = { +// data: data || [], +// loading: isLoading, +// error: error, +// isSuccess: !isLoading && !error, +// refresh: mutate, +// }; + +// return res; +// }; diff --git a/client/src/components/ui/button_quick_actions.tsx b/client/src/components/ui/button_quick_actions.tsx new file mode 100644 index 0000000..1b7f8b4 --- /dev/null +++ b/client/src/components/ui/button_quick_actions.tsx @@ -0,0 +1,62 @@ +// src/components/button_quick_actions.tsx + +import Link from "next/link"; +import React from "react"; + +// tmr night, wed night, friday night, sat mornign and sun morning + +const Quick_Actions = () => { + return ( +
+

Quick Actions

+ +
+ {/* 2. RIGHT COLUMN: Quick Actions */} + +
+
+ ); +}; + +export default Quick_Actions; diff --git a/client/src/components/ui/card_organization_inventory_details_modal.tsx b/client/src/components/ui/card_organization_inventory_details_modal.tsx new file mode 100644 index 0000000..ea39cec --- /dev/null +++ b/client/src/components/ui/card_organization_inventory_details_modal.tsx @@ -0,0 +1,123 @@ +// src/components/card_organization_inventory_details_modal.tsx + +/* +Simple modal is overlays + +How does the overlay work? +1. The item itself: which is the trigger, when clicked, it sets the state to open the modal and passes the ID to the panel +2. The panel itself: which just passes the state change and ID to the page +3. The page receives the state sent by panel and sends to the overlay, which since its true, it returns somethign to the page which is the overlay + +Key components +1. item itself which will be clicked on +1.1. A panel to hold the item +2. The page to manage the state and render the overlay +3. The overlay component itself +*/ + +import Link from "next/link"; +import React from "react"; + +import { deleteItemClean } from "./backend/organization_clean_backend_calls"; + +// Define the shape of the data this modal expects +// id? means optional +interface Inventory_Details_Interface { + id?: number; + name: string; + details: string; + categories?: string; + availability?: string; + organization?: string; + collectionPoint: string; + borrowerName?: string; + borrowedOn?: string; + returnedOn?: string; + dueOn?: string; + expiryDate: string; + dateAdded?: string; +} + +// to control the overlay modal visibility and data +interface Inventory_Details_Modal_Interface { + isOpen: boolean; + onClose: () => void; + itemData: Inventory_Details_Interface | null; // Data of the item to display +} + +const Inventory_Details_Modal: React.FC = ({ + isOpen, + onClose, + itemData, +}) => { + // 1. CONTROL: If it's not open or data is missing, render nothing. + if (!isOpen || !itemData) return null; + + return ( + // 2. RENDER: When 'isOpen' is true, this entire structure is placed on the screen. +
+
+ {/*CLOSING: When the close button is clicked, it calls the 'onClose' function + which updates the state in the ActivityPage (Step 1) */} + + + {/* ... Modal content rendered using itemData.name, itemData.details, etc. ... */} +
+

Item ID: {itemData.id}

+

Item Name: {itemData.name}

+

Description: {itemData.details}

+

Category: {itemData.categories}

+

Availability: {itemData.availability}

+

Organization: {itemData.organization}

+

Borrow Location: {itemData.collectionPoint}

+

Borrower Name: {itemData.borrowerName}

+

Borrowed On: {itemData.borrowedOn}

+

Returned On: {itemData.returnedOn}

+

Due On: {itemData.dueOn}

+

Expiry Date: {itemData.expiryDate}

+
+ + {/* NEW ACTION BUTTONS */} +
+ +
+ Returned?
Click me +
+ + + +
Modify
+ + + +
+
+
+ ); +}; + +export type { Inventory_Details_Interface }; // Exporting the interface for use in other files +export default Inventory_Details_Modal; diff --git a/client/src/components/ui/card_organization_inventory_status_card.tsx b/client/src/components/ui/card_organization_inventory_status_card.tsx new file mode 100644 index 0000000..5acdc26 --- /dev/null +++ b/client/src/components/ui/card_organization_inventory_status_card.tsx @@ -0,0 +1,74 @@ +// src/components/card_organization_inventory_status_card.tsx + +/* +This is to hold the inventory status summary cards like Expiring Inventory, Inventory Due, Borrowed Items, Returned Items +- Each card shows a list of items with their status details +- it also requires a list of items to display, passed as props +- Also includes a "View All" link to navigate to the full list page +*/ + +import Link from "next/link"; +import React from "react"; + +import { calcTime } from "@/helpers/helper_functions"; + +import { Inventory_Details_Interface } from "./card_organization_inventory_details_modal"; +import Inventory_Status_Data from "./card_organization_inventory_status_data"; + +interface Inventory_Status_Card_Interface { + title: string; // e.g., "Expiring Inventory" + viewAllHref: string; // The link for the View All button + data: Inventory_Details_Interface[]; // Array of the items to display (using the Inventory_Details_Interface interface) + onItemClick: (data: Inventory_Details_Interface) => void; // Optional click handler for items +} + +const decideWhichDataField = ( + data: Inventory_Details_Interface, + field: string, +): string => { + if (field == "Expiring Inventory") { + return data.expiryDate ? data.expiryDate : "unknown expiry date"; + } else if (field == "Inventory Due") { + return data.dueOn ? data.dueOn : "unknown due date"; + } else if (field == "Borrowed Items") { + return data.borrowedOn ? data.borrowedOn : "unknown borrowed date"; + } else if (field == "Returned Items") { + return data.returnedOn ? data.returnedOn : "unknown returned date"; + } else { + return "unknown date"; + } +}; + +const Inventory_Status_Card: React.FC = ({ + title, + viewAllHref, + data, + onItemClick, +}) => { + return ( +
+ {/* Header with Title and View All Link */} +
+

{title}

+ + View All + +
+ + {/* Item List */} +
+ {data.map((item, index) => ( + + ))} +
+
+ ); +}; + +export default Inventory_Status_Card; diff --git a/client/src/components/ui/card_organization_inventory_status_data.tsx b/client/src/components/ui/card_organization_inventory_status_data.tsx new file mode 100644 index 0000000..e34ac85 --- /dev/null +++ b/client/src/components/ui/card_organization_inventory_status_data.tsx @@ -0,0 +1,41 @@ +// src/components/ui/card_organization_inventory_status_data.tsx + +/* +Idea for this how the recent activities like Recently Borrowed Items will be displayed +- This component represents a single item in the inventory status list, showing the item name and its status detail. + + +card_organization_inventory_status_card.tsx +- uses mulitple of this component to show the list +- the card also is just to hold the data and the View All link +*/ + +import React from "react"; + +import { Inventory_Details_Interface } from "./card_organization_inventory_details_modal"; + +interface Inventory_Status_Data_Interface { + dataName: string; // e.g., "Cool Potato" + dataStatus: string; // e.g., "In 2 days" or "5 mins ago" + + // NEW: A prop that is the function passed from the parent component + onItemClick: (itemData: Inventory_Details_Interface) => void; + data: Inventory_Details_Interface; // The full data object for this specific item +} + +const Inventory_Status_Data: React.FC = ({ + dataName, + dataStatus, + onItemClick, + data, +}) => { + return ( +
onItemClick(data)}> + {dataName} + {dataStatus} +
+ ); +}; + +export type { Inventory_Status_Data_Interface }; +export default Inventory_Status_Data; diff --git a/client/src/components/ui/card_organization_member_details_modal.tsx b/client/src/components/ui/card_organization_member_details_modal.tsx new file mode 100644 index 0000000..9f7cac9 --- /dev/null +++ b/client/src/components/ui/card_organization_member_details_modal.tsx @@ -0,0 +1,136 @@ +// src/components/card_organization_inventory_details_modal.tsx + +/* +Simple modal is overlays + +How does the overlay work? +1. The item itself: which is the trigger, when clicked, it sets the state to open the modal and passes the ID to the panel +2. The panel itself: which just passes the state change and ID to the page +3. The page receives the state sent by panel and sends to the overlay, which since its true, it returns somethign to the page which is the overlay + +Key components +1. item itself which will be clicked on +1.1. A panel to hold the item +2. The page to manage the state and render the overlay +3. The overlay component itself +*/ + +import Link from "next/dist/client/link"; +import React from "react"; + +import { deleteItemClean } from "./backend/organization_clean_backend_calls"; + +// Define the shape of the data this modal expects +// id? means optional +export interface Member_Details_Interface { + // basic info + id?: number; + name: string; + email: string; + phoneNumber?: string; + notes?: string; + + // admin info + permissionLevel?: string; + joinedOn?: string; + isStillHere?: boolean; + + // borrow info + itemsBorrowed?: number; + lastBorrowedOn?: string; + + // friends info + totalFriends?: number; + friends?: Member_Details_Interface[]; + + // clubs info + totalClubs?: number; + clubs?: string[]; +} + +// to control the overlay modal visibility and data +interface Member_Details_Modal_Interface { + isOpen: boolean; + onClose: () => void; + itemData: Member_Details_Interface | null; // Data of the item to display +} + +export const Member_Details_Modal: React.FC = ({ + isOpen, + onClose, + itemData, +}) => { + // 1. CONTROL: If it's not open or data is missing, render nothing. + if (!isOpen || !itemData) return null; + + return ( + // 2. RENDER: When 'isOpen' is true, this entire structure is placed on the screen. +
+
+ {/*CLOSING: When the close button is clicked, it calls the 'onClose' function + which updates the state in the ActivityPage (Step 1) */} + + + {/* ... Modal content rendered using itemData.name, itemData.details, etc. ... */} +
+

Item ID: {itemData.id}

+

Member Name: {itemData.name}

+

Email: {itemData.email}

+

Phone Number: {itemData.phoneNumber}

+

Notes: {itemData.notes}

+

Permission Level: {itemData.permissionLevel}

+

Joined On: {itemData.joinedOn}

+

Is Still Here: {itemData.isStillHere ? "Yes" : "No"}

+

Items Borrowed: {itemData.itemsBorrowed}

+

Last Borrowed On: {itemData.lastBorrowedOn}

+

Total Friends: {itemData.totalFriends}

+
+

Friends:

+
    + {itemData.friends && + itemData.friends.map((friend, index) => ( +
  • + {friend.name} ({friend.email}) +
  • + ))} +
+
+

Total Clubs: {itemData.totalClubs}

+
+

Clubs:

+
    + {itemData.clubs && + itemData.clubs.map((club, index) => ( +
  • {club}
  • + ))} +
+
+
+ + {/* NEW ACTION BUTTONS */} +
+ +
Modify
+ + + +
+
+
+ ); +}; diff --git a/client/src/components/ui/card_organization_member_management.tsx b/client/src/components/ui/card_organization_member_management.tsx new file mode 100644 index 0000000..907b8ac --- /dev/null +++ b/client/src/components/ui/card_organization_member_management.tsx @@ -0,0 +1,31 @@ +import React from "react"; + +import { Member_Details_Interface } from "./card_organization_member_details_modal"; + +// 1. Define the shape of the data this modal expects +interface Member_Management_Interface { + onItemClick: (data: Member_Details_Interface) => void; + itemData: Member_Details_Interface; // Data of the item to display +} + +// 2. The Functional Component +const Card_Organization_Member_Management: React.FC< + Member_Management_Interface +> = ({ onItemClick, itemData }) => { + return ( +
onItemClick(itemData)} + style={{ cursor: "pointer" }} + > +

{itemData.name}

+

{itemData.email}

+

{itemData.joinedOn}

+ + {itemData.permissionLevel || "Member"} + +
+ ); +}; + +export default Card_Organization_Member_Management; diff --git a/client/src/components/ui/card_organization_recent_activities_item.tsx b/client/src/components/ui/card_organization_recent_activities_item.tsx new file mode 100644 index 0000000..999215c --- /dev/null +++ b/client/src/components/ui/card_organization_recent_activities_item.tsx @@ -0,0 +1,67 @@ +// src/components/card_organization_recent_activities_item.tsx + +import React from "react"; + +import { Inventory_Details_Interface } from "./card_organization_inventory_details_modal"; + +// Define the shape of the data for a single activity item +interface Recent_Activities_Item_Interface { + type: "increase" | "decrease" | "member" | null; + status: "up" | "down"; + time: string; + + data: Inventory_Details_Interface; + + onItemClick: (data: Inventory_Details_Interface) => void; // Click handler is mandatory here to work +} + +const Recent_Activities_Item: React.FC = ({ + type, + status, + time, + data, + + onItemClick, +}) => { + // Logic to determine arrow symbol and color + const arrow = status === "up" ? "↑" : "↓"; + + // Use CSS Variables defined in organization.css + const statusColor = + status === "up" ? "var(--color-success)" : "var(--color-error)"; + + // Format quantity to include + or - sign + // const formattedQuantity = `${quantity > 0 ? "+" : ""}${quantity}`; + + return ( + /* When clicked, it sends its specific 'id' back up the chain to the Page */ +
onItemClick(data)} + style={{ cursor: "pointer" }} + > +
+ {arrow} +
+ +
+

{data.name}

+

{data.details}

+

{type}

+
+ +
+ {time} +
+
+ ); +}; + +export default Recent_Activities_Item; + +/* + + + {formattedQuantity} + +*/ diff --git a/client/src/components/ui/card_organization_recent_activities_panel.tsx b/client/src/components/ui/card_organization_recent_activities_panel.tsx new file mode 100644 index 0000000..36b9f3c --- /dev/null +++ b/client/src/components/ui/card_organization_recent_activities_panel.tsx @@ -0,0 +1,67 @@ +// src/components/card_organization_recent_activities_panel.tsx + +/* +How does it show all the components in the list? +1. pass the list of data from the mockActivityData array +2. use the map function to iterate over each item in the array +3. for each item, render a RecentActivityItem component, passing the relevant props + +so what happens is, it will run the map and iterate over every item and then create a RecentActivityItem component +for each one, passing in the data and placing it under the div with class activity-list + +Note: +the data must already be sorted from the backend before passing the data here. this componenet is just for display. no processing is done here + +*/ + +import Link from "next/link"; // For the 'View All' link +import React from "react"; + +import { calcTime } from "@/helpers/helper_functions"; + +import { Inventory_Details_Interface } from "./card_organization_inventory_details_modal"; +import Recent_Activities_Item from "./card_organization_recent_activities_item"; + +// Define the interface for the Panel so it knows it receives onItemClick +interface Recent_Activity_Panel_Interface { + onItemClick: (data: Inventory_Details_Interface) => void; + data: Inventory_Details_Interface[]; +} + +const Recent_Activity_Panel: React.FC = ({ + onItemClick, + data, +}) => { + return ( +
+ {/* Header with Title and View All Link */} +
+

Recent Activity

+ + View All + +
+ + {/* The Activity List */} + {/* react requires a key for each item in list */} +
+ { + // Use the JavaScript map function to render one RecentActivityItem for each data entry + data.map((item, index) => ( + 0.5 ? "up" : "down"} // Random status for demo + time={calcTime(item.dateAdded)} // Placeholder, adjust as needed + data={item} + // PASS THE HANDLER DOWN TO THE ITEM + onItemClick={onItemClick} + /> + )) + } +
+
+ ); +}; + +export default Recent_Activity_Panel; diff --git a/client/src/components/ui/card_organization_statistics.tsx b/client/src/components/ui/card_organization_statistics.tsx new file mode 100644 index 0000000..8881fc2 --- /dev/null +++ b/client/src/components/ui/card_organization_statistics.tsx @@ -0,0 +1,67 @@ +// src/components/ui/Statistics_Card.tsx + +import React from "react"; + +/* +Interface +This component represents a single statistics card used in the organization dashboard. +So when you use this interaface, you can put in this data + +example usage: + +*/ +interface Statistics_Card_Interface { + title: string; // The card title (e.g., "Total Inventory") + value: number; // The main numerical value (e.g., 30) + delta: string; // The change string (e.g., "+3 this week") + status: "up" | "down"; // Controls the color and direction of the arrow +} + +/* +React.FC +- React.FC<> stands for React Functional Component + +{ title, value, delta, status } +- Destructuring the props object to directly access title, value, delta, and status +*/ +const Statistics_Card: React.FC = ({ + title, + value, + delta, + status, +}) => { + // Logic to determine the arrow character and color + const arrow = status === "up" ? "↑" : "↓"; + // Using CSS variables for colors defined in organization.css + // var() is a native function for css + const deltaColor = + status === "up" ? "var(--color-success)" : "var(--color-error)"; + + return ( + // The main gray box container +
+ {/* Arrow Indicator at the top right */} + {/* why {{ instead of {} }}, that is because, first is to go in to js script, second is that style only accepts js objects*/} +
+ {arrow} +
+ + {/* Main Content */} +
+

{title}

+

{value}

+ {/* why {{ instead of {} }}, that is because, first is to go in to js script, second is that style only accepts js objects*/} +

+ {delta} +

+
+
+ ); +}; + +export default Statistics_Card; diff --git a/client/src/components/ui/card_organization_whole_inventory.tsx b/client/src/components/ui/card_organization_whole_inventory.tsx new file mode 100644 index 0000000..79070c6 --- /dev/null +++ b/client/src/components/ui/card_organization_whole_inventory.tsx @@ -0,0 +1,28 @@ +import React from "react"; + +import { Inventory_Details_Interface } from "./card_organization_inventory_details_modal"; + +// 1. Define the shape of the data this modal expects +interface Whole_Inventory_Modal_Interface { + onItemClick: (data: Inventory_Details_Interface) => void; + itemData: Inventory_Details_Interface; // Data of the item to display +} + +// 2. The Functional Component +const Card_Organization_Whole_Inventroy: React.FC< + Whole_Inventory_Modal_Interface +> = ({ onItemClick, itemData }) => { + return ( +
onItemClick(itemData)} + style={{ cursor: "pointer" }} + > +

{itemData.name}

+

{itemData.details}

+ {itemData.categories || "General"} +
+ ); +}; + +export default Card_Organization_Whole_Inventroy; diff --git a/client/src/components/ui/navbar_organization.tsx b/client/src/components/ui/navbar_organization.tsx new file mode 100644 index 0000000..6268f89 --- /dev/null +++ b/client/src/components/ui/navbar_organization.tsx @@ -0,0 +1,99 @@ +// src/components/Header.tsx + +/* +How does the dropdown meny work? +1. the menu is hidden (isDropdownOpen = false) +2. when the user clicks on the profile area, the toggleDropdown function is called, which toggles the state to true, +making the menu visible by setting isDropdownOpen to true. + +Common issues: +1. The menu closes immediately when clicking on it. because you use onClick instead of MouseDown + user starts a click with OnClick and the browser processes the entire click event, which makes the wrapper in focus state, + and since the menu exist now ( on True ), it triggers the onBlur event, closing the menu. + +useState() +- const [isDropdownOpen, setIsDropdownOpen] = useState(false); + ^ variable ^ setter function ^ initial state +- used to add states in functional components. + +onBlur +- An event that occurs when an element loses focus or not the main thing the user will interact with. +- Here, it is used to close the dropdown menu when the user clicks outside of it. + +onMouseDown vs onClick +- onMouseDown: Triggered when the mouse button is pressed down. +- onClick: Triggered when the mouse button is pressed and released. +- In this case, onMouseDown is used to toggle the dropdown menu to ensure it opens before any blur event can occur. + +*/ + +// this is for the dropdown menu from user profile side +import Link from "next/link"; +import React, { useState } from "react"; // <-- Import useState + +const Header = () => { + // 1. Initialize state for the dropdown visibility + const [isDropdownOpen, setIsDropdownOpen] = useState(false); + + // Helper functions to control the state + const toggleDropdown = () => setIsDropdownOpen((prev) => !prev); + + // We keep the handleBlur logic simple (for when the user clicks *outside* the entire element) + const handleBlur = () => { + // A small delay is still necessary to ensure the menu's buttons are clickable + // before the menu hides. + setTimeout(() => { + setIsDropdownOpen(false); + }, 100); + }; + + return ( +
+ {/* Left side: Logo */} +
+ Logo +
+ {/* Center: Navigation Links */} + + + {/* Right side: User Profile */} +
+
+
+ User +
+ + {isDropdownOpen && ( +
e.preventDefault()} + > + + +
+ )} +
+
+ ); +}; + +export default Header; diff --git a/client/src/components/ui/simple_modals.tsx b/client/src/components/ui/simple_modals.tsx new file mode 100644 index 0000000..cb5d150 --- /dev/null +++ b/client/src/components/ui/simple_modals.tsx @@ -0,0 +1,91 @@ +// src/components/Simple_Modal.tsx +import React from "react"; + +/* +3 parts to an overlay + +part 1) +The idea is this component act as the overlay +- So this overlay will only be rendered if the switch is ON (isOpen = true) +- It will have a button inside it that will call the onClose function to turn the switch OFF + +part 2.1) +The page will have the switch state (isOpen) and the function to turn it OFF (onClose) with useState +- it will have a div with an onClick + +part 2.2) +The page will also have a div, with the onClick that will turn on and off the swtich +- it will also have +*/ + +interface Simple_Modal_Props_Interface { + isOpen: boolean; + onClose: () => void; // This is the function to flip the state back to false +} + +const Simple_Modal: React.FC = ({ + isOpen, + onClose, +}) => { + // If isOpen is false, the code below this line never runs + if (!isOpen) return null; + + return ( +
+
+

Study Overlay

+

This is a manual test of the overlay system.

+ + {/* The X Button */} + +
+
+ ); +}; + +export default Simple_Modal; + +/* +Use this code + + + +// src/pages/activity/all.tsx +import React, { useState } from 'react'; +import SimpleModal from '../../components/SimpleModal'; + +const ActivityPage = () => { + // 1. Define the "Light Switch" state. Default is 'false' (closed). + const [showStudyModal, setShowStudyModal] = useState(false); + + return ( +
+

Activity Page

+ + 2. The Button that turns the switch ON + + + 3. Place the Modal component here. + We pass the current state (showStudyModal) + and the function to turn it OFF (setShowStudyModal) + setShowStudyModal(false)} + /> +
+ ); +}; + + + +*/ diff --git a/client/src/helpers/helper_functions.tsx b/client/src/helpers/helper_functions.tsx new file mode 100644 index 0000000..793b27c --- /dev/null +++ b/client/src/helpers/helper_functions.tsx @@ -0,0 +1,43 @@ +export const calcTime = (dateString: string | undefined): string => { + if (!dateString) return "unknown time"; + + try { + const date = new Date(dateString); + if (isNaN(date.getTime())) throw new Error("Invalid date"); + + const now = new Date(); + const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000); + + // 1. Check if the date is in the FUTURE + const isFuture = diffInSeconds < 0; + const absDiff = Math.abs(diffInSeconds); // Use Absolute Value to remove the '-' + + // 2. Helper to format the string + const formatLabel = (value: number, unit: string) => { + return isFuture ? `in ${value} ${unit}` : `${value} ${unit} ago`; + }; + + // 3. Logic for relative time using the absolute difference + if (absDiff < 60) return formatLabel(absDiff, "seconds"); + + const diffInMinutes = Math.floor(absDiff / 60); + if (diffInMinutes < 60) return formatLabel(diffInMinutes, "minutes"); + + const diffInHours = Math.floor(diffInMinutes / 60); + if (diffInHours < 24) return formatLabel(diffInHours, "hours"); + + const diffInDays = Math.floor(diffInHours / 24); + return formatLabel(diffInDays, "days"); + } catch (error) { + return "invalid date: " + error; + } +}; + +export const formatDateForInput = (dateString: string | undefined): string => { + if (!dateString) return ""; + const d = new Date(dateString); + if (isNaN(d.getTime())) return ""; + + // This extracts the "2026-01-07" part of an ISO string + return d.toISOString().split("T")[0]; +}; diff --git a/client/src/hooks/old_notes.txt b/client/src/hooks/old_notes.txt new file mode 100644 index 0000000..81b3919 --- /dev/null +++ b/client/src/hooks/old_notes.txt @@ -0,0 +1,169 @@ +/* to put inside the _app.js, it will set a global fetch + +import { SWRConfig } from 'swr'; + +function MyApp({ Component, pageProps }) { + return ( + fetch(resource, init).then(res => res.json()), + refreshInterval: 10000, // Optional: Poll for new data every 10 seconds globally + }} + > + + + ); +} + +*/ + +/* + +// filterType: used to add to backend url call +// isDev: true if we want to use mock data + +export const getRecentActivities = (filterType: string, mode: string ) => { + + if (isDev) + { + + } + else + { + // 2. SWR handles the state, the effect, and the async logic + // useSWR(key, fetcher, options) + // why use it? shows cache data while it fetches new data + // key: API url + // fetcher: a function that returns a promise + // options: common options are: refreshInterval, revalidateOnFocus, revalidateOnReconnect, dedumpingInterval + + // more on fetcher + // This is your fetcher normally + // const fetcher = (url) => fetch(url).then(res => res.json()); + // useSWR is like UberEats app + // fetcher is delivery driver that fetches the data + // const { data } = useSWR('http://localhost:8000/api/data/', fetcher); + + // returns: data, error, isLoading, isValidating, mutate + // data: the data returned by the fetcher function + // error: the error returned by the fetcher function + // isLoading: true if the data is being fetched, false otherwise + // isValidating: true if the data is being validated, false otherwise + // mutate: a function that triggers a new fetch + + const { data, error, isLoading } = useSWR( + `/api/data?type=${filterType}`, + fetcher, + { refreshInterval: 10000 } // This handles the "polling" automatically! + ); + + return { + data: data || [], + loading: isLoading, + error + }; + } +}; +*/ + +// // 2. "Cleaning" and "Operations" (Sorting/Formatting) +// const cleanedData = json.map((item: any) => ({ +// id: item.id, +// title: item.action_name, // Mapping Django snake_case to Frontend camelCase +// detail: item.target_object, +// time: formatMyDate(item.timestamp), // Formatting logic +// status: item.change_type === 'increase' ? 'up' : 'down' +// })); + +// setData(cleanedData); + +/* How to use swr original without global fetch + +import useSWR from 'swr'; + +// 1. Define a simple fetcher function (standard for SWR) +const fetcher = (url: string) => fetch(url).then(res => res.json()); + +export const useActivities = (filterType: string) => { + // 2. SWR handles the state, the effect, and the async logic + // useSWR(key, fetcher, options) + // why use it? shows cache data while it fetches new data + // key: API url + // fetcher: a function that returns a promise + // options: common options are: refreshInterval, revalidateOnFocus, revalidateOnReconnect, dedumpingInterval + + // more on fetcher + // This is your fetcher normally + // const fetcher = (url) => fetch(url).then(res => res.json()); + // useSWR is like UberEats app + // fetcher is delivery driver that fetches the data + // const { data } = useSWR('http://localhost:8000/api/data/', fetcher); + + // returns: data, error, isLoading, isValidating, mutate + // data: the data returned by the fetcher function + // error: the error returned by the fetcher function + // isLoading: true if the data is being fetched, false otherwise + // isValidating: true if the data is being validated, false otherwise + // mutate: a function that triggers a new fetch + + const { data, error, isLoading } = useSWR( + `/api/data?type=${filterType}`, + fetcher, + { refreshInterval: 10000 } // This handles the "polling" automatically! + ); + + return { + data: data || [], + loading: isLoading, + error + }; +}; + + + + + + +//This function is used to invoke the get/post/put/delete calls to the backend for the data data +const useActivities = (filterType: string) => { + // this is the data from the backend + const [data, setData] = useState([]); + + // this is to update the state of the data + const [loading, setLoading] = useState(true); + + // runs code on the backend as a side effect + // the idea is, to useEffect, is used to decide on the timing + // bridge between react rendering and calling backend + // makes sure that the code rendering is not blocked by the backend call + // it will rerun if the second parameter changes useEffect(() => {}, [second parameter]) + // it can also stop backend task + useEffect(() => { + + // async and await + // tells js how to handle task that takes time to finish + // so how does the 2 pair? + // when the filterType ( second param ) changes, triggers useEffect + // useEffect calls fetchData + // fetchData is async, so it starts the task and moves on + // when fetchData finishes, it updates the state of data and loading + const fetchData = async () => { + setLoading(true); + + // 1. Fetch from Django + // const response = await fetch(`http://localhost:8000/api/data/?type=${filterType}`); + // const json = await response.json(); + + // setData(cleanedData); + // setLoading(false); + + + setLoading(false); + }; + + fetchData(); + }, [filterType]); // Re-run if the filter changes + + return { data, loading }; +}; +*/ \ No newline at end of file diff --git a/client/src/mocks/Activity_Items_Mocks.tsx b/client/src/mocks/Activity_Items_Mocks.tsx new file mode 100644 index 0000000..0e8a4aa --- /dev/null +++ b/client/src/mocks/Activity_Items_Mocks.tsx @@ -0,0 +1,12 @@ +// --- MOCK DATA FOR THE SUMMARY CARDS --- +export const mockInventoryItems = [ + { itemName: "Cool Potato", statusDetail: "In 2 days" }, + { itemName: "Cool Potato", statusDetail: "In 2 days" }, + { itemName: "Cool Potato", statusDetail: "In 2 days" }, +]; + +export const mockBorrowedItems = [ + { itemName: "Cool Potato", statusDetail: "5 mins ago" }, + { itemName: "Cool Potato", statusDetail: "5 mins ago" }, + { itemName: "Cool Potato", statusDetail: "5 mins ago" }, +]; diff --git a/client/src/mocks/Inventory_Details_Interface_Mocks.tsx b/client/src/mocks/Inventory_Details_Interface_Mocks.tsx new file mode 100644 index 0000000..7c2aa19 --- /dev/null +++ b/client/src/mocks/Inventory_Details_Interface_Mocks.tsx @@ -0,0 +1,253 @@ +import { Inventory_Details_Interface } from "../components/ui/card_organization_inventory_details_modal"; + +const names = [ + "Cool Potato", + "Heavy Duty Drill", + "First Aid Kit", + "Projector B", + "Foldable Chair", +]; +const details = [ + "A very cool potato", + "High-speed masonry drill", + "Fully stocked medical kit", + "4K Office projector", + "Standard seating", +]; +const categories = [ + "Food item", + "Power Tools", + "Safety", + "Electronics", + "Furniture", +]; +const locations = [ + "UWA Crawley", + "Guild Storage", + "Reid Library", + "Engineering Block", +]; +const users = ["Arush", "Cody", "Jane Doe", "Alex Smith", "System"]; + +/* + Generates a random Inventory_Details_Interface object + */ + +// 1. Helper Function: Generates a random date string +const getRandomDateTime = (start: Date, end: Date): string => { + const date = new Date( + start.getTime() + Math.random() * (end.getTime() - start.getTime()), + ); + + // Formats to e.g., "10 Nov 2025, 14:30" + return date.toLocaleString("en-GB", { + day: "2-digit", + month: "short", + year: "numeric", + hour: "2-digit", + minute: "2-digit", + hour12: false, // Use true for AM/PM + }); +}; + +// 2. Mock data for the overlay fields +const now = new Date(); +// const yesterday = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1); +const lastMonth = new Date( + now.getFullYear(), + now.getMonth() - 1, + now.getDate(), +); +const nextYear = new Date(now.getFullYear() + 1, now.getMonth(), now.getDate()); + +// --- Random time within TODAY --- +// const startOfToday = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0); +// const endOfToday = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59); + +// const startHour = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 10, 0); +// const endHour = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 1, 59); + +// const oneHourAgo = new Date(now.getTime() - (60 * 60 * 1000)); // Current time minus 3,600,000 milliseconds +const currentTime = now; + +const oneMinuteAgo = new Date(now.getTime() - 1 * 60 * 1000); // Current time minus 60,000 milliseconds + +// const randomTimeToday = getRandomDateTime(startOfToday, endOfToday); + +export const generateRandomMockInventoryDetails = + (): Inventory_Details_Interface => { + // Helper to pick a random item from an array + const getRandom = (arr: string[]) => + arr[Math.floor(Math.random() * arr.length)]; + + const id = Math.floor(Math.random() * 10); // Random ID between 0-9 + + return { + id: id, // Random ID between 0-9 + name: getRandom(names), + details: getRandom(details), + categories: getRandom(categories), + availability: Math.random() > 0.5 ? "Available" : "Borrowed", + organization: "Coders For Cause", + collectionPoint: getRandom(locations), + borrowerName: getRandom(users), + + // RANDOMLY GENERATED DATES: + borrowedOn: getRandomDateTime(lastMonth, now), // Somewhere in the last 30 days + returnedOn: getRandomDateTime(now, now), // Today + dueOn: getRandomDateTime(now, nextYear), // Somewhere in the next year + expiryDate: getRandomDateTime(now, nextYear), // Somewhere in the next year + dateAdded: getRandomDateTime(oneMinuteAgo, currentTime), // Somewhere between yesteday and today + }; + }; + +/* + // Example data structure that the component might use (or receive as props later) +const mockActivityData = [ + { + id: 1, + type: "member" as const, + status: "up" as const, + title: "New Member", + detail: "Cody", + quantity: 1, + time: "5 minutes ago", + }, + { + id: 2, + type: "member" as const, + status: "up" as const, + title: "Inventory increase", + detail: "Cool Potato", + quantity: 1, + time: "5 minutes ago", + }, + { + id: 3, + type: "member" as const, + status: "down" as const, + title: "Lost Member", + detail: "Cody", + quantity: -1, + time: "3 minutes ago", + }, + { + id: 4, + type: "member" as const, + status: "down" as const, + title: "Lost Member", + detail: "Cody", + quantity: -1, + time: "3 minutes ago", + }, + { + id: 5, + type: "member" as const, + status: "down" as const, + title: "Lost Member", + detail: "Cody", + quantity: -1, + time: "3 minutes ago", + }, + { + id: 6, + type: "member" as const, + status: "down" as const, + title: "Lost Member", + detail: "Cody", + quantity: -1, + time: "3 minutes ago", + }, + { + id: 7, + type: "member" as const, + status: "down" as const, + title: "Lost Member", + detail: "Cody", + quantity: -1, + time: "3 minutes ago", + }, + { + id: 8, + type: "member" as const, + status: "down" as const, + title: "Lost Member", + detail: "Cody", + quantity: -1, + time: "3 minutes ago", + }, + { + id: 9, + type: "member" as const, + status: "down" as const, + title: "Lost Member", + detail: "Cody", + quantity: -1, + time: "3 minutes ago", + }, + { + id: 10, + type: "member" as const, + status: "down" as const, + title: "Lost Member", + detail: "Cody", + quantity: -1, + time: "3 minutes ago", + }, + { + id: 11, + type: "member" as const, + status: "down" as const, + title: "Lost Member", + detail: "Cody", + quantity: -1, + time: "3 minutes ago", + }, + { + id: 12, + type: "member" as const, + status: "down" as const, + title: "Lost Member", + detail: "Cody", + quantity: -1, + time: "3 minutes ago", + }, + { + id: 13, + type: "member" as const, + status: "down" as const, + title: "Lost Member", + detail: "Cody", + quantity: -1, + time: "3 minutes ago", + }, + { + id: 14, + type: "member" as const, + status: "down" as const, + title: "Lost Member", + detail: "Cody", + quantity: -1, + time: "3 minutes ago", + }, + { + id: 15, + type: "member" as const, + status: "down" as const, + title: "Lost Member", + detail: "Cody", + quantity: -1, + time: "3 minutes ago", + }, + { + id: 16, + type: "member" as const, + status: "down" as const, + title: "Lost Member", + detail: "Cody", + quantity: -1, + time: "3 minutes ago", + }, + // Add more items here... +]; +*/ diff --git a/client/src/mocks/Members_Details_Interface_Mocks.tsx b/client/src/mocks/Members_Details_Interface_Mocks.tsx new file mode 100644 index 0000000..7ef2487 --- /dev/null +++ b/client/src/mocks/Members_Details_Interface_Mocks.tsx @@ -0,0 +1,31 @@ +import type { Members_Details_Interface } from "../pages/organization_member_management"; + +export const generateMockMember = (): Members_Details_Interface => { + const names = [ + "Alice Johnson", + "Bob Smith", + "Charlie Davis", + "Diana Prince", + "Ethan Hunt", + ]; + const levels = ["Admin", "Moderator", "Member", "Guest"]; + + return { + id: Math.floor(Math.random() * 10000), + name: names[Math.floor(Math.random() * names.length)], + email: `user${Math.floor(Math.random() * 10000)}@example.com`, + phoneNumber: `+65 ${Math.floor(Math.random() * 90000000 + 10000000)}`, + notes: "Regular contributor to the community garden project.", + + permissionLevel: levels[Math.floor(Math.random() * levels.length)], + joinedOn: "12 Oct 2023, 09:15", // You can use your getRandomDateTime here + isStillHere: Math.random() > 0.2, // 80% chance they are still here + + itemsBorrowed: Math.floor(Math.random() * 15), + lastBorrowedOn: "01 Jan 2026, 14:30", + + totalFriends: Math.floor(Math.random() * 50), + totalClubs: Math.floor(Math.random() * 5), + clubs: ["Chess Club", "Hiking Society", "Book Worms"], + }; +}; diff --git a/client/src/pages/_app.tsx b/client/src/pages/_app.tsx index 628e9f2..f54dcc9 100644 --- a/client/src/pages/_app.tsx +++ b/client/src/pages/_app.tsx @@ -1,4 +1,5 @@ import "@/styles/globals.css"; +import "../styles/organization.css"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; diff --git a/client/src/pages/organization_activity.tsx b/client/src/pages/organization_activity.tsx new file mode 100644 index 0000000..16a4026 --- /dev/null +++ b/client/src/pages/organization_activity.tsx @@ -0,0 +1,227 @@ +// src/pages/organization_activity.tsx + +import Head from "next/head"; +import { useState } from "react"; + +import { + organization_clean_backend_calls_return_interface, + useOrganizationBackendGetItems, +} from "@/components/ui/backend/organization_clean_backend_calls"; +import QuickActions from "@/components/ui/button_quick_actions"; +import Inventory_Details_Modal, { + Inventory_Details_Interface, +} from "@/components/ui/card_organization_inventory_details_modal"; +import Inventory_Status_Card from "@/components/ui/card_organization_inventory_status_card"; +import Recent_Activity_Panel from "@/components/ui/card_organization_recent_activities_panel"; + +import NavbarOrganization from "../components/ui/navbar_organization"; + +// destructuring with colons will rename the variables +const Organization_Activity_Page = () => { + // for the recent activity panel + const { + // renmaes data -> allData + data: allData, + loading: allLoading, + error: allError, + refresh: allRefresh, + }: organization_clean_backend_calls_return_interface = useOrganizationBackendGetItems( + "all", + ); + + console.log("Data from useOrganizationBackendGetItems:", allData); + console.log("Loading state:", allLoading); + console.log("Error state:", allError); + console.log("Refresh function:", allRefresh); + + // for the statistic cards // + const { + data: expiringData, + loading: expiringLoading, + error: expiringError, + refresh: expiringRefresh, + }: organization_clean_backend_calls_return_interface = useOrganizationBackendGetItems( + "Expiring", + ); + + console.log("Data from useOrganizationBackendGetItems:", expiringData); + console.log("Loading state:", expiringLoading); + console.log("Error state:", expiringError); + console.log("Refresh function:", expiringRefresh); + + const { + data: dueData, + loading: dueLoading, + error: dueError, + refresh: dueRefresh, + }: organization_clean_backend_calls_return_interface = useOrganizationBackendGetItems( + "Due", + ); + + console.log("Data from useOrganizationBackendGetItems:", dueData); + console.log("Loading state:", dueLoading); + console.log("Error state:", dueError); + console.log("Refresh function:", dueRefresh); + + const { + data: borrowedData, + loading: borrowedLoading, + error: borrowedError, + refresh: borrowedRefresh, + }: organization_clean_backend_calls_return_interface = useOrganizationBackendGetItems( + "Borrowed", + ); + + console.log("Data from useOrganizationBackendGetItems:", borrowedData); + console.log("Loading state:", borrowedLoading); + console.log("Error state:", borrowedError); + console.log("Refresh function:", borrowedRefresh); + + const { + data: returnedData, + loading: returnedLoading, + error: returnedError, + refresh: returnedRefresh, + }: organization_clean_backend_calls_return_interface = useOrganizationBackendGetItems( + "Returned", + ); + + console.log("Data from useOrganizationBackendGetItems:", returnedData); + console.log("Loading state:", returnedLoading); + console.log("Error state:", returnedError); + console.log("Refresh function:", returnedRefresh); + + // end of statistic cards // + + // This is for the overlay modal + const [isModalOpen, setIsModalOpen] = useState(false); + const [selectedItemData, setSelectedItemData] = + useState(null); + + /* + We will need to have a function here as well to handle the getting of the data + the onCLick handler is at this level because the modal is here + */ + const handleItemClick = (data: Inventory_Details_Interface) => { + console.log("Item clicked: ", data); + + // setSelectedItemData(generateRandomMockInventoryDetails()); + setSelectedItemData(data); + + // to open the modal + setIsModalOpen(true); + + // PENDING, remove after figuring out how to pass the correct data + console.log("Selected Item Data:", selectedItemData); + }; + + return ( + <> + + Activity Log - Full View + + +
+ + + {/* 2. @See card_organization_inventory_status_data to understand how it works */} +
+ {/* NEW SECTION: Inventory Status Cards (Summary/Filters) */} +
+ + + + +
+ + {/* Two-Column Layout for Activity and Actions */} +
+ {/* LEFT COLUMN: Recent Activity, used section here to group some assets */} +
+ +
+ + {/* 2. RIGHT COLUMN: Quick Actions, aside here is used for accessibility, it does not make it appear on the right*/} + +
+
+
+ {/* RENDER THE MODAL HERE, Remember to send the data of the item here as well*/} + setIsModalOpen(false)} + itemData={selectedItemData} + /> + + ); +}; + +export default Organization_Activity_Page; + +/* + 1. TOP SECTION: Filters and Search +
+ Temporary Filter/Search Placeholders + + + +
+ + 2. MAIN SECTION: Activity List +
+
+ { + mockFullActivityData.map((item, index) => ( + // Reusing the modular component + + )) + } +
+
+ + + 3. BOTTOM SECTION: Pagination +
+ + Page 1 of 10 + + +
+*/ diff --git a/client/src/pages/organization_add_member.tsx b/client/src/pages/organization_add_member.tsx new file mode 100644 index 0000000..ad2e32a --- /dev/null +++ b/client/src/pages/organization_add_member.tsx @@ -0,0 +1,214 @@ +// 1. Swap react-router-dom for next/router +import { useRouter } from "next/router"; +import React, { useState } from "react"; +import { useSWRConfig } from "swr/_internal"; + +import { BASE_URL } from "@/components/ui/backend/organization_call_backend"; +// import { createItem } from '../hooks/organization_call_backend'; +import { + createMemberClean, + updateMemberClean, +} from "@/components/ui/backend/organization_clean_backend_calls"; +import { Member_Details_Interface } from "@/components/ui/card_organization_member_details_modal"; +import Header from "@/components/ui/navbar_organization"; + +const Organization_Add_Member = () => { + // get the query from the other buttons + const { mode, data } = useRouter().query; + + // covnert the data back from json + const parsedData: Member_Details_Interface | null = data + ? JSON.parse(data as string) + : null; + + // it creates a page stack that we can use to navigate + // when to use and should we replace all href with router.push? + // we only use this when we want to have buttons that go back to certain pages or after finishing a task + // we use Link href when we want to move to static pages + const router = useRouter(); + + // this is the form data management state + // so that meaans itsl like a clipboard, at the start its empty, but when the user writes, it updates + // we use partial so that we dont ahve to include each field at the start + const [formData, setFormData] = useState>({ + // id is excluded because it will be auto generated by backend + // id: number, + name: mode === "add" ? "" : parsedData?.name, + email: mode === "add" ? "" : parsedData?.email, + phoneNumber: mode === "add" ? "" : parsedData?.phoneNumber, + notes: mode === "add" ? "" : parsedData?.notes, + + // organization is kept away because we will fix it to the name of the organization that is logged in + // organization: "", + + // joinedOn is kept away because we will fix it to the current date + // joinedOn: "", + + // isStillHere is kept away because we will fix it to true + // isStillHere: "", + + // itemsBorrowed is kept away because we will fix it to 0 + // itemsBorrowed: "", + + // lastBorrowedOn is kept away because we will fix it to the current date + // lastBorrowedOn: "", + + // totalFriends is kept away because we will fix it to 0 + // totalFriends: "", + + // friends is kept away because we will fix it to an empty array + // friends: "", + + // totalClubs is kept away because we will fix it to 0 + // totalClubs: "", + + // clubs is kept away because we will fix it to an empty array + // clubs: "", + }); + + // used whenever you type in the form fields + // e is event, and we get the name and value from the target of the event + const handleChange = ( + // so only handle changes in input or textarea elements + e: React.ChangeEvent, + ) => { + // name is the name of the element like details field + // value is the input + const { name, value } = e.target; + + // it copies the exisiting form data and updates the field that changed\ + // use brackets to use a variable as the key name + // if no brackets, it would just update the name field + setFormData({ ...formData, [name]: value }); + }; + + // this is to get the refresh function from SWR + const { mutate } = useSWRConfig(); // 1. Initiate at the TOP + + const handleSave = async (e: React.FormEvent) => { + // this is to prevent the page from emptying all the fields and refreshing + e.preventDefault(); + + try { + if (mode === "add" && parsedData) { + // 2. Call the action (The "Messenger") + await createMemberClean({ ...formData } as Member_Details_Interface); + + // 3. Tell SWR to refresh the Dashboard data + // This tells SWR: "The data at BASE_URL is old, please go get the new list!" + mutate([`${BASE_URL}`, "all"]); + + alert("Member created!"); + } else { + // 2. Call the action (The "Messenger") + await updateMemberClean(formData); + + // 3. Tell SWR to refresh the Dashboard data + // This tells SWR: "The data at BASE_URL is old, please go get the new list!" + mutate([`${BASE_URL}`, "all"]); + + alert("Member updated!"); + } + + // go back to dashboard + // router.push('/organization_dashboard'); + router.back(); + } catch (error) { + alert("Failed to save member: " + error); + } + }; + + return ( +
+
+
+

+ {" "} + {mode === "add" ? "Add new" : "Edit"} Member +

+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +