diff --git a/.gitignore b/.gitignore
index 5710fbaa..6293debb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,6 +3,9 @@
# dependencies
node_modules
+.pnp
+.pnp.js
+.yarn/install-state.gz
# virtual environments
mycity-venv/
@@ -16,5 +19,34 @@ __pycache__/
.env.development
.env.production
+# testing
+coverage
+
+# next.js
+.next/
+out/
+
+# production
+build
+
+# misc
+.DS_Store
+*.pem
+
+# debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
# next.js transpiler
.swc
+
+# vercel
+.vercel
+
+# typescript
+*.tsbuildinfo
+next-env.d.ts
+
+# jest & playwright test results
+/test-results
diff --git a/backend/app.py b/backend/app.py
index 51c94979..5c2c2e27 100644
--- a/backend/app.py
+++ b/backend/app.py
@@ -4,6 +4,7 @@
from chalicelib.issues.issues_routes import issues_routes
from chalicelib.tickets.tickets_routes import tickets_blueprint
from chalicelib.searching.searching_routes import searching_blueprint
+from chalicelib.municipalities.municipalities_routes import municipalities_blueprint
app = Chalice(app_name="mycity")
cors_config = CORSConfig(
@@ -20,6 +21,8 @@
app.register_blueprint(searching_blueprint, "Search", "/search")
+app.register_blueprint(municipalities_blueprint, "Municipality", "/municipality")
+
@app.route("/", cors=True)
def index():
diff --git a/backend/chalicelib/municipalities/municipalities_controllers.py b/backend/chalicelib/municipalities/municipalities_controllers.py
new file mode 100644
index 00000000..8f653160
--- /dev/null
+++ b/backend/chalicelib/municipalities/municipalities_controllers.py
@@ -0,0 +1,39 @@
+import boto3
+from botocore.exceptions import ClientError
+from chalice import BadRequestError, Response
+import json
+
+dynamodb = boto3.resource("dynamodb")
+municipalities_table = dynamodb.Table("municipalities")
+
+
+def format_response(status_code, body):
+ return Response(
+ body=json.dumps(body),
+ status_code=status_code,
+ headers={
+ "Access-Control-Allow-Origin": "*",
+ "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE",
+ "Access-Control-Allow-Headers": "Authorization,Content-Type,X-Amz-Date,X-Amz-Security-Token,X-Api-Key",
+ },
+ )
+
+
+def get_all_municipalities():
+ try:
+ response = municipalities_table.scan()
+ municipalities = response.get("Items", [])
+
+ # Note that only the name of the municipality is being fetched
+ municipalities_list = [
+ {
+ "municipality_id": municipality["municipality_id"],
+ }
+ for municipality in municipalities
+ ]
+
+ return format_response(200, municipalities_list)
+
+ except ClientError as e:
+ error_message = e.response["Error"]["Message"]
+ return {"Status": "FAILED", "Error": error_message}
diff --git a/backend/chalicelib/municipalities/municipalities_routes.py b/backend/chalicelib/municipalities/municipalities_routes.py
new file mode 100644
index 00000000..4c30741d
--- /dev/null
+++ b/backend/chalicelib/municipalities/municipalities_routes.py
@@ -0,0 +1,13 @@
+from chalice import Blueprint, BadRequestError, Response
+from chalicelib.municipalities.municipalities_controllers import (
+ get_all_municipalities,
+)
+
+municipalities_blueprint = Blueprint(__name__)
+
+
+@municipalities_blueprint.route("/municipalities-list", methods=["GET"], cors=True)
+# Note that only the name of the municipality is being fetched
+def get_all_municipalities_list():
+ municipalities_list = get_all_municipalities()
+ return municipalities_list
diff --git a/backend/chalicelib/profiles/__init__.py b/backend/chalicelib/profiles/__init__.py
deleted file mode 100644
index 0095f54a..00000000
--- a/backend/chalicelib/profiles/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-# This file can be left empty or used for package-level imports and initialization
diff --git a/backend/chalicelib/profiles/profiles_controllers.py b/backend/chalicelib/profiles/profiles_controllers.py
deleted file mode 100644
index d878277e..00000000
--- a/backend/chalicelib/profiles/profiles_controllers.py
+++ /dev/null
@@ -1,106 +0,0 @@
-import boto3
-from botocore.exceptions import ClientError
-import hashlib
-import re
-
-dynamodb = boto3.resource("dynamodb")
-table = dynamodb.Table("users")
-
-
-def is_valid_email(email):
- return re.match(r"[^@]+@[^@]+\.[^@]+", email) is not None
-
-
-def is_valid_cellphone(cellphone):
- return re.match(r"^0[0-9]{9}$", cellphone) is not None
-
-
-def is_valid_dob(dob):
- return re.match(r"^\d{4}\\d{2}\\d{2}$", dob) is not None
-
-
-def hash_password(password):
- return hashlib.md5(password.encode()).hexdigest()
-
-
-def is_unique_email(email):
- try:
- response = table.scan(
- FilterExpression="email = :email",
- ExpressionAttributeValues={":email": email},
- )
- return response["Count"] == 0
- except ClientError as e:
- return False
-
-
-def update_profile(username, profile_data):
- try:
- # Fetch the current details of the user
- response = table.get_item(Key={"username": username})
- if "Item" not in response:
- return {"status": "error", "message": "User not found"}
-
- current_data = response["Item"]
-
- email = profile_data.get("email", current_data["email"])
- cellphone = profile_data.get("cellphone", current_data["cellphone"])
- dob = profile_data.get("dob", current_data["dob"])
- firstname = profile_data.get("first", current_data["first"])
- surname = profile_data.get("surname", current_data["surname"])
- password = profile_data.get("password")
- municipality_name = profile_data.get(
- "municipality_name", current_data["municipality_name"]
- )
-
- # Check if any required field is missing
- if not all([email, cellphone, dob, firstname, surname, municipality_name]):
- return {"status": "error", "message": "All profile fields are required"}
-
- # Validate data
- if not is_valid_email(email):
- return {"status": "error", "message": "Invalid email format"}
- if not is_valid_cellphone(cellphone):
- return {"status": "error", "message": "Invalid cellphone format"}
- if not is_valid_dob(dob):
- return {"status": "error", "message": "Invalid date of birth format"}
- if email != current_data["email"] and not is_unique_email(email):
- return {"status": "error", "message": "Email already exists"}
-
- hashed_password = (
- hash_password(password) if password else current_data["password"]
- )
-
- # Update the user's details
- update_expression = "set "
- expression_attribute_values = {
- ":email": email,
- ":cellphone": cellphone,
- ":dob": dob,
- ":first": firstname,
- ":surname": surname,
- ":password": hashed_password,
- ":municipality_name": municipality_name,
- }
-
- update_expression += "email = :email, "
- update_expression += "cellphone = :cellphone, "
- update_expression += "dob = :dob, "
- update_expression += "first = :first, "
- update_expression += "surname = :surname, "
- update_expression += "password = :password, "
- update_expression += "municipality_name = :municipality_name, "
-
- update_expression = update_expression.rstrip(", ")
-
- response = table.update_item(
- Key={"username": username},
- UpdateExpression=update_expression,
- ExpressionAttributeValues=expression_attribute_values,
- ReturnValues="UPDATED_NEW",
- )
-
- return {"status": "success", "updated_attributes": response.get("Attributes")}
-
- except ClientError as e:
- return {"status": "error", "message": str(e)}
diff --git a/backend/chalicelib/profiles/profiles_routes.py b/backend/chalicelib/profiles/profiles_routes.py
deleted file mode 100644
index f684f2ba..00000000
--- a/backend/chalicelib/profiles/profiles_routes.py
+++ /dev/null
@@ -1,28 +0,0 @@
-from chalice import Blueprint, Response
-from chalicelib.profiles.profiles_controllers import update_citizen_profile
-
-profile_routes = Blueprint(__name__)
-
-
-@profile_routes.route(
- "/profile/update", methods=["PUT"], content_types=["application/json"]
-)
-def update_user_profile():
- request = profile_routes.current_request
- profile_data = request.json_body
- username = request.context["authorizer"]["username"]
-
- if not username:
- return Response(body={"error": "Unauthorized"}, status_code=401)
-
- result = update_citizen_profile(username, profile_data)
- if result["status"] == "error":
- return Response(body={"error": result["message"]}, status_code=400)
-
- return Response(
- body={
- "message": "Profile updated successfully",
- "updated_attributes": result["updated_attributes"],
- },
- status_code=200,
- )
diff --git a/backend/chalicelib/tickets/tickets_controllers.py b/backend/chalicelib/tickets/tickets_controllers.py
index 4d00f6d0..e08f4d6e 100644
--- a/backend/chalicelib/tickets/tickets_controllers.py
+++ b/backend/chalicelib/tickets/tickets_controllers.py
@@ -220,6 +220,14 @@ def get_in_my_municipality(tickets_data):
"municipality_id",
]
for field in required_fields:
+ if tickets_data == None:
+ error_response = {
+ "Error": {
+ "Code": "Nothing",
+ "Message": f"No data has been sent",
+ }
+ }
+ raise ClientError(error_response, "NothingSent")
if field not in tickets_data:
error_response = {
"Error": {
diff --git a/backend/requirements.txt b/backend/requirements.txt
index 5b07d0c3..d6e3c937 100644
--- a/backend/requirements.txt
+++ b/backend/requirements.txt
@@ -3,10 +3,10 @@ bcrypt==4.1.3
black==24.4.2
blessed==1.20.0
blinker==1.8.2
-boto3==1.34.117
-botocore==1.34.111
+boto3==1.34.132
+botocore==1.34.132
certifi==2024.6.2
-chalice==1.31.1
+chalice==1.31.2
chardet==5.2.0
charset-normalizer==3.3.2
click==8.1.7
@@ -21,9 +21,10 @@ jinxed==1.2.1
jmespath==1.0.1
MarkupSafe==2.1.5
mypy-extensions==1.0.0
-packaging==24.0
+packaging==24.1
pathspec==0.12.1
platformdirs==4.2.2
+pur==7.3.2
pyasn1==0.6.0
python-dateutil==2.9.0.post0
python-dotenv==1.0.1
@@ -31,8 +32,10 @@ python-editor==1.0.4
PyYAML==6.0.1
readchar==4.1.0
requests==2.32.3
-s3transfer==0.10.1
+s3transfer==0.10.2
six==1.16.0
-urllib3==2.2.1
+tomli==2.0.1
+typing_extensions==4.12.2
+urllib3==2.2.2
wcwidth==0.2.13
Werkzeug==3.0.3
diff --git a/frontend/__tests__/components/Dashboard/FaultCardContainer.test.tsx b/frontend/__tests__/components/Dashboard/FaultCardContainer.test.tsx
new file mode 100644
index 00000000..0054634e
--- /dev/null
+++ b/frontend/__tests__/components/Dashboard/FaultCardContainer.test.tsx
@@ -0,0 +1,58 @@
+import CitizenLogin from "@/components/Login/CitizenLogin";
+import { fireEvent, render, screen } from "@testing-library/react";
+
+describe("Dashboard", () => {
+
+
+ /* it("renders an email input", () => {
+ render();
+ const emailInput = screen.getByLabelText("Email");
+
+ expect(emailInput).toBeInTheDocument();
+ expect(emailInput).toHaveAttribute("type", "email");
+ }); */
+
+ it("renders an email input", () => {
+ render();
+ // Use getByPlaceholderText if the placeholder is unique
+ const emailInput = screen.getByPlaceholderText("example@mail.com");
+
+ expect(emailInput).toBeInTheDocument();
+ expect(emailInput).toHaveAttribute("type", "email");
+ });
+
+ // it("renders a password input", () => {
+ // render();
+ // const passwordInput = screen.getByLabelText("Password");
+
+ // expect(passwordInput).toBeInTheDocument();
+ // expect(passwordInput).toHaveAttribute("type", "password");
+ // });
+
+
+ // it("renders a forgot password link", () => {
+ // render();
+ // const forgotPasswordLink = screen.getByText("Forgot password?");
+
+ // expect(forgotPasswordLink).toBeInTheDocument();
+ // });
+
+ // it("renders a Login button", () => {
+ // render();
+ // const submitButton = screen.getByTestId("submit-btn")
+
+ // expect(submitButton).toBeInTheDocument();
+ // });
+
+
+ // test("handler function is called after clicking submit button", () => {
+ // render();
+ // const mockFunction = jest.fn();
+ // const loginForm = screen.getByTestId("citizen-login-form");
+ // loginForm.addEventListener("submit", mockFunction);
+
+ // fireEvent.submit(loginForm)
+ // expect(mockFunction).toHaveBeenCalledTimes(1);
+ // });
+
+});
\ No newline at end of file
diff --git a/frontend/__tests__/components/Login/CitizenLogin.test.tsx b/frontend/__tests__/components/Login/CitizenLogin.test.tsx
index 4cbfe299..a03eda46 100644
--- a/frontend/__tests__/components/Login/CitizenLogin.test.tsx
+++ b/frontend/__tests__/components/Login/CitizenLogin.test.tsx
@@ -3,15 +3,6 @@ import { fireEvent, render, screen } from "@testing-library/react";
describe("CitizenLogin", () => {
-
- /* it("renders an email input", () => {
- render();
- const emailInput = screen.getByLabelText("Email");
-
- expect(emailInput).toBeInTheDocument();
- expect(emailInput).toHaveAttribute("type", "email");
- }); */
-
it("renders an email input", () => {
render();
// Use getByPlaceholderText if the placeholder is unique
@@ -21,38 +12,39 @@ describe("CitizenLogin", () => {
expect(emailInput).toHaveAttribute("type", "email");
});
- // it("renders a password input", () => {
- // render();
- // const passwordInput = screen.getByLabelText("Password");
+ it("renders a password input", () => {
+ render();
+ const passwordInput = screen.getByLabelText(/Password/);
- // expect(passwordInput).toBeInTheDocument();
- // expect(passwordInput).toHaveAttribute("type", "password");
- // });
+ expect(passwordInput).toBeInTheDocument();
+ expect(passwordInput).toHaveAttribute("type", "password");
+ });
- // it("renders a forgot password link", () => {
- // render();
- // const forgotPasswordLink = screen.getByText("Forgot password?");
+ it("renders a forgot password link", () => {
+ render();
+ const forgotPasswordLink = screen.getByText("Forgot password?");
- // expect(forgotPasswordLink).toBeInTheDocument();
- // });
+ expect(forgotPasswordLink).toBeInTheDocument();
+ });
- // it("renders a Login button", () => {
- // render();
- // const submitButton = screen.getByTestId("submit-btn")
+ it("renders a Login button", () => {
+ render();
+ const submitButton = screen.getByTestId("submit-btn")
- // expect(submitButton).toBeInTheDocument();
- // });
+ expect(submitButton).toBeInTheDocument();
+ });
- // test("handler function is called after clicking submit button", () => {
- // render();
- // const mockFunction = jest.fn();
- // const loginForm = screen.getByTestId("citizen-login-form");
- // loginForm.addEventListener("submit", mockFunction);
+ test("handler function is called after clicking submit button", () => {
+ render();
+ const mockFunction = jest.fn();
+ // const mockLogin = handleSignIn.mockResolvedValue({ status: 200 });
+ const loginForm = screen.getByTestId("citizen-login-form");
+ loginForm.addEventListener("submit", mockFunction);
- // fireEvent.submit(loginForm)
- // expect(mockFunction).toHaveBeenCalledTimes(1);
- // });
+ fireEvent.submit(loginForm)
+ expect(mockFunction).toHaveBeenCalledTimes(1);
+ });
});
\ No newline at end of file
diff --git a/frontend/__tests__/components/Login/MunicipalityLogin.test.tsx b/frontend/__tests__/components/Login/MunicipalityLogin.test.tsx
index 081ada73..c142c350 100644
--- a/frontend/__tests__/components/Login/MunicipalityLogin.test.tsx
+++ b/frontend/__tests__/components/Login/MunicipalityLogin.test.tsx
@@ -3,14 +3,6 @@ import { fireEvent, render, screen } from "@testing-library/react";
describe("MunicipalityLogin", () => {
- /*it("renders an email input", () => {
- render();
- const emailInput = screen.getByLabelText("Email");
-
- expect(emailInput).toBeInTheDocument();
- expect(emailInput).toHaveAttribute("type", "email");
- });*/
-
it("renders an email input", () => {
render();
// Use getByPlaceholderText if the placeholder is unique
@@ -20,36 +12,31 @@ describe("MunicipalityLogin", () => {
expect(emailInput).toHaveAttribute("type", "email");
});
- // it("renders textbox to select municipality", () => {
- // render();
- // const muniAutocomplete = screen.getByRole("combobox");
- // expect(muniAutocomplete).toHaveAttribute("type", "text");
- // });
+ it("renders a password input", () => {
+ render();
+ const passwordInput = screen.getByLabelText(/Municipality Password/);
+ expect(passwordInput).toHaveAttribute("type", "password");
+ });
- // it("renders a password input", () => {
- // render();
- // const passwordInput = screen.getByPlaceholderText("Password");
- // expect(passwordInput).toHaveAttribute("type", "password");
- // });
+ it("renders a submit button", () => {
+ render();
- // it("renders a submit button", () => {
- // render();
+ const submitButton = screen.getByTestId("submit-btn")
- // const submitButton = screen.getByRole("button", { name: /submit/i });
+ expect(submitButton).toBeInTheDocument();
- // expect(submitButton).toBeInTheDocument();
- // expect(submitButton).toHaveTextContent("Submit");
- // });
+ expect(submitButton).toHaveTextContent("Submit");
+ });
- // test("handler function is called after clicking submit button", () => {
- // render();
- // const mockFunction = jest.fn();
- // const loginForm = screen.getByTestId("municipality-login-form");
- // loginForm.addEventListener("submit", mockFunction);
+ test("handler function is called after clicking submit button", () => {
+ render();
+ const mockFunction = jest.fn();
+ const loginForm = screen.getByTestId("municipality-login-form");
+ loginForm.addEventListener("submit", mockFunction);
- // fireEvent.submit(loginForm)
- // expect(mockFunction).toHaveBeenCalledTimes(1);
- // });
+ fireEvent.submit(loginForm)
+ expect(mockFunction).toHaveBeenCalledTimes(1);
+ });
});
\ No newline at end of file
diff --git a/frontend/__tests__/components/Login/ServiceProviderLogin.test.tsx b/frontend/__tests__/components/Login/ServiceProviderLogin.test.tsx
index ebda9322..1c4f27a1 100644
--- a/frontend/__tests__/components/Login/ServiceProviderLogin.test.tsx
+++ b/frontend/__tests__/components/Login/ServiceProviderLogin.test.tsx
@@ -3,14 +3,6 @@ import { fireEvent, render, screen } from "@testing-library/react";
describe("ServiceProviderLogin", () => {
- /*it("renders an email input", () => {
- render();
- const emailInput = screen.getByLabelText("Email");
-
- expect(emailInput).toBeInTheDocument();
- expect(emailInput).toHaveAttribute("type", "email");
- });*/
-
it("renders an email input", () => {
render();
// Use getByPlaceholderText if the placeholder is unique
@@ -20,42 +12,34 @@ describe("ServiceProviderLogin", () => {
expect(emailInput).toHaveAttribute("type", "email");
});
- // it("renders a password input", () => {
- // render();
- // const passwordInput = screen.getByLabelText("Password");
-
- // expect(passwordInput).toBeInTheDocument();
- // expect(passwordInput).toHaveAttribute("type", "password");
- // });
-
+ it("renders a password input", () => {
+ render();
+ const passwordInput = screen.getByLabelText(/Service Provider Password/);
- // it("renders a forgot password link", () => {
- // render();
+ expect(passwordInput).toBeInTheDocument();
+ expect(passwordInput).toHaveAttribute("type", "password");
+ });
- // const forgotPasswordLink = screen.getByRole("link");
- // expect(forgotPasswordLink).toBeInTheDocument();
- // expect(forgotPasswordLink).toHaveTextContent("Forgot password?");
- // });
+ it("renders a submit button", () => {
+ render();
- // it("renders a submit button", () => {
- // render();
+ const submitButton = screen.getByTestId("submit-btn")
- // const submitButton = screen.getByRole("button", { name: /submit/i });
+ expect(submitButton).toBeInTheDocument();
- // expect(submitButton).toBeInTheDocument();
- // expect(submitButton).toHaveTextContent("Submit");
- // });
+ expect(submitButton).toHaveTextContent("Submit");
+ });
- // test("handler function is called after clicking submit button", () => {
- // render();
- // const mockFunction = jest.fn();
- // const loginForm = screen.getByTestId("service-provider-login-form");
- // loginForm.addEventListener("submit", mockFunction);
+ test("handler function is called after clicking submit button", () => {
+ render();
+ const mockFunction = jest.fn();
+ const loginForm = screen.getByTestId("service-provider-login-form");
+ loginForm.addEventListener("submit", mockFunction);
- // fireEvent.submit(loginForm);
- // expect(mockFunction).toHaveBeenCalledTimes(1);
- // });
+ fireEvent.submit(loginForm);
+ expect(mockFunction).toHaveBeenCalledTimes(1);
+ });
});
\ No newline at end of file
diff --git a/frontend/__tests__/components/Navbar/Navbar.test.tsx b/frontend/__tests__/components/Navbar/Navbar.test.tsx
new file mode 100644
index 00000000..23b047c9
--- /dev/null
+++ b/frontend/__tests__/components/Navbar/Navbar.test.tsx
@@ -0,0 +1,71 @@
+import NavbarUser from "@/components/Navbar/NavbarUser";
+import { fireEvent, render, screen } from "@testing-library/react";
+import { act } from "react";
+
+
+describe("Authenticated user Navbar", () => {
+
+ it("renders a dashboard link", () => {
+
+ act(() => {
+ render();
+ });
+
+ const dashboardLink = screen.getByRole("link", {name:/Dashboard/});
+
+ expect(dashboardLink).toBeInTheDocument();
+ expect(dashboardLink).toHaveAttribute("href", "/dashboard/citizen");
+ });
+
+
+
+ test("logout button triggers logout event", () => {
+
+ act(() => {
+ render();
+ });
+
+ const profileAvatar = screen.getByTestId("profile-dropdown-trigger");
+
+ act(() => {
+ fireEvent.click(profileAvatar);
+
+ });
+
+ const logoutButton = screen.getByText(/Log out/);
+
+
+ const mockFunction = jest.fn();
+ logoutButton.addEventListener("click", mockFunction);
+
+ expect(logoutButton).toBeInTheDocument();
+ act(() => {
+ logoutButton.click();
+ });
+
+ expect(mockFunction).toHaveBeenCalledTimes(1);
+ });
+
+
+
+});
+
+// describe("Unauthenticated user Navbar", () => {
+
+// it("renders an email input", () => {
+// render();
+// // Use getByPlaceholderText if the placeholder is unique
+// const emailInput = screen.getByPlaceholderText("example@mail.com");
+
+// expect(emailInput).toBeInTheDocument();
+// expect(emailInput).toHaveAttribute("type", "email");
+// });
+
+// it("renders a password input", () => {
+// render();
+// const passwordInput = screen.getByLabelText(/Password/);
+
+// expect(passwordInput).toBeInTheDocument();
+// expect(passwordInput).toHaveAttribute("type", "password");
+// });
+// });
\ No newline at end of file
diff --git a/frontend/e2e/help.spec.ts b/frontend/e2e/help.spec.ts
new file mode 100644
index 00000000..64671def
--- /dev/null
+++ b/frontend/e2e/help.spec.ts
@@ -0,0 +1,64 @@
+import { test, expect } from '@playwright/test';
+
+test.describe("help menu", () => {
+
+ test("user can access help menu on any page", async ({ page }) => {
+ await page.goto("http://localhost:3000/");
+ await page.waitForTimeout(5000);
+
+ // login
+ await page.getByTestId("login-btn").click();
+
+ await page.waitForTimeout(5000);
+
+ await page.getByLabel("Email").fill("janedoe@example.com");
+ await page.getByLabel("Password").fill("Password@123");
+
+ await page.waitForTimeout(5000);
+
+ // click on login button
+ await page.getByTestId("submit-btn").click();
+
+ await page.waitForURL("http://localhost:3000/dashboard/citizen");
+
+ // await page.waitForTimeout(5000);
+ // page.getByTestId("open-help-menu").click();
+ // expect(page.getByTestId("help")).toBeVisible();
+
+ // await page.waitForTimeout(5000);
+ // page.getByTestId("close-help-menu").click();
+
+ await page.waitForTimeout(5000);
+
+ await page.goto("http://localhost:3000/search/citizen");
+
+
+
+ //try search
+ page.getByTestId("searchbox").fill("mak");
+ page.getByTestId("filter").click();
+ await page.waitForTimeout(1000);
+ await page.getByText(/Municipality Tickets/).click();
+ page.getByTestId("search-btn").click();
+
+ await page.waitForTimeout(10000);
+
+ // page.getByTestId("open-help-menu").click();
+ // expect(page.getByTestId("help")).toBeVisible();
+ // await page.waitForTimeout(5000);
+ // page.getByTestId("close-help-menu").click();
+
+ // await page.waitForTimeout(5000);
+
+
+ await page.goto("http://localhost:3000/settings/citizen");
+ page.getByTestId("open-help-menu").click();
+ expect(page.getByTestId("help")).toBeVisible();
+ // await page.waitForTimeout(5000);
+ // page.getByTestId("close-help-menu").click();
+
+ // await page.waitForTimeout(5000);
+
+ });
+
+});
\ No newline at end of file
diff --git a/frontend/e2e/login.spec.ts b/frontend/e2e/login.spec.ts
index 172d0f4d..d1996169 100644
--- a/frontend/e2e/login.spec.ts
+++ b/frontend/e2e/login.spec.ts
@@ -1,22 +1,35 @@
import { test, expect } from '@playwright/test';
-test("citizen login takes them to dashboard", async ({ page }) => {
- await page.goto("http://localhost:3000/");
+test.describe("login", () => {
- expect(page.url()).toEqual("http://localhost:3000");
+ test("unauthenticated citizen cannot access dashboard", async ({ page }) => {
+ await page.goto("http://localhost:3000/");
- // click the login link
- // await page.getByText("Login").click();
+ await page.goto("http://localhost:3000/dashboard/citizen");
- // // fill in email
- // await page.getByLabel("Email").fill("james@gmail.com");
+ expect(page.url()).not.toEqual("http://localhost:3000/dashboard/citizen");
+ });
- // // fill in password
- // await page.getByLabel("Password").fill("password");
- // // click on login button
- // await page.getByText("Login").click();
- // // expects page url to not have changed
- // expect(page.url()).toEqual("http://localhost:3000/dashboard");
-});
+ test("citizen is redirected to dashboard on successful login", async ({ page }) => {
+ await page.goto("http://localhost:3000/");
+
+ // click the login link
+ await page.getByText("Log In").click();
+
+ // fill in email
+ await page.getByLabel("Email").fill("janedoe@example.com");
+
+ // fill in password
+ await page.getByLabel("Password").fill("Password@123");
+
+ // click on login button
+ await page.getByText("Log In").click();
+
+ // expects page url to not have changed
+
+ expect(page.url()).toEqual("http://localhost:3000/dashboard/citizen");
+ });
+
+});
\ No newline at end of file
diff --git a/frontend/jest.setup.ts b/frontend/jest.setup.ts
index 7da0d534..f2296895 100644
--- a/frontend/jest.setup.ts
+++ b/frontend/jest.setup.ts
@@ -2,12 +2,29 @@ import "@testing-library/jest-dom";
jest.mock('next/navigation', () => ({
usePathname: jest.fn(),
- useRouter: jest.fn()
+ useRouter: jest.fn(() => { return { push: (url: string) => { return true; } } })
}));
+jest.mock('./src/services/auth.service', () => ({
+ handleSignIn: jest.fn(() => { const isSignedIn = false; return { isSignedIn }; }),
+ handleSignUp: jest.fn(() => { const isSignedIn = false; return { isSignedIn }; }),
+ authenticateClient: jest.fn(() => { return false; }),
+ handleSignOut: jest.fn(),
+ // signIn: jest.fn(() => { const isSignedIn = true; return isSignedIn; }),
+ // fetchAuthSession: jest.fn(),
+ // autoSignIn: jest.fn()
+}));
+
+jest.mock('./src/utils/authActions');
+jest.mock('./src/hooks/useProfile', () => ({
+ useProfile: () => { return { getUserProfile: jest.fn(() => { return { current: {} }; }) } },
+}));
+
// jest.mock('aws-amplify/auth', () => ({
-// signUp: jest.fn(),
-// signIn:jest.fn(),
-// signOut:jest.fn()
+// signIn: jest.fn(() => { const isSignedIn = false; return { isSignedIn }; }),
+// signUp: jest.fn(() => { const isSignedIn = false; return { isSignedIn }; }),
+// signOut: jest.fn(),
+// fetchAuthSession: jest.fn(() => { return { session: { tokens: {} } } }),
+// // autoSignIn: jest.fn()
// }));
diff --git a/frontend/next.config.mjs b/frontend/next.config.mjs
index c749a69e..fe3a1358 100644
--- a/frontend/next.config.mjs
+++ b/frontend/next.config.mjs
@@ -1,23 +1,22 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
env: {
- GOOGLE_MAPS_API_KEY: process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY,
USER_POOL_ID: process.env.NEXT_PUBLIC_USER_POOL_ID,
USER_POOL_CLIENT_ID: process.env.NEXT_PUBLIC_USER_POOL_CLIENT_ID,
MAPBOX_ACCESS_TOKEN: process.env.NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN,
- PLACEKIT_API_KEY: process.env.NEXT_PUBLIC_PLACEKIT_API_KEY
+ PLACEKIT_API_KEY: process.env.NEXT_PUBLIC_PLACEKIT_API_KEY,
+ API_BASE_URL: process.env.NEXT_PUBLIC_API_BASE_URL
},
- images: {
- unoptimized: !process.env.PROD_ENV ? true : false,
- unoptimized: true,
- remotePatterns: [
- {
- protocol: "https",
- hostname: "i.imgur.com",
- },
- ],
- },
+ // images: {
+ // unoptimized: process.env.NODE_ENV != "production" ? true : false,
+ // remotePatterns: [
+ // {
+ // protocol: "https",
+ // hostname: "i.imgur.com",
+ // },
+ // ],
+ // },
webpack(config) {
// Grab the existing rule that handles SVG imports
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index bca014dd..2d881cdc 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -16,7 +16,6 @@
"@svgr/webpack": "^8.1.0",
"@types/react-modal": "^3.16.3",
"aws-amplify": "^6.3.6",
- "axios": "^1.7.2",
"framer-motion": "^11.2.6",
"lucide-react": "^0.383.0",
"mapbox-gl": "^3.4.0",
@@ -20955,7 +20954,8 @@
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
- "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "dev": true
},
"node_modules/auto-bind": {
"version": "4.0.0",
@@ -21519,16 +21519,6 @@
"node": ">=4"
}
},
- "node_modules/axios": {
- "version": "1.7.2",
- "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz",
- "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==",
- "dependencies": {
- "follow-redirects": "^1.15.6",
- "form-data": "^4.0.0",
- "proxy-from-env": "^1.1.0"
- }
- },
"node_modules/axobject-query": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz",
@@ -22424,6 +22414,7 @@
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "dev": true,
"dependencies": {
"delayed-stream": "~1.0.0"
},
@@ -23153,6 +23144,7 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "dev": true,
"engines": {
"node": ">=0.4.0"
}
@@ -24424,25 +24416,6 @@
"integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==",
"dev": true
},
- "node_modules/follow-redirects": {
- "version": "1.15.6",
- "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
- "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
- "funding": [
- {
- "type": "individual",
- "url": "https://github.com/sponsors/RubenVerborgh"
- }
- ],
- "engines": {
- "node": ">=4.0"
- },
- "peerDependenciesMeta": {
- "debug": {
- "optional": true
- }
- }
- },
"node_modules/for-each": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
@@ -24471,6 +24444,7 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
+ "dev": true,
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
@@ -27482,6 +27456,7 @@
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "dev": true,
"engines": {
"node": ">= 0.6"
}
@@ -27490,6 +27465,7 @@
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "dev": true,
"dependencies": {
"mime-db": "1.52.0"
},
@@ -28857,11 +28833,6 @@
"resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz",
"integrity": "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw=="
},
- "node_modules/proxy-from-env": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
- "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
- },
"node_modules/psl": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index b7810bc1..d87c1c5f 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -7,7 +7,7 @@
"build": "next build",
"start": "next start",
"lint": "next lint",
- "test": "jest",
+ "test": "jest --silent",
"test:watch": "jest --watch",
"test:e2e": "playwright test"
},
@@ -20,7 +20,6 @@
"@svgr/webpack": "^8.1.0",
"@types/react-modal": "^3.16.3",
"aws-amplify": "^6.3.6",
- "axios": "^1.7.2",
"framer-motion": "^11.2.6",
"lucide-react": "^0.383.0",
"mapbox-gl": "^3.4.0",
diff --git a/frontend/playwright.config.ts b/frontend/playwright.config.ts
index 0d18b5fd..2b3aa8cc 100644
--- a/frontend/playwright.config.ts
+++ b/frontend/playwright.config.ts
@@ -12,6 +12,7 @@ import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './e2e',
outputDir: 'test-results/playwright/results',
+ timeout: 120000,
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
diff --git a/frontend/src/app/api/[...slug]/route.ts b/frontend/src/app/api/[...slug]/route.ts
new file mode 100644
index 00000000..d6afc811
--- /dev/null
+++ b/frontend/src/app/api/[...slug]/route.ts
@@ -0,0 +1,50 @@
+import { NextRequest } from "next/server";
+
+const API_BASE_URL = process.env.API_BASE_URL;
+
+export async function GET(req: NextRequest, { params }: { params: { slug: string[] } }) {
+
+ if (!API_BASE_URL) {
+ throw new Error("missing api base url");
+ }
+
+ const endpointUrl = req.url.replace("http://localhost:3000", API_BASE_URL);
+ const etag = params.slug.join("-");
+
+ const res = await fetch(endpointUrl, {
+ headers: req.headers,
+ next: { tags: [etag] },
+ cache: "force-cache"
+ });
+
+ const data = await res.json();
+
+ return Response.json({ data });
+}
+
+
+export async function POST(req: NextRequest, { params }: { params: { slug: string[] } }) {
+
+ if (!API_BASE_URL) {
+ throw new Error("missing api base url");
+ }
+
+ const endpointUrl = req.url.replace("http://localhost:3000", API_BASE_URL);
+ const etag = params.slug.join("-");
+
+ const requestBody = await req.json();
+
+ const res = await fetch(endpointUrl, {
+ method: "POST",
+ headers: req.headers,
+ body: JSON.stringify(requestBody),
+ next: { tags: [etag] },
+ cache: "force-cache"
+ });
+
+
+ const data = await res.json();
+
+
+ return Response.json({ data });
+}
\ No newline at end of file
diff --git a/frontend/src/app/dashboard/citizen/page.tsx b/frontend/src/app/dashboard/citizen/page.tsx
index fb93db1e..5bade657 100644
--- a/frontend/src/app/dashboard/citizen/page.tsx
+++ b/frontend/src/app/dashboard/citizen/page.tsx
@@ -1,18 +1,15 @@
-"use client";
+'use client'
import React, { Key, useEffect, useRef, useState } from "react";
import { Tabs, Tab } from "@nextui-org/react";
-import FaultCardContainer from "@/components/FaultCardContainer/FaultCardContainer";
import FaultTable from "@/components/FaultTable/FaultTable";
import FaultMapView from "@/components/FaultMapView/FaultMapView";
import Navbar from "@/components/Navbar/Navbar";
-import { useProfile } from "@/context/UserProfileContext";
-import { FaQuestionCircle, FaTimes } from "react-icons/fa";
+import { FaTimes } from "react-icons/fa";
import { HelpCircle } from "lucide-react";
import DashboardFaultCardContainer from "@/components/FaultCardContainer/DashboardFualtCardContainer";
-import axios from "axios";
-
-
+import { useProfile } from "@/hooks/useProfile";
+import { getTicket, getTicketsInMunicipality } from "@/services/tickets.service";
export default function CitizenDashboard() {
const user = useRef(null);
@@ -24,30 +21,26 @@ export default function CitizenDashboard() {
useEffect(() => {
const fetchData = async () => {
try {
- const user_data = await userProfile.getUserProfile()
- const user_id = user_data.current?.sub
- const rspwatchlist = await axios.post('https://f1ihjeakmg.execute-api.af-south-1.amazonaws.com/api/tickets/view?ticket_id=8f4cf09d-754e-4d71-96dc-952173fab07c',{
- username : user_id
- });
- const municipality = user_data.current?.municipality
- const rspmunicipality = await axios.post('https://f1ihjeakmg.execute-api.af-south-1.amazonaws.com/api/tickets/getinarea',{
- municipality_id : municipality
- });
- console.log(user_id)
- console.log(rspmunicipality.data)
- console.log(municipality)
- console.log(rspwatchlist.data)
- const flattenedWatchlist = rspwatchlist.data.flat();
- setDashMuniResults(rspmunicipality.data)
- setDashWatchResults(flattenedWatchlist)
-
+ const user_data = await userProfile.getUserProfile();
+ const user_id = user_data.current?.sub;
+ const mockTicketId = "8f4cf09d-754e-4d71-96dc-952173fab07c";
+ const rspwatchlist = await getTicket(mockTicketId);
+ const municipality = user_data.current?.municipality;
+ const rspmunicipality = await getTicketsInMunicipality(municipality);
+ console.log(user_id);
+ console.log(rspmunicipality);
+ console.log(municipality);
+ const flattenedWatchlist = rspwatchlist.flat();
+ console.log(flattenedWatchlist);
+ setDashMuniResults(Array.isArray(rspmunicipality) ? rspmunicipality : []);
+ setDashWatchResults(flattenedWatchlist);
} catch (error) {
- console.log(error)
+ console.log(error);
}
};
fetchData();
- }, []);
+ }, [userProfile]); // Add userProfile to the dependency array
const handleTabChange = (key: Key) => {
const index = Number(key);
@@ -57,8 +50,8 @@ export default function CitizenDashboard() {
setIsHelpOpen(!isHelpOpen);
};
- const hasStatusFieldMuni = dashMuniResults.some(item => item.Status !== undefined);
- const hasStatusFieldWatch = dashWatchResults.some(item => item.Status !== undefined);
+ const hasStatusFieldMuni = Array.isArray(dashMuniResults) && dashMuniResults.some(item => item.Status !== undefined);
+ const hasStatusFieldWatch = Array.isArray(dashWatchResults) && dashWatchResults.some(item => item.Status !== undefined);
return (
@@ -82,17 +75,22 @@ export default function CitizenDashboard() {
Dashboard
-
+ {/* Persistent Help Icon */}
+
+
+
{isHelpOpen && (
-
+
-
+
+
+
Nearest to you
-
-
- Based on your proximity to the issue.
-
-
-
-
-
- {hasStatusFieldMuni ? (
-
- ) : (
+
+ Based on your proximity to the issue.
+
+
- )}
+
Watchlist
-
-
- Based on your watchlist.
-
-
-
-
-
- {hasStatusFieldWatch ? (
-
- ) : (
+
+
+ All of the issues you have added to your watchlist.
+
+
+
- )}
diff --git a/frontend/src/app/home/page.tsx b/frontend/src/app/home/page.tsx
index 3cad838e..399fd8f9 100644
--- a/frontend/src/app/home/page.tsx
+++ b/frontend/src/app/home/page.tsx
@@ -29,7 +29,7 @@ export default function Home() {
{/* Content */}
- {/* Ensure content is above the background */}
+
{/* Ensure content is above the background */}
Be the change in your city
with MyCity.
@@ -46,7 +46,7 @@ export default function Home() {
-
+
diff --git a/frontend/src/app/profile/citizen/page.tsx b/frontend/src/app/profile/citizen/page.tsx
index 68bf9818..1986146a 100644
--- a/frontend/src/app/profile/citizen/page.tsx
+++ b/frontend/src/app/profile/citizen/page.tsx
@@ -3,8 +3,8 @@
import React, { useEffect, useState } from "react";
import EditCitizenProfile from '@/components/EditProfile/UserProfile';
import Navbar from "@/components/Navbar/Navbar";
-import { useProfile } from "@/context/UserProfileContext";
-import { UserData } from "@/types/user.types";
+import { UserData } from "@/types/custom.types";
+import { useProfile } from "@/hooks/useProfile";
export default function CitizenProfile() {
diff --git a/frontend/src/app/search/citizen/page.tsx b/frontend/src/app/search/citizen/page.tsx
index 077e6bdb..89fde43b 100644
--- a/frontend/src/app/search/citizen/page.tsx
+++ b/frontend/src/app/search/citizen/page.tsx
@@ -5,17 +5,16 @@ import Navbar from "@/components/Navbar/Navbar";
import SearchTicket from "@/components/Search/SearchTicket";
import SearchMunicipality from "@/components/Search/SearchMunicipality";
import SearchSP from "@/components/Search/SearchSP";
-import axios from "axios";
import { FaFilter } from "react-icons/fa";
import { HelpCircle, X } from "lucide-react";
-import Modal from "react-modal";
import { ThreeDots } from "react-loader-spinner";
+import { searchIssue, searchMunicipality, searchMunicipalityTickets, searchServiceProvider } from "@/services/search.service";
export default function CreateTicket() {
const [searchTerm, setSearchTerm] = useState("");
const [searchResults, setSearchResults] = useState([]);
const [selectedFilter, setSelectedFilter] = useState<
- "myLocation" | "serviceProviders" | "municipalities"
+ "myLocation" | "serviceProviders" | "municipalities" | "municipalityTickets"
>("myLocation");
const [isFilterOpen, setIsFilterOpen] = useState(false);
const filterRef = useRef(null);
@@ -23,54 +22,52 @@ export default function CreateTicket() {
const [municipalityTickets, setMunicipalityTickets] = useState([]);
const [isHelpModalOpen, setIsHelpModalOpen] = useState(false);
const [loading, setLoading] = useState(false);
+ const [currentPage, setCurrentPage] = useState(1);
+ const [resultsPerPage] = useState(10);
+ const [searchTime, setSearchTime] = useState(0);
+ const [showToast, setShowToast] = useState(false);
+ const [totalResults, setTotalResults] = useState(0);
const handleSearch = async () => {
try {
setHasSearched(true);
setLoading(true);
- let response = { data: [] };
+ const startTime = Date.now();
+ let data: any[] = [];
+ let allTickets = [];
switch (selectedFilter) {
case "myLocation":
- response = await axios.get(
- `https://f1ihjeakmg.execute-api.af-south-1.amazonaws.com/api/search/issues?q=${encodeURIComponent(
- searchTerm
- )}`
- );
+ data = await searchIssue(searchTerm);
break;
case "serviceProviders":
- response = await axios.get(
- `https://f1ihjeakmg.execute-api.af-south-1.amazonaws.com/api/search/service-provider?q=${encodeURIComponent(
- searchTerm
- )}`
- );
+ data = await searchServiceProvider(searchTerm);
break;
case "municipalities":
- response = await axios.get(
- `https://f1ihjeakmg.execute-api.af-south-1.amazonaws.com/api/search/municipality?q=${encodeURIComponent(
- searchTerm
- )}`
- );
- const municipalityIds = response.data.map(
+ data = await searchMunicipality(searchTerm);
+ break;
+ case "municipalityTickets":
+ data = await searchMunicipality(searchTerm);
+ const municipalityIds = data.map(
(municipality: any) => municipality.municipality_id
);
const ticketsPromises = municipalityIds.map(
- (municipalityId: string) =>
- axios.get(
- `https://f1ihjeakmg.execute-api.af-south-1.amazonaws.com/api/search/municipality-tickets?q=${encodeURIComponent(
- municipalityId
- )}`
- )
+ (municipalityId: string) => searchMunicipalityTickets(municipalityId)
);
const ticketsResponses = await Promise.all(ticketsPromises);
- const allTickets = ticketsResponses.flatMap(
- (response) => response.data
+ allTickets = ticketsResponses.flatMap(
+ (response) => response
);
setMunicipalityTickets(allTickets);
break;
default:
break;
}
- setSearchResults(response.data);
+ const endTime = Date.now();
+ setSearchTime((endTime - startTime) / 1000); // Time in seconds
+ setTotalResults(selectedFilter === "municipalityTickets" ? allTickets.length : data.length);
+ setSearchResults(selectedFilter === "municipalityTickets" ? allTickets : data);
+ setShowToast(true);
+ setTimeout(() => setShowToast(false), 3000); // Hide toast after 3 seconds
setLoading(false);
} catch (error) {
console.error("Error fetching search results:", error);
@@ -79,13 +76,14 @@ export default function CreateTicket() {
};
const handleFilterChange = (
- filter: "myLocation" | "serviceProviders" | "municipalities"
+ filter: "myLocation" | "serviceProviders" | "municipalities" | "municipalityTickets"
) => {
setSelectedFilter(filter);
setIsFilterOpen(false);
setSearchResults([]);
setHasSearched(false);
setMunicipalityTickets([]);
+ setCurrentPage(1); // Reset to the first page on filter change
};
const handleSearchInputChange = (
@@ -121,12 +119,18 @@ export default function CreateTicket() {
};
}, []);
+ const paginate = (pageNumber: number) => setCurrentPage(pageNumber);
+
+ const indexOfLastResult = currentPage * resultsPerPage;
+ const indexOfFirstResult = indexOfLastResult - resultsPerPage;
+ const currentResults = searchResults.slice(indexOfFirstResult, indexOfLastResult);
+
return (
-
+
-
Search
+
+ Search
+
{isHelpModalOpen && (
-
+