diff --git a/package-lock.json b/package-lock.json index 502c97e..d21dc98 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,12 +13,15 @@ "@radix-ui/react-dialog": "^1.1.13", "@radix-ui/react-dropdown-menu": "^2.1.14", "@radix-ui/react-label": "^2.1.6", + "@radix-ui/react-radio-group": "^1.3.6", "@radix-ui/react-select": "^2.2.4", "@radix-ui/react-separator": "^1.1.6", "@radix-ui/react-slot": "^1.2.2", + "@radix-ui/react-switch": "^1.2.4", "@radix-ui/react-tabs": "^1.1.11", "@radix-ui/react-toast": "^1.2.13", "@tailwindcss/vite": "^4.1.6", + "@tanstack/react-table": "^8.21.3", "axios": "^1.9.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -27,6 +30,7 @@ "next-themes": "^0.4.6", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-popover": "^0.5.10", "react-router-dom": "^7.6.0", "sonner": "^2.0.3", "tailwind-merge": "^3.3.0" @@ -1506,6 +1510,38 @@ } } }, + "node_modules/@radix-ui/react-radio-group": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.3.6.tgz", + "integrity": "sha512-1tfTAqnYZNVwSpFhCT273nzK8qGBReeYnNTPspCggqk1fvIrfVxJekIuBFidNivzpdiMqDwVGnQvHqXrRPM4Og==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.2", + "@radix-ui/react-roving-focus": "1.1.9", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-roving-focus": { "version": "1.1.9", "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.9.tgz", @@ -1619,6 +1655,35 @@ } } }, + "node_modules/@radix-ui/react-switch": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.4.tgz", + "integrity": "sha512-yZCky6XZFnR7pcGonJkr9VyNRu46KcYAbyg1v/gVVCZUr8UJ4x+RpncC27hHtiZ15jC+3WS8Yg/JSgyIHnYYsQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.2", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-tabs": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.11.tgz", @@ -2341,6 +2406,39 @@ "vite": "^5.2.0 || ^6" } }, + "node_modules/@tanstack/react-table": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.21.3.tgz", + "integrity": "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==", + "license": "MIT", + "dependencies": { + "@tanstack/table-core": "8.21.3" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@tanstack/table-core": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.3.tgz", + "integrity": "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -3086,6 +3184,15 @@ "node": ">= 8" } }, + "node_modules/css-vendor": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/css-vendor/-/css-vendor-0.3.8.tgz", + "integrity": "sha512-Vx/Vl3zsHj32Z+WTNzGjd2iSbSIJTYHMmyGUT2nzCjj0Xk4qLfwpQ8nF6TQ5oo3Cf0s/An3DTc7LclH1BkAXbQ==", + "license": "MIT", + "dependencies": { + "is-in-browser": "^1.0.2" + } + }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", @@ -4057,6 +4164,12 @@ "node": ">=0.10.0" } }, + "node_modules/is-in-browser": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/is-in-browser/-/is-in-browser-1.1.3.tgz", + "integrity": "sha512-FeXIBgG/CPGd/WUxuEyvgGTEfwiG9Z4EKGxjNMRqviiIIfsmgrpnHLffEDdwUHqNva1VEW91o3xBT/m8Elgl9g==", + "license": "MIT" + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -4399,12 +4512,36 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash._getnative": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz", + "integrity": "sha512-RrL9VxMEPyDMHOd9uFbvMe8X55X16/cGM5IgOKgRElQZutpX89iS6vwl64duTV1/16w5JY7tuFNXqoekmh1EmA==", + "license": "MIT" + }, + "node_modules/lodash.debounce": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-3.1.1.tgz", + "integrity": "sha512-lcmJwMpdPAtChA4hfiwxTtgFeNAaow701wWUgVUqeD0XJF7vMXIN+bu/2FJSGxT0NUbZy9g9VFrlOFfPjl+0Ew==", + "license": "MIT", + "dependencies": { + "lodash._getnative": "^3.0.0" + } + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "node_modules/lodash.throttle": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-3.0.4.tgz", + "integrity": "sha512-dRU/xiF4W8a521NYnQosG5drDqv4+hp3ND6yWNJUMnwO1E87Q/A7oc9M/g6pk29K9U3j/ZWhM3BAQZyr/P6TTQ==", + "license": "MIT", + "dependencies": { + "lodash.debounce": "^3.0.0" + } + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -4638,7 +4775,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -4839,6 +4975,17 @@ "node": ">= 0.8.0" } }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -4948,6 +5095,39 @@ "react": "^18.3.1" } }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/react-popover": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/react-popover/-/react-popover-0.5.10.tgz", + "integrity": "sha512-5SYDTfncywSH00I70oHd4gFRUR8V0rJ4sRADSI/P6G0RVXp9jUgaWloJ0Bk+SFnjpLPuipTKuzQNNd2CTs5Hrw==", + "license": "MIT", + "dependencies": { + "css-vendor": "^0.3.1", + "debug": "^2.6.8", + "lodash.throttle": "^3.0.3", + "prop-types": "^15.5.10" + } + }, + "node_modules/react-popover/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/react-popover/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, "node_modules/react-refresh": { "version": "0.17.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", diff --git a/package.json b/package.json index 7e296b6..843f60e 100644 --- a/package.json +++ b/package.json @@ -15,12 +15,15 @@ "@radix-ui/react-dialog": "^1.1.13", "@radix-ui/react-dropdown-menu": "^2.1.14", "@radix-ui/react-label": "^2.1.6", + "@radix-ui/react-radio-group": "^1.3.6", "@radix-ui/react-select": "^2.2.4", "@radix-ui/react-separator": "^1.1.6", "@radix-ui/react-slot": "^1.2.2", + "@radix-ui/react-switch": "^1.2.4", "@radix-ui/react-tabs": "^1.1.11", "@radix-ui/react-toast": "^1.2.13", "@tailwindcss/vite": "^4.1.6", + "@tanstack/react-table": "^8.21.3", "axios": "^1.9.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -29,6 +32,7 @@ "next-themes": "^0.4.6", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-popover": "^0.5.10", "react-router-dom": "^7.6.0", "sonner": "^2.0.3", "tailwind-merge": "^3.3.0" diff --git a/src/App.tsx b/src/App.tsx index b910afb..c2cd4c2 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,7 +1,6 @@ import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; import MigrantProfilePage from "./pages/MigrantProfilePage"; import NotFoundPage from "./pages/NotFoundPage"; -import "./App.css"; import LoginPage from "./components/admin/LoginPage"; import Migrants from "./components/admin/Migrants"; import ProfileSettings from "./components/ui/ProfileSettings"; @@ -9,6 +8,10 @@ import AdminDashboardPage from "./components/admin/AdminDashboard"; import HomePage from "./pages/HomePage"; import RegisterPage from "./components/admin/Register"; import AddMigrantPage from "./components/admin/AddMigrant"; +import SettingsPage from "./components/admin/Setting"; +import ReportsPage from "./components/admin/Reports"; +import EditMigrant from "./components/admin/EditMigrant"; +import "./App.css"; function App() { return ( @@ -18,6 +21,9 @@ function App() { } /> } /> } /> + } /> + } /> + } /> } /> } /> } /> diff --git a/src/components/admin/AddMigrant.tsx b/src/components/admin/AddMigrant.tsx index e5f26db..e3216f8 100644 --- a/src/components/admin/AddMigrant.tsx +++ b/src/components/admin/AddMigrant.tsx @@ -1,66 +1,52 @@ -"use client" - -import { useState } from "react"; -import { ArrowLeft, Save } from "lucide-react"; +import { useState, useRef } from "react"; import { Link } from "react-router-dom"; +import { ArrowLeft } from "lucide-react"; import apiService from "@/services/apiService"; import { Button } from "@/components/ui/button"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; - import Header from "../layout/Header"; import Sidebar from "../layout/Sidebar"; -import { PersonalInfoTab } from "./migrant/PersonalInfoTab"; -import { MigrationDetailsTab } from "./migrant/MigrationDetailsTab"; -import { LocationsTab } from "./migrant/LocationsTab"; -import { InterneeDetailsTab } from "./migrant/InterneeDetailsTab"; -import { PhotosTab } from "./migrant/PhotosTab"; -import { NotesTab } from "./migrant/NotesTab"; +import MigrantForm from "@/components/admin/migrant/MigrationForm"; +import type { MigrantFormRef } from "@/components/admin/migrant/MigrationForm"; +import AddDialog from "@/components/admin/migrant/table/AddDialog"; -export default function AddUserPage() { - const [formData, setFormData] = useState({ - surname: "", - christian_name: "", - full_name: "", - date_of_birth: "", - date_of_death: "", - place_of_birth: "", - home_at_death: "", - occupation: "", - names_of_parents: "", - names_of_children: "", - data_source: "", - reference: "", - cav: "", - id_card_no: "", - date_of_arrival_australia: "", - date_of_arrival_nt: "", - date_of_naturalisation: "", - corps_issued: "", - no_of_cert: "", - issued_at: "", - }); - - const handleInputChange = (e: React.ChangeEvent) => { - const { id, value } = e.target; - setFormData(prev => ({ ...prev, [id]: value })); +export default function AddMigrant() { + const formRef = useRef(null); + const [showConfirmDialog, setShowConfirmDialog] = useState(false); + const [formData, setFormData] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); + + const handleCreate = async (data: any) => { + // Temporarily store the form data and show confirmation dialog + setFormData(data); + setShowConfirmDialog(true); }; - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); + + const handleConfirmCreate = async () => { + if (!formData) return; + setIsSubmitting(true); try { - const res = await apiService.createPerson(formData); + await apiService.createPerson(formData); alert("Migrant created successfully!"); - console.log(res); + setFormData(null); + setShowConfirmDialog(false); + + // Reset the form to its initial state + if (formRef.current) { + formRef.current.resetForm(); + } + // Optionally: clear form or redirect } catch (err) { alert("Failed to create migrant."); console.error(err); + } finally { + setIsSubmitting(false); } }; return ( -
+
-
+
@@ -74,53 +60,17 @@ export default function AddUserPage() {
-
- - - Personal Information - Migration Details - Locations - Internee Details - Photos & Documents - Additional Notes - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
-
+
+ + {/* Add Confirmation Dialog */} +
); } diff --git a/src/components/admin/AdminDashboard.tsx b/src/components/admin/AdminDashboard.tsx index 63919d7..b68ef90 100644 --- a/src/components/admin/AdminDashboard.tsx +++ b/src/components/admin/AdminDashboard.tsx @@ -4,13 +4,14 @@ import { Calendar, Clock, Database, - FileText, PlusCircle, Search, User, Users, + Flag, + AlertCircle, } from "lucide-react"; -import { Link } from "react-router-dom"; +import { Link, useNavigate } from "react-router-dom"; import { Button } from "@/components/ui/button"; import { @@ -32,17 +33,103 @@ import Header from "../layout/Header"; import Sidebar from "../layout/Sidebar"; import RecentActivityList from "../common/RecentActivity"; import StatCard from "../common/StatCard"; +import ApiService from "@/services/apiService"; +import type { DashboardStats } from "@/types/api"; +import { useEffect } from "react"; export default function DashboardPage() { + const navigate = useNavigate(); const [searchQuery, setSearchQuery] = useState(""); + const [results, setResults] = useState([]); // TODO: Replace any with proper type if available + const [stats, setStats] = useState(null); + const [loading, setLoading] = useState(true); + const [searchLoading, setSearchLoading] = useState(false); + const [error, setError] = useState(null); + useEffect(() => { + const delayDebounce = setTimeout(() => { + if (searchQuery.trim()) { + handleSearch(); + } else { + setResults([]); + } + }, 300); + + return () => clearTimeout(delayDebounce); + }, [searchQuery]); + + const handleSearch = async () => { + const trimmed = searchQuery.trim(); + if (!trimmed) { + setResults([]); + return; + } + + setSearchLoading(true); + try { + const data = await ApiService.searchPeople({ query: trimmed }); + setResults(data); + } catch (error) { + console.error("Search failed:", error); + setResults([]); + } finally { + setSearchLoading(false); + } + }; + + const handleSearchSubmit = (e: React.FormEvent) => { + e.preventDefault(); + handleSearch(); + }; + + useEffect(() => { + const fetchStats = async () => { + try { + setLoading(true); + const response = await ApiService.getDashboardStats(); + if (response.success) { + setStats(response.data); + } else { + setError('Failed to load dashboard data'); + } + } catch (err) { + setError('An error occurred while fetching dashboard data'); + console.error(err); + } finally { + setLoading(false); + } + }; + + fetchStats(); + }, []); + if (loading) { + return ( +
+ {[...Array(4)].map((_, index) => ( +
+
+
+
+
+ ))} +
+ ); + } + + if (error) { + return ( +
+ {error} +
+ ); + } return ( -
+
-
+
-
+

Welcome, Admin

@@ -51,40 +138,42 @@ export default function DashboardPage() {

+
} /> } /> } /> } + title="Incomplete Records" + value={stats?.incomplete_records || 0} + description="Need more information" + icon={} />
- - - Quick Search - Find migrant records quickly - - + + + Quick Search + Find migrant records quickly + + +
setSearchQuery(e.target.value)} />
+
- - -
- - +
+ + {/* Results */} +
+ {searchLoading ? ( +
+
+

Searching...

+
+ ) : results.length > 0 ? ( + results.map((person) => ( +
navigate(`/migrants/${person.person_id}`)} + className="cursor-pointer border rounded px-4 py-3 hover:bg-neutral-100 transition" + > +
{person.full_name}
+ {person.migration?.date_of_arrival_nt && ( +
+ Date of Arrival: {person.migration.date_of_arrival_nt} +
+ )} +
+ )) + ) : ( + searchQuery.trim() && ( +

No results found.

+ ) + )} +
+
+
@@ -169,4 +289,4 @@ export default function DashboardPage() {
); -} +} \ No newline at end of file diff --git a/src/components/admin/EditMigrant.tsx b/src/components/admin/EditMigrant.tsx new file mode 100644 index 0000000..bf00ad2 --- /dev/null +++ b/src/components/admin/EditMigrant.tsx @@ -0,0 +1,101 @@ +import { useEffect, useState, useRef } from "react"; +import { useParams } from "react-router-dom"; +import apiService from "@/services/apiService"; +import MigrantForm from "@/components/admin/migrant/MigrationForm"; +import Header from "../layout/Header"; +import Sidebar from "../layout/Sidebar"; +import type { Person } from "@/types/api"; +import { Link } from "react-router-dom"; +import { ArrowLeft } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import UpdateDialog from "./migrant/table/UpdateDialog"; + +export default function EditUserPage() { + const { id } = useParams(); + const [formData, setFormData] = useState(null); + const [open, setOpen] = useState(false); + const [isSubmitting, setIsSubmitting] = useState(false); + const formDataRef = useRef(null); + useEffect(() => { + if (!id) return; // Avoid calling API with undefined/null id + + const fetchData = async () => { + try { + const data = await apiService.getPersonById(id); + setFormData(data); + } catch (error) { + console.error("Failed to fetch migrant data:", error); + alert("Failed to load migrant data. Please try again."); + } + }; + + fetchData(); + }, [id]); + + const handleFormSubmit = async (data: any): Promise => { + // Store the form data in ref for later use + formDataRef.current = data; + // Open the confirmation dialog + setOpen(true); + + // Return a resolved promise to satisfy the form's onSubmit type + return Promise.resolve(); + }; + const handleUpdate = async () => { + if (!formDataRef.current || !id) return; + + try { + setIsSubmitting(true); + + // Prepare the form data, ensuring it's properly structured + const formattedData = { ...formDataRef.current }; + + // Send the update request + await apiService.updatePerson(id, formattedData); + + setOpen(false); + alert("Migrant updated successfully!"); + } catch (err: any) { + // More descriptive error message + const errorMsg = err.response?.data?.message || "An unexpected error occurred"; + alert(`Failed to update migrant: ${errorMsg}`); + console.error("Update error:", err); + } finally { + setIsSubmitting(false); + } + }; + + if (!formData) return

Loading...

; + + return ( +
+ +
+
+
+
+ + + +

+ Edit Migrant +

+
+ + +
+
+
+ ); +} diff --git a/src/components/admin/LoginPage.tsx b/src/components/admin/LoginPage.tsx index 055406b..072b9b9 100644 --- a/src/components/admin/LoginPage.tsx +++ b/src/components/admin/LoginPage.tsx @@ -3,10 +3,12 @@ import type React from "react" import { useState } from "react" -import { motion } from "framer-motion" import { useNavigate } from "react-router-dom" import { Eye, EyeOff, Lock, Mail } from "lucide-react" -import { Link } from "react-router-dom"; +import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card" +import { Label } from "@/components/ui/label" +import { Input } from "@/components/ui/input" +import { Button } from "@/components/ui/button" import apiService from "@/services/apiService" export default function LoginPage() { @@ -42,184 +44,78 @@ export default function LoginPage() { setIsLoading(false) } } - return ( -
-
- -
-
+
+
+
+ + {/* Italian flag stripe at the top */} +
+
+
+
+
- -
- -
- -

Admin Access

-

Northern Territory Italian Migration History

-
-
- - - {error && ( - - {error} - - )} - -
-
- -
-
- -
- setEmail(e.target.value)} - className="block w-full pl-10 pr-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-[#01796F] focus:border-[#01796F]" - placeholder="admin@example.com" - required - /> + +
+
+ NT
- -
- -
-
- + Italian Migrants Database + Enter your credentials to access the admin panel + + + +
+ +
+ + setEmail(e.target.value)} + required + />
- setPassword(e.target.value)} - className="block w-full pl-10 pr-10 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-[#01796F] focus:border-[#01796F]" - placeholder="••••••••" - required - /> -
+
+
+ +
+ + setPassword(e.target.value)} + required + />
-
- -
-
- - -
- -
- - Forgot password? - -
-
- - - {isLoading ? ( -
- - - - - Signing in... -
- ) : ( - "Sign in" - )} -
+
+ + + - -
-
- or -
-
- -
- - Return to public site - -
- - - - - © {new Date().getFullYear()} Northern Territory Italian Migration History Project - + +
) } diff --git a/src/components/admin/Migrants.tsx b/src/components/admin/Migrants.tsx index d4ef695..45034ff 100644 --- a/src/components/admin/Migrants.tsx +++ b/src/components/admin/Migrants.tsx @@ -1,243 +1,103 @@ "use client" -import { useState } from "react" -import { ArrowUpDown, Download, Filter, MoreHorizontal, PlusCircle, Search, Trash2, Upload } from "lucide-react" -import {Link} from "react-router-dom" +import { useEffect, useState } from "react" +import { Link } from "react-router-dom" +import { PlusCircle, Filter, Upload, Download } from "lucide-react" +import Header from "@/components/layout/Header" +import Sidebar from "@/components/layout/Sidebar" import { Button } from "@/components/ui/button" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" -import { Checkbox } from "@/components/ui/checkbox" -import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu" import { Input } from "@/components/ui/input" -import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" - -import Header from "@/components/layout/Header" -import Sidebar from "@/components/layout/Sidebar" - -// Sample data for migrants -const migrants = [ - { - id: 1, - name: "Marco Rossi", - birthDate: "1935-05-12", - birthPlace: "Rome, Italy", - arrivalDate: "1952-08-23", - occupation: "Carpenter", - hasPhotos: true, - }, - { - id: 2, - name: "Sofia Bianchi", - birthDate: "1942-11-03", - birthPlace: "Naples, Italy", - arrivalDate: "1960-02-15", - occupation: "Seamstress", - hasPhotos: true, - }, - { - id: 3, - name: "Antonio Esposito", - birthDate: "1928-07-22", - birthPlace: "Milan, Italy", - arrivalDate: "1950-10-05", - occupation: "Farmer", - hasPhotos: false, - }, - { - id: 4, - name: "Lucia Romano", - birthDate: "1940-03-18", - birthPlace: "Florence, Italy", - arrivalDate: "1958-06-30", - occupation: "Teacher", - hasPhotos: true, - }, - { - id: 5, - name: "Giuseppe Colombo", - birthDate: "1932-09-08", - birthPlace: "Venice, Italy", - arrivalDate: "1955-12-10", - occupation: "Fisherman", - hasPhotos: false, - }, -] +import MigrantTable from "@/components/admin/migrant/table/MigrantTable" +import apiService from "@/services/apiService" +import type { Person, Pagination } from "@/types/api" export default function MigrantsPage() { - const [searchQuery, setSearchQuery] = useState("") - const [selectedMigrants, setSelectedMigrants] = useState([]) + const [migrants, setMigrants] = useState([]) + const [filter, setFilter] = useState("") + const [loading, setLoading] = useState(false) + const [pagination, setPagination] = useState({ + current_page: 1, + per_page: 10, + total: 0, + next_page_url: null, + prev_page_url: null, + }) - const toggleSelectAll = () => { - if (selectedMigrants.length === migrants.length) { - setSelectedMigrants([]) - } else { - setSelectedMigrants(migrants.map((m) => m.id)) + const fetchMigrants = async (url?: string) => { + setLoading(true) + try { + const res = url ? await apiService.getMigrantsByUrl(url) : await apiService.getMigrants(pagination.current_page) + const { data, current_page, per_page, total, next_page_url, prev_page_url } = res.data + setMigrants(data) + setPagination({ current_page, per_page, total, next_page_url, prev_page_url }) + } catch (err) { + console.error("Error fetching migrants:", err) + } finally { + setLoading(false) } } - const toggleSelectMigrant = (id: number) => { - if (selectedMigrants.includes(id)) { - setSelectedMigrants(selectedMigrants.filter((m) => m !== id)) - } else { - setSelectedMigrants([...selectedMigrants, id]) - } - } + useEffect(() => { + fetchMigrants() + }, []) - - - const filteredMigrants = migrants.filter( - (migrant) => - migrant.name.toLowerCase().includes(searchQuery.toLowerCase()) || - migrant.birthPlace.toLowerCase().includes(searchQuery.toLowerCase()) || - migrant.occupation.toLowerCase().includes(searchQuery.toLowerCase()), - ) + const handlePageChange = (url?: string) => url && fetchMigrants(url) return ( -
+
-
+
-
+

