From e2cbea76f9abafead7d2a2ac2afe68909bc0ab26 Mon Sep 17 00:00:00 2001 From: sylee212 Date: Fri, 9 Jan 2026 15:29:26 +0800 Subject: [PATCH 01/13] removed the need for /items/ --- server/inventory/urls.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/inventory/urls.py b/server/inventory/urls.py index 8619aa9..17a6d42 100644 --- a/server/inventory/urls.py +++ b/server/inventory/urls.py @@ -4,7 +4,7 @@ router = DefaultRouter() # router.register(r'items', InventoryItemViewSet) # This creates /items/ -router.register(r'items', InventoryItemViewSet, basename='inventoryitem') # to fix error with missing basename +router.register('', InventoryItemViewSet, basename='inventoryitem') # to fix error with missing basename urlpatterns = [ path('', include(router.urls)), From 74eba4a296c8a4c9a1df2be30c6a591a5ee0409e Mon Sep 17 00:00:00 2001 From: sylee212 Date: Fri, 9 Jan 2026 15:32:57 +0800 Subject: [PATCH 02/13] successfully made the frontend called the backend in the dashboard --- .../ui/backend/organization_call_backend.ts | 23 ++++++----- .../organization_clean_backend_calls.ts | 40 +++++++++++++------ client/src/pages/organization_dashboard.tsx | 2 +- 3 files changed, 41 insertions(+), 24 deletions(-) diff --git a/client/src/components/ui/backend/organization_call_backend.ts b/client/src/components/ui/backend/organization_call_backend.ts index 695ee3b..2c7bd21 100644 --- a/client/src/components/ui/backend/organization_call_backend.ts +++ b/client/src/components/ui/backend/organization_call_backend.ts @@ -7,14 +7,14 @@ import { generateRandomMockInventoryDetails } from "@/mocks/Inventory_Details_In import { generateMockMember } from "@/mocks/Members_Details_Interface_Mocks"; // tha main URL -export const BASE_URL = "http://localhost:8000/api/activities/"; +export const BASE_INVENTORY_URL = "http://localhost:8000/api/inventory/"; // change when backend is ready -const isDev: boolean = true; +const isDev: boolean = false; // --- GET: Fetch all items --- export const getItems = async ( - filterType: string, + URL: string, ): Promise => { if (isDev) { // Mock data for development @@ -27,7 +27,8 @@ export const getItems = async ( } else { // 1. Fetch from Django // fetch() is used to get the data from backend using URL - const response = await fetch(`${BASE_URL}?type=${filterType}`); + // `${}` is place holders for code, anything inside the curly braces is code + const response = await fetch(`${URL}`); // 2. Check if the request was successful if (!response.ok) throw new Error("Failed to fetch"); @@ -53,7 +54,7 @@ export const createItem = async ( console.log("Mock create item called with data:", newData); return true; // Simulate successful creation } else { - const response = await fetch(BASE_URL, { + const response = await fetch(BASE_INVENTORY_URL, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(newData), @@ -70,7 +71,7 @@ export const updateItem = async ( newData: Inventory_Details_Interface, ) => { // Django usually expects a trailing slash after the ID - const response = await fetch(`${BASE_URL}${id}/`, { + const response = await fetch(`${BASE_INVENTORY_URL}${id}/`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify(newData), @@ -80,7 +81,7 @@ export const updateItem = async ( // --- DELETE: Remove an item --- export const deleteItem = async (id: number) => { - const response = await fetch(`${BASE_URL}${id}/`, { + const response = await fetch(`${BASE_INVENTORY_URL}${id}/`, { method: "DELETE", }); // DELETE usually returns a 204 No Content status, so we don't always .json() it @@ -103,7 +104,7 @@ export const getMembers = async ( } else { // 1. Fetch from Django // fetch() is used to get the data from backend using URL - const response = await fetch(`${BASE_URL}?type=${filterType}`); + const response = await fetch(`${BASE_INVENTORY_URL}?type=${filterType}`); // 2. Check if the request was successful if (!response.ok) throw new Error("Failed to fetch"); @@ -124,7 +125,7 @@ export const createMember = async ( console.log("Mock create Member called with data:", newData); return true; // Simulate successful creation } else { - const response = await fetch(BASE_URL, { + const response = await fetch(BASE_INVENTORY_URL, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(newData), @@ -141,7 +142,7 @@ export const updateMember = async ( newData: Member_Details_Interface, ) => { // Django usually expects a trailing slash after the ID - const response = await fetch(`${BASE_URL}${id}/`, { + const response = await fetch(`${BASE_INVENTORY_URL}${id}/`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify(newData), @@ -151,7 +152,7 @@ export const updateMember = async ( // --- DELETE: Remove an Member --- export const deleteMember = async (id: number) => { - const response = await fetch(`${BASE_URL}${id}/`, { + const response = await fetch(`${BASE_INVENTORY_URL}${id}/`, { method: "DELETE", }); // DELETE usually returns a 204 No Content status, so we don't always .json() it diff --git a/client/src/components/ui/backend/organization_clean_backend_calls.ts b/client/src/components/ui/backend/organization_clean_backend_calls.ts index efa1225..de9b551 100644 --- a/client/src/components/ui/backend/organization_clean_backend_calls.ts +++ b/client/src/components/ui/backend/organization_clean_backend_calls.ts @@ -1,11 +1,11 @@ // src/hooks/organization_clean_backend_calls.ts -import SWR, { KeyedMutator } from "swr"; +import useSWR, { 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, + BASE_INVENTORY_URL, createItem, createMember, deleteItem, @@ -54,9 +54,6 @@ Remember, this will only be invoked once, when its mounted, if you want to call // 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 @@ -64,7 +61,8 @@ filterType: d to add to backend url call isDev: true if we want to mock data */ export const useOrganizationBackendGetItems = ( - filterType: string, + category: string, + sortBy: string, ): organization_clean_backend_calls_return_interface => { // 2. SWR handles the state, the effect, and the async logic // SWR(key, fetcher, options) @@ -116,9 +114,27 @@ export const useOrganizationBackendGetItems = ( // 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) + + // generate the URL + // 1. GENERATE THE URL STRING + // Instead of complex if/else, we use URLSearchParams + const params = new URLSearchParams(); + if (category && category !== "") params.append("categories", category); // Django expects 'categories' + if (sortBy && sortBy !== "") params.append("ordering", sortBy); // Django expects 'ordering' + + // If params exist, add '?' and the params, otherwise just the base URL + const queryString = params.toString(); + + // 2. Fix the URL construction + // We need backticks ` ` to use ${} + // We need to add 'items/' because the router is registered there + const finalUrl = queryString + ? `${BASE_INVENTORY_URL}?${queryString}` + : `${BASE_INVENTORY_URL}`; + + const { data, error, isLoading, mutate } = useSWR( + finalUrl, // The "Key" (Unique identifier) + getItems, // The "Fetcher" (Your function) { refreshInterval: 30, // Poll every 30 seconds 30000ms revalidateOnFocus: true, // Refresh when r clicks back into the tab @@ -184,8 +200,8 @@ 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) + const { data, error, isLoading, mutate } = useSWR( + [`${BASE_INVENTORY_URL}`, filterType], // The "Key" (Unique identifier) fetcher2, // The "Fetcher" (Your function) { refreshInterval: 30, // Poll every 30 seconds 30000ms @@ -246,7 +262,7 @@ export const deleteMemberClean = async (memberId: number): Promise => { // // to SWR for PUT // const { data, error, isLoading, mutate } = SWR( -// [`${BASE_URL}`, "newItem"], +// [`${BASE_INVENTORY_URL}`, "newItem"], // () => createItem(itemData), // { // revalidateOnFocus: true, diff --git a/client/src/pages/organization_dashboard.tsx b/client/src/pages/organization_dashboard.tsx index 02836db..4c31b75 100644 --- a/client/src/pages/organization_dashboard.tsx +++ b/client/src/pages/organization_dashboard.tsx @@ -25,7 +25,7 @@ const Organization_Dashboard = () => { error, refresh, }: organization_clean_backend_calls_return_interface = - useOrganizationBackendGetItems("all"); + useOrganizationBackendGetItems("", ""); console.log("Data from useOrganizationBackendGetItems:", data); console.log("Loading state:", loading); console.log("Error state:", error); From a45f001b9f5bed38efad6f2a011231ebe0d30a0d Mon Sep 17 00:00:00 2001 From: sylee212 Date: Fri, 9 Jan 2026 16:28:44 +0800 Subject: [PATCH 03/13] managed to get backend calls to count to work --- .../ui/backend/organization_call_backend.ts | 21 ++++++++++++ .../organization_clean_backend_calls.ts | 33 +++++++++++++++++++ client/src/pages/organization_dashboard.tsx | 24 +++++++++++++- server/inventory/views.py | 21 +++++++++++- 4 files changed, 97 insertions(+), 2 deletions(-) diff --git a/client/src/components/ui/backend/organization_call_backend.ts b/client/src/components/ui/backend/organization_call_backend.ts index 2c7bd21..a23e6f7 100644 --- a/client/src/components/ui/backend/organization_call_backend.ts +++ b/client/src/components/ui/backend/organization_call_backend.ts @@ -8,6 +8,12 @@ import { generateMockMember } from "@/mocks/Members_Details_Interface_Mocks"; // tha main URL export const BASE_INVENTORY_URL = "http://localhost:8000/api/inventory/"; +export const COUNT_ITEMS_DUE_THIS_WEEK_URL = + "http://localhost:8000/api/inventory/countDueThisWeek/"; + +export interface django_count_response_interface { + count: number; +} // change when backend is ready const isDev: boolean = false; @@ -42,6 +48,21 @@ export const getItems = async ( } }; +export const getITemsDueThisWeek = + async (): Promise => { + const response = await fetch(`${COUNT_ITEMS_DUE_THIS_WEEK_URL}`); + + // 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 diff --git a/client/src/components/ui/backend/organization_clean_backend_calls.ts b/client/src/components/ui/backend/organization_clean_backend_calls.ts index de9b551..19e40ed 100644 --- a/client/src/components/ui/backend/organization_clean_backend_calls.ts +++ b/client/src/components/ui/backend/organization_clean_backend_calls.ts @@ -6,10 +6,13 @@ import { Member_Details_Interface } from "@/components/ui/card_organization_memb import type { Inventory_Details_Interface } from "../card_organization_inventory_details_modal"; import { BASE_INVENTORY_URL, + COUNT_ITEMS_DUE_THIS_WEEK_URL, createItem, createMember, deleteItem, + django_count_response_interface, getItems, + getITemsDueThisWeek, getMembers, updateItem, updateMember, @@ -45,6 +48,14 @@ export interface organization_clean_backend_calls_return_boolean_members_interfa refresh: KeyedMutator; // Optional refresh function } +export interface organization_clean_backend_calls_return_number_interface { + data: number; + 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 @@ -152,6 +163,28 @@ export const useOrganizationBackendGetItems = ( return res; }; +export const useOrganizationBackendCountItemsDueThisWeek = + (): organization_clean_backend_calls_return_number_interface => { + const { data, error, isLoading, mutate } = useSWR( + COUNT_ITEMS_DUE_THIS_WEEK_URL, // The "Key" (Unique identifier) + getITemsDueThisWeek, // 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_number_interface = { + data: data?.count || 0, + 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 ( diff --git a/client/src/pages/organization_dashboard.tsx b/client/src/pages/organization_dashboard.tsx index 4c31b75..50797cd 100644 --- a/client/src/pages/organization_dashboard.tsx +++ b/client/src/pages/organization_dashboard.tsx @@ -6,6 +6,8 @@ import { useState } from "react"; import { organization_clean_backend_calls_return_interface, + organization_clean_backend_calls_return_number_interface, + useOrganizationBackendCountItemsDueThisWeek, useOrganizationBackendGetItems, } from "@/components/ui/backend/organization_clean_backend_calls"; import Inventory_Details_Modal, { @@ -18,6 +20,8 @@ import Statistics_Card from "../components/ui/card_organization_statistics"; import Header from "../components/ui/navbar_organization"; const Organization_Dashboard = () => { + // calling backend starts // + // this is for calling the data for the modal const { data, @@ -31,6 +35,24 @@ const Organization_Dashboard = () => { console.log("Error state:", error); console.log("Refresh function:", refresh); + // this is for calling the data for the modal + const { + data: countItemsDueThisWeekData, + loading: countItemsDueThisWeekLoading, + error: countItemsDueThisWeekError, + refresh: countItemsDueThisWeekRefresh, + }: organization_clean_backend_calls_return_number_interface = useOrganizationBackendCountItemsDueThisWeek(); + + console.log( + "Data from useOrganizationBackendCountItemsDueThisWeek:", + countItemsDueThisWeekData, + ); + console.log("Loading state:", countItemsDueThisWeekLoading); + console.log("Error state:", countItemsDueThisWeekError); + console.log("Refresh function:", countItemsDueThisWeekRefresh); + + // calling backend ends // + // This is for the overlay modal const [isModalOpen, setIsModalOpen] = useState(false); const [selectedItemData, setSelectedItemData] = @@ -93,7 +115,7 @@ const Organization_Dashboard = () => { /> diff --git a/server/inventory/views.py b/server/inventory/views.py index c2d82eb..ab84f18 100644 --- a/server/inventory/views.py +++ b/server/inventory/views.py @@ -6,6 +6,10 @@ from rest_framework import viewsets from .models import InventoryItem from .serializers import InventoryItemSerializer +from rest_framework.decorators import action +from rest_framework.response import Response +from django.utils import timezone +from datetime import timedelta class InventoryItemViewSet(viewsets.ModelViewSet): serializer_class = InventoryItemSerializer @@ -35,4 +39,19 @@ def get_queryset(self): # class InventoryItemViewSet(viewsets.ModelViewSet): # # This tells the view where to get the data and how to translate it # queryset = InventoryItem.objects.all() -# serializer_class = InventoryItemSerializer \ No newline at end of file +# serializer_class = InventoryItemSerializer + + # url = GET /api/inventory/countDueThisWeek/ + @action(detail=False, methods=['get']) + def countDueThisWeek(self, request): + # 1. Define the time range + now = timezone.now() + one_week_later = now + timedelta(days=7) + + # 2. Filter and count in the database (efficient!) + count = InventoryItem.objects.filter( + dueOn__range=[now, one_week_later] + ).count() + + # 3. Return a simple response + return Response({'count': count}) \ No newline at end of file From 8ee93c43cc1abaf07ace686e7317a44563f85243 Mon Sep 17 00:00:00 2001 From: sylee212 Date: Fri, 9 Jan 2026 20:05:08 +0800 Subject: [PATCH 04/13] finished dueOn Card for dashboard, will now copy and paste code --- .../ui/backend/organization_call_backend.ts | 19 ++++++++++++- .../organization_clean_backend_calls.ts | 27 +++++++++++++++++-- client/src/pages/organization_dashboard.tsx | 19 +++++++++++-- server/inventory/views.py | 15 +++++++++++ 4 files changed, 75 insertions(+), 5 deletions(-) diff --git a/client/src/components/ui/backend/organization_call_backend.ts b/client/src/components/ui/backend/organization_call_backend.ts index a23e6f7..327be9d 100644 --- a/client/src/components/ui/backend/organization_call_backend.ts +++ b/client/src/components/ui/backend/organization_call_backend.ts @@ -10,6 +10,8 @@ import { generateMockMember } from "@/mocks/Members_Details_Interface_Mocks"; export const BASE_INVENTORY_URL = "http://localhost:8000/api/inventory/"; export const COUNT_ITEMS_DUE_THIS_WEEK_URL = "http://localhost:8000/api/inventory/countDueThisWeek/"; +export const COUNT_ITEMS_DUE_LAST_WEEK_URL = + "http://localhost:8000/api/inventory/countDueLastWeek/"; export interface django_count_response_interface { count: number; @@ -48,7 +50,7 @@ export const getItems = async ( } }; -export const getITemsDueThisWeek = +export const getItemsDueThisWeek = async (): Promise => { const response = await fetch(`${COUNT_ITEMS_DUE_THIS_WEEK_URL}`); @@ -63,6 +65,21 @@ export const getITemsDueThisWeek = return await response.json(); // Returns the list from Django }; +export const getItemsDueLastWeek = + async (): Promise => { + const response = await fetch(`${COUNT_ITEMS_DUE_LAST_WEEK_URL}`); + + // 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 diff --git a/client/src/components/ui/backend/organization_clean_backend_calls.ts b/client/src/components/ui/backend/organization_clean_backend_calls.ts index 19e40ed..e2fc8a5 100644 --- a/client/src/components/ui/backend/organization_clean_backend_calls.ts +++ b/client/src/components/ui/backend/organization_clean_backend_calls.ts @@ -6,13 +6,14 @@ import { Member_Details_Interface } from "@/components/ui/card_organization_memb import type { Inventory_Details_Interface } from "../card_organization_inventory_details_modal"; import { BASE_INVENTORY_URL, + COUNT_ITEMS_DUE_LAST_WEEK_URL, COUNT_ITEMS_DUE_THIS_WEEK_URL, createItem, createMember, deleteItem, django_count_response_interface, getItems, - getITemsDueThisWeek, + getItemsDueThisWeek, getMembers, updateItem, updateMember, @@ -167,7 +168,29 @@ export const useOrganizationBackendCountItemsDueThisWeek = (): organization_clean_backend_calls_return_number_interface => { const { data, error, isLoading, mutate } = useSWR( COUNT_ITEMS_DUE_THIS_WEEK_URL, // The "Key" (Unique identifier) - getITemsDueThisWeek, // The "Fetcher" (Your function) + getItemsDueThisWeek, // 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_number_interface = { + data: data?.count || 0, + loading: isLoading, + error: error, + isSuccess: !isLoading && !error, + refresh: mutate, // SWR calls its refresh function "mutate" + }; + + return res; + }; + +export const useOrganizationBackendCountItemsDueLastWeek = + (): organization_clean_backend_calls_return_number_interface => { + const { data, error, isLoading, mutate } = useSWR( + COUNT_ITEMS_DUE_LAST_WEEK_URL, // The "Key" (Unique identifier) + getItemsDueThisWeek, // The "Fetcher" (Your function) { refreshInterval: 30, // Poll every 30 seconds 30000ms revalidateOnFocus: true, // Refresh when r clicks back into the tab diff --git a/client/src/pages/organization_dashboard.tsx b/client/src/pages/organization_dashboard.tsx index 50797cd..705ac25 100644 --- a/client/src/pages/organization_dashboard.tsx +++ b/client/src/pages/organization_dashboard.tsx @@ -21,7 +21,6 @@ import Header from "../components/ui/navbar_organization"; const Organization_Dashboard = () => { // calling backend starts // - // this is for calling the data for the modal const { data, @@ -51,6 +50,22 @@ const Organization_Dashboard = () => { console.log("Error state:", countItemsDueThisWeekError); console.log("Refresh function:", countItemsDueThisWeekRefresh); + const { + data: countItemsDueLastWeekData, + loading: countItemsDueLastWeekLoading, + error: countItemsDueLastWeekError, + refresh: countItemsDueLastWeekRefresh, + }: organization_clean_backend_calls_return_number_interface = useOrganizationBackendCountItemsDueThisWeek(); + + console.log( + "Data from useOrganizationBackendCountItemsDueThisWeek:", + countItemsDueLastWeekData, + ); + console.log("Loading state:", countItemsDueLastWeekLoading); + console.log("Error state:", countItemsDueLastWeekError); + console.log("Refresh function:", countItemsDueLastWeekRefresh); + + const dueDifference = countItemsDueThisWeekData - countItemsDueLastWeekData; // calling backend ends // // This is for the overlay modal @@ -116,7 +131,7 @@ const Organization_Dashboard = () => { diff --git a/server/inventory/views.py b/server/inventory/views.py index ab84f18..adc5eaa 100644 --- a/server/inventory/views.py +++ b/server/inventory/views.py @@ -53,5 +53,20 @@ def countDueThisWeek(self, request): dueOn__range=[now, one_week_later] ).count() + # 3. Return a simple response + return Response({'count': count}) + + # url = GET /api/inventory/countDueThisWeek/ + @action(detail=False, methods=['get']) + def countDueLastWeek(self, request): + # 1. Define the time range + now = timezone.now() + one_week_ago = now - timedelta(days=7) + + # 2. Filter and count in the database (efficient!) + count = InventoryItem.objects.filter( + dueOn__range=[one_week_ago, now] + ).count() + # 3. Return a simple response return Response({'count': count}) \ No newline at end of file From 2f27b46c914d591f9c38329d6b4642ff5ca45de7 Mon Sep 17 00:00:00 2001 From: sylee212 Date: Fri, 9 Jan 2026 21:03:48 +0800 Subject: [PATCH 05/13] finished dashboard --- .../ui/backend/organization_call_backend.ts | 55 +++--- .../organization_clean_backend_calls.ts | 75 +++----- client/src/pages/organization_dashboard.tsx | 172 ++++++++++++++---- server/inventory/views.py | 83 ++++++--- 4 files changed, 245 insertions(+), 140 deletions(-) diff --git a/client/src/components/ui/backend/organization_call_backend.ts b/client/src/components/ui/backend/organization_call_backend.ts index 327be9d..8eaf134 100644 --- a/client/src/components/ui/backend/organization_call_backend.ts +++ b/client/src/components/ui/backend/organization_call_backend.ts @@ -8,13 +8,14 @@ import { generateMockMember } from "@/mocks/Members_Details_Interface_Mocks"; // tha main URL export const BASE_INVENTORY_URL = "http://localhost:8000/api/inventory/"; -export const COUNT_ITEMS_DUE_THIS_WEEK_URL = - "http://localhost:8000/api/inventory/countDueThisWeek/"; -export const COUNT_ITEMS_DUE_LAST_WEEK_URL = - "http://localhost:8000/api/inventory/countDueLastWeek/"; +export const WEEKLY_REPORT_BY_FIELD = + "http://localhost:8000/api/inventory/weeklyReportByField/"; export interface django_count_response_interface { - count: number; + field: number; + thisWeek: number; + lastWeek: number; + difference: number; } // change when backend is ready @@ -50,35 +51,21 @@ export const getItems = async ( } }; -export const getItemsDueThisWeek = - async (): Promise => { - const response = await fetch(`${COUNT_ITEMS_DUE_THIS_WEEK_URL}`); - - // 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 getItemsDueLastWeek = - async (): Promise => { - const response = await fetch(`${COUNT_ITEMS_DUE_LAST_WEEK_URL}`); - - // 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 getWeeklyReportByField = async ( + URL: string, +): Promise => { + const response = await fetch(`${URL}`); + + // 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 diff --git a/client/src/components/ui/backend/organization_clean_backend_calls.ts b/client/src/components/ui/backend/organization_clean_backend_calls.ts index e2fc8a5..ab6a852 100644 --- a/client/src/components/ui/backend/organization_clean_backend_calls.ts +++ b/client/src/components/ui/backend/organization_clean_backend_calls.ts @@ -6,15 +6,13 @@ import { Member_Details_Interface } from "@/components/ui/card_organization_memb import type { Inventory_Details_Interface } from "../card_organization_inventory_details_modal"; import { BASE_INVENTORY_URL, - COUNT_ITEMS_DUE_LAST_WEEK_URL, - COUNT_ITEMS_DUE_THIS_WEEK_URL, createItem, createMember, deleteItem, django_count_response_interface, getItems, - getItemsDueThisWeek, getMembers, + getWeeklyReportByField, updateItem, updateMember, } from "./organization_call_backend"; @@ -49,8 +47,11 @@ export interface organization_clean_backend_calls_return_boolean_members_interfa refresh: KeyedMutator; // Optional refresh function } -export interface organization_clean_backend_calls_return_number_interface { - data: number; +export interface organization_clean_backend_calls_return_count_interface { + // data? means "if data exists". ?? 0 means "otherwise use 0" + thisWeek: number; + lastWeek: number; + difference: number; loading: boolean; error: Error | null; isSuccess: boolean; @@ -164,50 +165,32 @@ export const useOrganizationBackendGetItems = ( return res; }; -export const useOrganizationBackendCountItemsDueThisWeek = - (): organization_clean_backend_calls_return_number_interface => { - const { data, error, isLoading, mutate } = useSWR( - COUNT_ITEMS_DUE_THIS_WEEK_URL, // The "Key" (Unique identifier) - getItemsDueThisWeek, // 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_number_interface = { - data: data?.count || 0, - loading: isLoading, - error: error, - isSuccess: !isLoading && !error, - refresh: mutate, // SWR calls its refresh function "mutate" - }; - - return res; - }; +export const useOrganizationBackendGetWeeklyReportByField = ( + URL: string, +): organization_clean_backend_calls_return_count_interface => { + const { data, error, isLoading, mutate } = useSWR( + URL, // The "Key" (Unique identifier) + getWeeklyReportByField, // 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_count_interface = { + thisWeek: data?.thisWeek || 0, + lastWeek: data?.lastWeek || 0, + difference: data?.difference || 0, -export const useOrganizationBackendCountItemsDueLastWeek = - (): organization_clean_backend_calls_return_number_interface => { - const { data, error, isLoading, mutate } = useSWR( - COUNT_ITEMS_DUE_LAST_WEEK_URL, // The "Key" (Unique identifier) - getItemsDueThisWeek, // 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_number_interface = { - data: data?.count || 0, - loading: isLoading, - error: error, - isSuccess: !isLoading && !error, - refresh: mutate, // SWR calls its refresh function "mutate" - }; - - return res; + 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 ( diff --git a/client/src/pages/organization_dashboard.tsx b/client/src/pages/organization_dashboard.tsx index 705ac25..cf3b11f 100644 --- a/client/src/pages/organization_dashboard.tsx +++ b/client/src/pages/organization_dashboard.tsx @@ -4,11 +4,12 @@ // so the path is '../components/Header' import { useState } from "react"; +import { WEEKLY_REPORT_BY_FIELD } from "@/components/ui/backend/organization_call_backend"; import { + organization_clean_backend_calls_return_count_interface, organization_clean_backend_calls_return_interface, - organization_clean_backend_calls_return_number_interface, - useOrganizationBackendCountItemsDueThisWeek, useOrganizationBackendGetItems, + useOrganizationBackendGetWeeklyReportByField, } from "@/components/ui/backend/organization_clean_backend_calls"; import Inventory_Details_Modal, { Inventory_Details_Interface, @@ -34,38 +35,141 @@ const Organization_Dashboard = () => { console.log("Error state:", error); console.log("Refresh function:", refresh); - // this is for calling the data for the modal const { - data: countItemsDueThisWeekData, - loading: countItemsDueThisWeekLoading, - error: countItemsDueThisWeekError, - refresh: countItemsDueThisWeekRefresh, - }: organization_clean_backend_calls_return_number_interface = useOrganizationBackendCountItemsDueThisWeek(); + thisWeek: countItemsDueThisWeek, + lastWeek: countItemsDueLastWeek, + difference: countItemsDueDifference, + loading: countItemsDueLoading, + error: countItemsDueError, + refresh: countItemsDueRefresh, + }: organization_clean_backend_calls_return_count_interface = useOrganizationBackendGetWeeklyReportByField( + `${WEEKLY_REPORT_BY_FIELD}?field=dueOn`, + ); console.log( - "Data from useOrganizationBackendCountItemsDueThisWeek:", - countItemsDueThisWeekData, + "Data from useOrganizationBackendGetItems:", + countItemsDueThisWeek, + ); + console.log( + "Data from useOrganizationBackendGetItems:", + countItemsDueLastWeek, + ); + console.log( + "Data from useOrganizationBackendGetItems:", + countItemsDueDifference, ); - console.log("Loading state:", countItemsDueThisWeekLoading); - console.log("Error state:", countItemsDueThisWeekError); - console.log("Refresh function:", countItemsDueThisWeekRefresh); + console.log("Loading state:", countItemsDueLoading); + console.log("Error state:", countItemsDueError); + console.log("Refresh function:", countItemsDueRefresh); const { - data: countItemsDueLastWeekData, - loading: countItemsDueLastWeekLoading, - error: countItemsDueLastWeekError, - refresh: countItemsDueLastWeekRefresh, - }: organization_clean_backend_calls_return_number_interface = useOrganizationBackendCountItemsDueThisWeek(); + thisWeek: countItemsExpiringThisWeek, + lastWeek: countItemsExpiringLastWeek, + difference: countItemsExpiringDifference, + loading: countItemsExpiringLoading, + error: countItemsExpiringError, + refresh: countItemsExpiringRefresh, + }: organization_clean_backend_calls_return_count_interface = useOrganizationBackendGetWeeklyReportByField( + `${WEEKLY_REPORT_BY_FIELD}?field=expiryDate`, + ); console.log( - "Data from useOrganizationBackendCountItemsDueThisWeek:", - countItemsDueLastWeekData, + "Data from useOrganizationBackendGetItems:", + countItemsExpiringThisWeek, + ); + console.log( + "Data from useOrganizationBackendGetItems:", + countItemsExpiringLastWeek, + ); + console.log( + "Data from useOrganizationBackendGetItems:", + countItemsExpiringDifference, + ); + console.log("Loading state:", countItemsExpiringLoading); + console.log("Error state:", countItemsExpiringError); + console.log("Refresh function:", countItemsExpiringRefresh); + + const { + thisWeek: countItemsBorrowedThisWeek, + lastWeek: countItemsBorrowedLastWeek, + difference: countItemsBorrowedDifference, + loading: countItemsBorrowedLoading, + error: countItemsBorrowedError, + refresh: countItemsBorrowedRefresh, + }: organization_clean_backend_calls_return_count_interface = useOrganizationBackendGetWeeklyReportByField( + `${WEEKLY_REPORT_BY_FIELD}?field=borrowedOn`, + ); + + console.log( + "Data from useOrganizationBackendGetItems:", + countItemsBorrowedThisWeek, + ); + console.log( + "Data from useOrganizationBackendGetItems:", + countItemsBorrowedLastWeek, + ); + console.log( + "Data from useOrganizationBackendGetItems:", + countItemsBorrowedDifference, + ); + console.log("Loading state:", countItemsBorrowedLoading); + console.log("Error state:", countItemsBorrowedError); + console.log("Refresh function:", countItemsBorrowedRefresh); + + const { + thisWeek: countItemsReturnedThisWeek, + lastWeek: countItemsReturnedLastWeek, + difference: countItemsReturnedDifference, + loading: countItemsReturnedLoading, + error: countItemsReturnedError, + refresh: countItemsReturnedRefresh, + }: organization_clean_backend_calls_return_count_interface = useOrganizationBackendGetWeeklyReportByField( + `${WEEKLY_REPORT_BY_FIELD}?field=returnedOn`, + ); + + console.log( + "Data from useOrganizationBackendGetItems:", + countItemsReturnedThisWeek, + ); + console.log( + "Data from useOrganizationBackendGetItems:", + countItemsReturnedLastWeek, + ); + console.log( + "Data from useOrganizationBackendGetItems:", + countItemsReturnedDifference, + ); + console.log("Loading state:", countItemsReturnedLoading); + console.log("Error state:", countItemsReturnedError); + console.log("Refresh function:", countItemsReturnedRefresh); + + const { + thisWeek: countItemsDateAddedThisWeek, + lastWeek: countItemsDateAddedLastWeek, + difference: countItemsDateAddedDifference, + loading: countItemsDateAddedLoading, + error: countItemsDateAddedError, + refresh: countItemsDateAddedRefresh, + }: organization_clean_backend_calls_return_count_interface = useOrganizationBackendGetWeeklyReportByField( + `${WEEKLY_REPORT_BY_FIELD}?field=dateAdded`, + ); + + console.log( + "Data from useOrganizationBackendGetItems:", + countItemsDateAddedThisWeek, + ); + console.log( + "Data from useOrganizationBackendGetItems:", + countItemsDateAddedLastWeek, + ); + console.log( + "Data from useOrganizationBackendGetItems:", + countItemsDateAddedDifference, ); - console.log("Loading state:", countItemsDueLastWeekLoading); - console.log("Error state:", countItemsDueLastWeekError); - console.log("Refresh function:", countItemsDueLastWeekRefresh); + console.log("Loading state:", countItemsDateAddedLoading); + console.log("Error state:", countItemsDateAddedError); + console.log("Refresh function:", countItemsDateAddedRefresh); - const dueDifference = countItemsDueThisWeekData - countItemsDueLastWeekData; // calling backend ends // // This is for the overlay modal @@ -106,32 +210,32 @@ const Organization_Dashboard = () => { {/* This class will control the layout of the 5 cards */} diff --git a/server/inventory/views.py b/server/inventory/views.py index adc5eaa..1a73577 100644 --- a/server/inventory/views.py +++ b/server/inventory/views.py @@ -41,32 +41,63 @@ def get_queryset(self): # queryset = InventoryItem.objects.all() # serializer_class = InventoryItemSerializer - # url = GET /api/inventory/countDueThisWeek/ + ### calls for dashboard starts ### + + # url = GET /api/inventory/weeklyReportByField/ @action(detail=False, methods=['get']) - def countDueThisWeek(self, request): - # 1. Define the time range - now = timezone.now() - one_week_later = now + timedelta(days=7) - - # 2. Filter and count in the database (efficient!) - count = InventoryItem.objects.filter( - dueOn__range=[now, one_week_later] - ).count() + def weeklyReportByField(self, request): + # 1. Get the field name from the URL (e.g., ?field=dueOn) + field_name = request.query_params.get('field', 'dueOn') - # 3. Return a simple response - return Response({'count': count}) - - # url = GET /api/inventory/countDueThisWeek/ - @action(detail=False, methods=['get']) - def countDueLastWeek(self, request): - # 1. Define the time range + # 2. Calculate Calendar Week Boundaries now = timezone.now() - one_week_ago = now - timedelta(days=7) - - # 2. Filter and count in the database (efficient!) - count = InventoryItem.objects.filter( - dueOn__range=[one_week_ago, now] - ).count() - - # 3. Return a simple response - return Response({'count': count}) \ No newline at end of file + days_since_monday = now.weekday() + this_monday = (now - timedelta(days=days_since_monday)).replace(hour=0, minute=0, second=0, microsecond=0) + last_monday = this_monday - timedelta(days=7) + last_sunday = this_monday - timedelta(microseconds=1) + + # 3. Build the dynamic filter keys + # Example: if field_name is 'borrowedOn', this becomes 'borrowedOn__range' + this_week_filter = {f"{field_name}__range": [this_monday, now]} + last_week_filter = {f"{field_name}__range": [last_monday, last_sunday]} + + # 4. Execute counts + this_week_count = InventoryItem.objects.filter(**this_week_filter).count() + last_week_count = InventoryItem.objects.filter(**last_week_filter).count() + + return Response({ + 'field': field_name, + 'thisWeek': this_week_count, + 'lastWeek': last_week_count, + 'difference': this_week_count - last_week_count + }) + + +# old mat method +# # 1. Define the time range +# now = timezone.now() +# one_week_ago = now - timedelta(days=7) + +# # 2. Filter and count in the database (efficient!) +# count = InventoryItem.objects.filter( +# dueOn__range=[one_week_ago, now] +# ).count() + +# @action(detail=False, methods=['get']) +# def dueSummary(self, request): +# now = timezone.now() +# days_since_monday = now.weekday() +# this_monday = (now - timedelta(days=days_since_monday)).replace(hour=0, minute=0, second=0, microsecond=0) + +# last_monday = this_monday - timedelta(days=7) +# last_sunday = this_monday - timedelta(microseconds=1) + +# # Database does both counts +# this_week_count = InventoryItem.objects.filter(dueOn__range=[this_monday, now]).count() +# last_week_count = InventoryItem.objects.filter(dueOn__range=[last_monday, last_sunday]).count() + +# return Response({ +# 'thisWeek': this_week_count, +# 'lastWeek': last_week_count, +# 'difference': this_week_count - last_week_count +# }) \ No newline at end of file From 14860ffba09b8e207b4a8d673734f6cd379dc5e2 Mon Sep 17 00:00:00 2001 From: sylee212 Date: Fri, 9 Jan 2026 21:40:04 +0800 Subject: [PATCH 06/13] finished making the createItem --- .../ui/backend/organization_call_backend.ts | 8 ++- ...d_organization_inventory_details_modal.tsx | 2 +- client/src/pages/organization_add_product.tsx | 72 ++++++++++++------- 3 files changed, 55 insertions(+), 27 deletions(-) diff --git a/client/src/components/ui/backend/organization_call_backend.ts b/client/src/components/ui/backend/organization_call_backend.ts index 8eaf134..1c2576b 100644 --- a/client/src/components/ui/backend/organization_call_backend.ts +++ b/client/src/components/ui/backend/organization_call_backend.ts @@ -85,8 +85,12 @@ export const createItem = async ( body: JSON.stringify(newData), }); - // must return true when coding backend - return await response.json(); + if (response.ok) return true; + + // If it's not OK, let's see why + const errorText = await response.text(); + console.error("Server Error Response:", errorText); + return false; } }; diff --git a/client/src/components/ui/card_organization_inventory_details_modal.tsx b/client/src/components/ui/card_organization_inventory_details_modal.tsx index ea39cec..e7bb0e8 100644 --- a/client/src/components/ui/card_organization_inventory_details_modal.tsx +++ b/client/src/components/ui/card_organization_inventory_details_modal.tsx @@ -27,7 +27,7 @@ interface Inventory_Details_Interface { name: string; details: string; categories?: string; - availability?: string; + availability?: boolean; organization?: string; collectionPoint: string; borrowerName?: string; diff --git a/client/src/pages/organization_add_product.tsx b/client/src/pages/organization_add_product.tsx index b64884a..32d5041 100644 --- a/client/src/pages/organization_add_product.tsx +++ b/client/src/pages/organization_add_product.tsx @@ -3,7 +3,7 @@ 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 { BASE_INVENTORY_URL } from "@/components/ui/backend/organization_call_backend"; import { createItemClean, updateItemClean, @@ -39,7 +39,7 @@ const Organization_Add_Product = () => { name: mode === "add" ? "" : parsedData?.name, details: mode === "add" ? "" : parsedData?.details, categories: mode === "add" ? "" : parsedData?.categories, - availability: mode === "add" ? "" : parsedData?.availability, + availability: mode === "add" ? true : parsedData?.availability, // organization is kept away because we will fix it to the name of the organization that is logged in // organization: "", @@ -76,35 +76,59 @@ const Organization_Add_Product = () => { 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 createItemClean(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"]); + const dataToSubmit = { ...formData }; - alert("Product Saved!"); - } else { - // 2. Call the action (The "Messenger") - await updateItemClean(formData); + // We use "keyof Inventory_Details_Interface" to tell TS exactly what strings are allowed + const dateTimeFields: (keyof Inventory_Details_Interface)[] = [ + "borrowedOn", + "returnedOn", + "dueOn", + ]; - // 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"]); + dateTimeFields.forEach((field) => { + const value = dataToSubmit[field]; - alert("Product updated!"); + if (!value || value === "") { + dataToSubmit[field] = undefined; // Use undefined or null + } else if (typeof value === "string" && !value.includes("T")) { + // Use a type assertion to tell TS this is a valid assignment + (dataToSubmit[field] as string) = `${value}T00:00:00Z`; } + }); + + // 2. Handle Date fields (MUST NOT have time) + // If it's empty, send null. If it has a value, leave it as YYYY-MM-DD + if (!dataToSubmit.expiryDate || dataToSubmit.expiryDate === "") { + dataToSubmit.expiryDate = undefined; + } - // go back to dashboard - // router.push('/organization_dashboard'); - router.back(); + try { + // Ensure we are sending the right field names to Django + // If your model uses dueOn, formData must have dueOn + const success = + mode === "add" + ? await createItemClean(dataToSubmit) + : await updateItemClean(dataToSubmit); + + if (success) { + // 1. Refresh the main list + // Use the exact string you used in your useSWR hook for the list + mutate(`${BASE_INVENTORY_URL}`); + + // 2. Refresh the statistics cards too! + mutate( + (key) => typeof key === "string" && key.includes("weekly_report"), + ); + + alert(mode === "add" ? "Product Saved!" : "Product Updated!"); + router.back(); + } else { + alert("Failed to save. Check console for details."); + } } catch (error) { - alert("Failed to save product: " + error); + alert("Network Error: " + error); } }; @@ -145,7 +169,7 @@ const Organization_Add_Product = () => { From 0478bd734be4860b0dd4c14856c9d3a9032861d8 Mon Sep 17 00:00:00 2001 From: sylee212 Date: Fri, 9 Jan 2026 21:45:52 +0800 Subject: [PATCH 07/13] finished update and create --- .../ui/backend/organization_call_backend.ts | 13 +++++++++---- client/src/pages/organization_add_product.tsx | 2 +- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/client/src/components/ui/backend/organization_call_backend.ts b/client/src/components/ui/backend/organization_call_backend.ts index 1c2576b..d68f46c 100644 --- a/client/src/components/ui/backend/organization_call_backend.ts +++ b/client/src/components/ui/backend/organization_call_backend.ts @@ -98,14 +98,19 @@ export const createItem = async ( export const updateItem = async ( id: number, newData: Inventory_Details_Interface, -) => { - // Django usually expects a trailing slash after the ID +): Promise => { const response = await fetch(`${BASE_INVENTORY_URL}${id}/`, { - method: "PATCH", + method: "PATCH", // PATCH is better than PUT for Partial updates headers: { "Content-Type": "application/json" }, body: JSON.stringify(newData), }); - return await response.json(); + + if (response.ok) return true; + + // If it's not OK, let's see why + const errorText = await response.text(); + console.error("Server Error Response:", errorText); + return false; }; // --- DELETE: Remove an item --- diff --git a/client/src/pages/organization_add_product.tsx b/client/src/pages/organization_add_product.tsx index 32d5041..17d1831 100644 --- a/client/src/pages/organization_add_product.tsx +++ b/client/src/pages/organization_add_product.tsx @@ -35,7 +35,7 @@ const Organization_Add_Product = () => { Partial >({ // id is excluded because it will be auto generated by backend - // id: number, + id: mode === "add" ? 0 : parsedData?.id, name: mode === "add" ? "" : parsedData?.name, details: mode === "add" ? "" : parsedData?.details, categories: mode === "add" ? "" : parsedData?.categories, From de0176e5158eb0abfea65a11d4eecdc0976d2515 Mon Sep 17 00:00:00 2001 From: sylee212 Date: Fri, 9 Jan 2026 22:04:21 +0800 Subject: [PATCH 08/13] adde delete --- .../ui/card_organization_inventory_details_modal.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/client/src/components/ui/card_organization_inventory_details_modal.tsx b/client/src/components/ui/card_organization_inventory_details_modal.tsx index e7bb0e8..2ba6e1f 100644 --- a/client/src/components/ui/card_organization_inventory_details_modal.tsx +++ b/client/src/components/ui/card_organization_inventory_details_modal.tsx @@ -45,6 +45,11 @@ interface Inventory_Details_Modal_Interface { itemData: Inventory_Details_Interface | null; // Data of the item to display } +const deleteHelper = (id: number, onclose: () => void) => { + deleteItemClean(id || 0); + onclose(); +}; + const Inventory_Details_Modal: React.FC = ({ isOpen, onClose, @@ -108,7 +113,7 @@ const Inventory_Details_Modal: React.FC = ({