Migrants Database

-
- - - Search & Filter - -
-
- - setSearchQuery(e.target.value)} - /> -
+
+ setFilter(e.target.value)} + />
- -
-
- {selectedMigrants.length > 0 && ( - - )} -
- - -
- - - - - 0} - onCheckedChange={toggleSelectAll} - aria-label="Select all" - /> - - -
- Name - -
-
- -
- Birth Date - -
-
- Birth Place - -
- Arrival Date - -
-
- Occupation - Photos - Actions -
-
- - {filteredMigrants.length === 0 ? ( - - - No migrants found matching your search criteria. - - - ) : ( - filteredMigrants.map((migrant) => ( - - - toggleSelectMigrant(migrant.id)} - aria-label={`Select ${migrant.name}`} - /> - - {migrant.name} - {new Date(migrant.birthDate).toLocaleDateString()} - {migrant.birthPlace} - {new Date(migrant.arrivalDate).toLocaleDateString()} - {migrant.occupation} - - {migrant.hasPhotos ? ( - - Yes - - ) : ( - - No - - )} - - - - - - - - - Edit - - Delete - - - - - )) - )} - -
-
-
-
+ handlePageChange(pagination.next_page_url!)} + onPrevPage={() => handlePageChange(pagination.prev_page_url!)} + onRefresh={() => fetchMigrants()} + />
diff --git a/src/components/admin/Reports.tsx b/src/components/admin/Reports.tsx new file mode 100644 index 0000000..972d800 --- /dev/null +++ b/src/components/admin/Reports.tsx @@ -0,0 +1,442 @@ +"use client" + +import { useState } from "react" +import { + BarChart, + Download, + FileText, + PieChart, + RefreshCw, + User, +} from "lucide-react" + +import { Button } from "@/components/ui/button" +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" +import { Input } from "@/components/ui/input" +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" + +import Header from "@/components/layout/Header" +import Sidebar from "@/components/layout/Sidebar" + +export default function ReportsPage() { + const [dateRange, setDateRange] = useState<"all" | "year" | "decade" | "custom">("all") + const [reportType, setReportType] = useState<"demographics" | "migration" | "occupation">("demographics") + const [loading, setLoading] = useState(false) + + const handleGenerateReport = () => { + setLoading(true) + + + } + + const handleExportReport = () => { + + } + + return ( +
+ +
+
+
+
+

Data Reports

+

Generate and analyze reports from the Italian Migrants Database

+
+ + {/* Report Controls */} + +
+ + Report Generator + Configure and generate custom reports + + +
+
+ + +
+ +
+ + +
+ + {dateRange === "custom" && ( +
+
+ + +
+
+ + +
+
+ )} + +
+ +
+
+ + {dateRange === "decade" && ( +
+
+ {["1940s", "1950s", "1960s", "1970s", "1980s", "1990s"].map((decade) => ( + + ))} +
+
+ )} +
+
+ + {/* Report Tabs */} + +
+ + + Demographics + + + Occupation + + + +
+ + +
+
+ + {/* Demographics Report */} + +
+ +
+ + Age Distribution + Age breakdown of Italian migrants + + +
+
+ {[28, 45, 65, 42, 18, 10].map((height, i) => ( +
+
+ {["0-18", "19-30", "31-45", "46-60", "61-75", "76+"][i]}: {height}% +
+
+ + {["0-18", "19-30", "31-45", "46-60", "61-75", "76+"][i]} + +
+ ))} +
+
+
+
+ + +
+ + Gender Distribution + Gender breakdown of Italian migrants + + +
+
+ {/* Simple pie chart visualization */} +
+
+
+
+
+
+ +
+
+
+
+
+ Male (62%) +
+
+
+ Female (38%) +
+
+
+
+
+
+
+ + +
+ + Family Status + Family composition of Italian migrants + + +
+
+
+ +
+

42%

+

Single

+
+ +
+
+
+ + +
+
+

35%

+

Married

+
+ +
+
+
+ + + +
+
+

23%

+

Family

+
+
+
+
+
+ + + + {/* Occupation Report */} + +
+ +
+ + Top Occupations + Most common professions among Italian migrants + + +
+
+ {[ + { name: "Farmer", count: 287, percent: 23 }, + { name: "Carpenter", count: 215, percent: 17 }, + { name: "Seamstress", count: 189, percent: 15 }, + { name: "Mason", count: 156, percent: 12 }, + { name: "Cook", count: 124, percent: 10 }, + { name: "Fisherman", count: 98, percent: 8 }, + { name: "Merchant", count: 87, percent: 7 }, + { name: "Other", count: 102, percent: 8 }, + ].map((occupation, i) => ( +
+ {occupation.name} +
+
+
+ {occupation.count} + ({occupation.percent}%) +
+ ))} +
+
+
+
+ + +
+ + Occupation by Gender + Distribution of occupations by gender + + +
+
+ {[ + { name: "Farmer", male: 85, female: 15 }, + { name: "Carpenter", male: 98, female: 2 }, + { name: "Seamstress", male: 5, female: 95 }, + { name: "Mason", male: 92, female: 8 }, + { name: "Cook", male: 45, female: 55 }, + ].map((occupation, i) => ( +
+
+
+
+
+ + {occupation.name} + +
+ ))} +
+
+
+
+ Male +
+
+
+ Female +
+
+
+
+
+
+ + +
+ + Occupation Trends + Changes in occupations over time + + +
+
+
+
+ {/* Bar chart visualization */} +
+ {[1940, 1950, 1960, 1970, 1980, 1990].map((decade, i) => ( +
+
+
+
+
+ ))} +
+ +
+
+
+ {[1940, 1950, 1960, 1970, 1980, 1990].map((decade) => ( +
+ {decade}s +
+ ))} +
+
+
+
+
+
+ Agricultural +
+
+
+ Trade/Craft +
+
+
+ Service +
+
+
+
+
+
+
+
+
+ ) +} diff --git a/src/components/admin/Setting.tsx b/src/components/admin/Setting.tsx new file mode 100644 index 0000000..4d549cc --- /dev/null +++ b/src/components/admin/Setting.tsx @@ -0,0 +1,219 @@ +"use client" + +import { useState } from "react" +import { Key, Lock } from "lucide-react" + +import { Button } from "@/components/ui/button" +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { Switch } from "@/components/ui/switch" +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" +import { Textarea } from "@/components/ui/textarea" + +import Header from "@/components/layout/Header" +import Sidebar from "@/components/layout/Sidebar" + +export default function SettingsPage() { + const [loading, setLoading] = useState(false) + + const handleSaveSettings = () => { + setLoading(true) + setTimeout(() => { + setLoading(false) + }, 1000) + } + + return ( +
+ +
+
+
+
+

User Settings

+

Manage your account preferences and settings

+
+ + + + + Profile + + + Account + + + + {/* Profile Settings */} + + +
+ + Personal Information + Update your personal details + + +
+
+
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ +
+
+
+ A +
+
+ + +
+
+ +
+ +