-
-
-
-
+
Italian Migrants Database
+
Enter your credentials to access the admin panel
+
+
-
-
-
-
-
- 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
-
+
+
+
+ Search & Filter
+
Advanced Filters
-
-
-
- setSearchQuery(e.target.value)}
- />
-
+
+
setFilter(e.target.value)}
+ />
-
+
Import
-
+
Export
-
- {selectedMigrants.length > 0 && (
-
- Delete Selected ({selectedMigrants.length})
-
- )}
-
-
-
-
-
-
-
-
- 0}
- onCheckedChange={toggleSelectAll}
- aria-label="Select all"
- />
-
-
-
-
-
-
-
- Birth Place
-
-
-
- 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
-
- )}
-
-
-
-
-
-
- Open menu
-
-
-
-
- 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" && (
+
+ )}
+
+
+
+ {loading ? (
+ <>
+
+ Generating...
+ >
+ ) : (
+ <>
+
+ Generate Report
+ >
+ )}
+
+
+
+
+ {dateRange === "decade" && (
+
+
+ {["1940s", "1950s", "1960s", "1970s", "1980s", "1990s"].map((decade) => (
+
+ {decade}
+
+ ))}
+
+
+ )}
+
+
+
+ {/* Report Tabs */}
+
+
+
+
+ Demographics
+
+
+ Occupation
+
+
+
+
+ handleExportReport()}
+ >
+ PDF
+
+ handleExportReport()}
+ >
+ CSV
+
+
+
+
+ {/* 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 */}
+
+
+
+
+
+
+
+
+
+
+
+
+ Family Status
+ Family composition of Italian migrants
+
+
+
+
+
+
+
+
+
+ {/* 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}
+
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+ 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
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
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
+
+
+
+
+
+
+
+
+ Upload New Photo
+
+
+ Remove Photo
+
+
+
+
+
+
+
+
+
+
+
+ {loading ? "Saving..." : "Save Profile"}
+
+
+
+
+
+
+ {/* Account Settings */}
+
+
+
+
+ Account Security
+ Manage your password and security settings
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {loading ? "Updating..." : "Update Password"}
+
+
+
+
+
+
Two-Factor Authentication
+
+
+
Protect your account with 2FA
+
Add an extra layer of security to your account
+
+
+
+
+
+
+
Login Sessions
+
+
+
+
+
Current Session
+
Windows 11 • Chrome • Sydney, Australia
+
Started 2 hours ago
+
+
+ Active Now
+
+
+
+
+
+
+ Sign Out All Other Sessions
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/src/components/admin/migrant/MigrationForm.tsx b/src/components/admin/migrant/MigrationForm.tsx
new file mode 100644
index 0000000..b5b1312
--- /dev/null
+++ b/src/components/admin/migrant/MigrationForm.tsx
@@ -0,0 +1,138 @@
+import { useState, forwardRef, useImperativeHandle } from "react";
+import { Save } from "lucide-react";
+import { Button } from "@/components/ui/button";
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
+
+import { PersonalInfoTab } from "./PersonalInfoTab";
+import { MigrationDetailsTab } from "./MigrationDetailsTab";
+import { LocationsTab } from "./LocationsTab";
+import { InterneeDetailsTab } from "./InterneeDetailsTab";
+import { PhotosTab } from "./PhotosTab";
+import { NotesTab } from "./NotesTab";
+
+type FormDataType = {
+ surname: string;
+ christian_name: string;
+ full_name: string;
+ date_of_birth: string;
+ date_of_death: string;
+ place_of_birth: string;
+ home_at_death: string;
+ occupation: string;
+ names_of_parents: string;
+ names_of_children: string;
+ data_source: string;
+ reference: string;
+ cav: string;
+ id_card_no: string;
+ date_of_arrival_australia: string;
+ date_of_arrival_nt: string;
+ date_of_naturalisation: string;
+ corps_issued: string;
+ no_of_cert: string;
+ issued_at: string;
+};
+
+type MigrantFormProps = {
+ initialData?: Partial;
+ mode?: "add" | "edit";
+ onSubmit: (formData: FormDataType) => Promise;
+};
+
+export type MigrantFormRef = {
+ resetForm: () => void;
+};
+
+const MigrantForm = forwardRef(function MigrantForm(
+ {
+ initialData = {},
+ mode = "add",
+ onSubmit,
+ }: MigrantFormProps,
+ ref
+) {
+ const defaultFormData: FormDataType = {
+ 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: "",
+ ...initialData,
+ };
+
+ const [formData, setFormData] = useState(defaultFormData);
+
+ useImperativeHandle(ref, () => ({
+ resetForm: () => setFormData(defaultFormData)
+ }));
+
+
+ const handleInputChange = (e: React.ChangeEvent) => {
+ const { id, value } = e.target;
+ setFormData((prev) => ({ ...prev, [id]: value }));
+ };
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ await onSubmit(formData);
+ };
+
+ return (
+
+ );
+});
+
+export default MigrantForm;
diff --git a/src/components/admin/migrant/table/AddDialog.tsx b/src/components/admin/migrant/table/AddDialog.tsx
new file mode 100644
index 0000000..d12d506
--- /dev/null
+++ b/src/components/admin/migrant/table/AddDialog.tsx
@@ -0,0 +1,58 @@
+import { Check,Loader2 } from "lucide-react"
+import { Button } from "@/components/ui/button"
+
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog"
+
+interface AddDialogProps {
+ open: boolean
+ onOpenChange: (open: boolean) => void
+ onConfirm: () => void
+ isSubmitting: boolean
+}
+
+export default function AddDialog({
+ open,
+ onOpenChange,
+ onConfirm,
+ isSubmitting,
+}: AddDialogProps) {
+ return (
+
+
+ )
+}
diff --git a/src/components/admin/migrant/table/DeleteDialog.tsx b/src/components/admin/migrant/table/DeleteDialog.tsx
new file mode 100644
index 0000000..c7ce176
--- /dev/null
+++ b/src/components/admin/migrant/table/DeleteDialog.tsx
@@ -0,0 +1,103 @@
+import { Trash2 } from "lucide-react"
+import { Button } from "@/components/ui/button"
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog"
+import { useState } from "react"
+import apiService from "@/services/apiService"
+import { toast } from "sonner"
+
+interface DeleteDialogProps {
+ open: boolean
+ onOpenChange: (open: boolean) => void
+ bulkDelete: boolean
+ selectedCount: number
+ ids: string[]
+ onDeleteSuccess?: () => void
+}
+
+export default function DeleteDialog({
+ open,
+ onOpenChange,
+ bulkDelete,
+ selectedCount,
+ ids,
+ onDeleteSuccess,
+}: DeleteDialogProps) {
+ const [loading, setLoading] = useState(false)
+
+ const handleDelete = async () => {
+ // Validate that we have IDs to delete
+ if (!ids.length) {
+ toast.error("No records to delete", {
+ description: "Could not find valid IDs for deletion."
+ })
+ onOpenChange(false)
+ return
+ }
+
+ setLoading(true)
+ try {
+ // Use Promise.all if bulk deleting
+ if (bulkDelete) {
+ await Promise.all(ids.map(id => apiService.deletePerson(id)))
+ toast.success(`${ids.length} records deleted`, {
+ description: "The selected migrant records have been removed."
+ })
+ } else {
+ await apiService.deletePerson(ids[0])
+ toast.success("Record deleted", {
+ description: "The migrant record has been successfully removed."
+ })
+ }
+
+ // Notify parent component about successful deletion
+ onDeleteSuccess?.()
+ onOpenChange(false)
+ } catch (error: any) {
+ console.error("Failed to delete record(s):", error)
+ toast.error("Delete operation failed", {
+ description: error.response?.data?.message || "An unexpected error occurred during deletion."
+ })
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ return (
+
+ )
+}
diff --git a/src/components/admin/migrant/table/LogoutDialog.tsx b/src/components/admin/migrant/table/LogoutDialog.tsx
new file mode 100644
index 0000000..f75d0b7
--- /dev/null
+++ b/src/components/admin/migrant/table/LogoutDialog.tsx
@@ -0,0 +1,59 @@
+
+import { LogOut } from "lucide-react"
+import { Button } from "@/components/ui/button"
+
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog"
+
+interface LogoutDialogProps {
+ open: boolean
+ onOpenChange: (open: boolean) => void
+ onConfirm: () => void
+ isSubmitting: boolean
+}
+
+export default function LogoutDialog({
+ open,
+ onOpenChange,
+ onConfirm,
+ isSubmitting,
+}: LogoutDialogProps) {
+ return (
+
+
+ )
+}
diff --git a/src/components/admin/migrant/table/MigrantTable.tsx b/src/components/admin/migrant/table/MigrantTable.tsx
new file mode 100644
index 0000000..da5d62d
--- /dev/null
+++ b/src/components/admin/migrant/table/MigrantTable.tsx
@@ -0,0 +1,164 @@
+"use client"
+
+import { useState } from "react"
+import {
+ useReactTable,
+ getCoreRowModel,
+ getFilteredRowModel,
+ getSortedRowModel,
+ type SortingState,
+ type VisibilityState,
+} from "@tanstack/react-table"
+
+import { Card, CardContent } from "@/components/ui/card"
+import { Button } from "@/components/ui/button"
+import { ChevronLeft, ChevronRight, Trash2 } from "lucide-react"
+
+import { type Person, type PaginationMeta } from "@/types/api"
+import TableView from "./TableView"
+import useColumns from "./useColumnHooks"
+import DeleteDialog from "./DeleteDialog"
+
+interface MigrantsTableProps {
+ data: Person[]
+ globalFilter: string
+ loading: boolean
+ page?: number
+ meta: PaginationMeta
+ onNextPage: () => void
+ onPrevPage: () => void
+ onRefresh?: () => void
+}
+
+export default function MigrantTable({
+ data,
+ globalFilter,
+ loading,
+ meta,
+ onNextPage,
+ onPrevPage,
+ onRefresh,
+}: MigrantsTableProps) {
+ const [sorting, setSorting] = useState([])
+ const [columnVisibility, setColumnVisibility] = useState({})
+ const [rowSelection, setRowSelection] = useState>({})
+ const [deleteDialogOpen, setDeleteDialogOpen] = useState(false)
+ const [migrantToDelete, setMigrantToDelete] = useState(null)
+ const [bulkDelete, setBulkDelete] = useState(false)
+
+ const columns = useColumns({
+ onDeleteMigrant: (migrant) => {
+ setMigrantToDelete(migrant)
+ setBulkDelete(false)
+ setDeleteDialogOpen(true)
+ },
+ })
+
+ const table = useReactTable({
+ data,
+ columns,
+ state: {
+ sorting,
+ columnVisibility,
+ rowSelection,
+ globalFilter,
+ },
+ onSortingChange: setSorting,
+ onColumnVisibilityChange: setColumnVisibility,
+ onRowSelectionChange: setRowSelection,
+ getCoreRowModel: getCoreRowModel(),
+ getSortedRowModel: getSortedRowModel(),
+ getFilteredRowModel: getFilteredRowModel(),
+ })
+
+ const selectedRows = table.getFilteredSelectedRowModel().rows
+
+ const handleBulkDeleteClick = () => {
+ setBulkDelete(true)
+ setDeleteDialogOpen(true)
+ }
+
+ // This function is called when deletion is successful from the DeleteDialog
+ const handleDeleteMigrant = () => {
+ // Reset table selection if it was a bulk delete
+ if (bulkDelete) {
+ table.resetRowSelection()
+ }
+
+ // Reset the state
+ setDeleteDialogOpen(false)
+ setMigrantToDelete(null)
+ setBulkDelete(false)
+
+ // Refresh the data after deletion
+ if (onRefresh) {
+ onRefresh()
+ }
+ }
+
+ const pageRangeStart = (meta.current_page - 1) * meta.per_page + 1
+ const pageRangeEnd = Math.min(meta.current_page * meta.per_page, meta.total)
+
+ return (
+
+
+
+
+ {selectedRows.length > 0 && (
+
+
+ Delete Selected ({selectedRows.length})
+
+ )}
+
+
+
+
+
+
+ {data.length === 0
+ ? "No migrants to display"
+ : `Showing ${pageRangeStart} to ${pageRangeEnd} of ${meta.total} migrants`}
+
+
+
+
+ Previous
+
+ = meta.last_page}
+ size="sm"
+ variant="outline"
+ >
+ Next
+
+
+
+
+
+
+ row.original.person_id || '').filter(id => id !== '') as string[]
+ : migrantToDelete && migrantToDelete.person_id ? [migrantToDelete.person_id]
+ : []}
+ onDeleteSuccess={handleDeleteMigrant}
+ />
+
+ )
+}
diff --git a/src/components/admin/migrant/table/TableView.tsx b/src/components/admin/migrant/table/TableView.tsx
new file mode 100644
index 0000000..239722f
--- /dev/null
+++ b/src/components/admin/migrant/table/TableView.tsx
@@ -0,0 +1,70 @@
+import { type Table, flexRender } from "@tanstack/react-table"
+import type { ColumnDef } from "@tanstack/react-table"
+
+import {
+ Table as UITable,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/components/ui/table"
+
+import type { Person } from "@/types/api"
+
+interface TableViewProps {
+ table: Table
+ columns: ColumnDef[]
+ loading: boolean
+}
+
+export default function TableView({ table, columns, loading }: TableViewProps) {
+ return (
+
+
+
+ {table.getHeaderGroups().map((headerGroup) => (
+
+ {headerGroup.headers.map((header) => (
+
+ {header.isPlaceholder
+ ? null
+ : flexRender(header.column.columnDef.header, header.getContext())}
+
+ ))}
+
+ ))}
+
+
+ {loading ? (
+
+
+ Loading migrants data...
+
+
+ ) : table.getRowModel().rows.length ? (
+ table.getRowModel().rows.map((row) => (
+
+ {row.getVisibleCells().map((cell) => (
+
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
+
+ ))}
+
+ ))
+ ) : (
+
+
+ No migrants found matching your search criteria.
+
+
+ )}
+
+
+
+ )
+}
diff --git a/src/components/admin/migrant/table/UpdateDialog.tsx b/src/components/admin/migrant/table/UpdateDialog.tsx
new file mode 100644
index 0000000..cce9b9a
--- /dev/null
+++ b/src/components/admin/migrant/table/UpdateDialog.tsx
@@ -0,0 +1,57 @@
+import { Loader2, Save } from "lucide-react"
+import { Button } from "@/components/ui/button"
+
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog"
+
+interface AddDialogProps {
+ open: boolean
+ onOpenChange: (open: boolean) => void
+ onConfirm: () => void
+ isSubmitting: boolean
+}
+
+export default function UpdateDialog({
+ open,
+ onOpenChange,
+ onConfirm,
+ isSubmitting,
+}: AddDialogProps) {
+ return (
+
+ )
+}
diff --git a/src/components/admin/migrant/table/useColumnHooks.tsx b/src/components/admin/migrant/table/useColumnHooks.tsx
new file mode 100644
index 0000000..1ca5ef0
--- /dev/null
+++ b/src/components/admin/migrant/table/useColumnHooks.tsx
@@ -0,0 +1,178 @@
+import { useMemo } from "react"
+import { type ColumnDef } from "@tanstack/react-table"
+import { ArrowDown, ArrowUp, ArrowUpDown, MoreHorizontal } from "lucide-react"
+import { Link } from "react-router-dom"
+
+import { Button } from "@/components/ui/button"
+import { Checkbox } from "@/components/ui/checkbox"
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu"
+
+import type { Person } from "@/types/api"
+
+interface UseColumnsProps {
+ onDeleteMigrant: (migrant: Person) => void
+}
+
+export default function useColumns({ onDeleteMigrant }: UseColumnsProps) {
+ const columns = useMemo[]>(
+ () => [
+ {
+ id: "select",
+ header: ({ table }) => (
+ table.toggleAllPageRowsSelected(!!value)}
+ aria-label="Select all"
+ />
+ ),
+ cell: ({ row }) => (
+ row.toggleSelected(!!value)}
+ aria-label="Select row"
+ />
+ ),
+ enableSorting: false,
+ enableHiding: false,
+ },
+ {
+ accessorKey: "full_name",
+ header: ({ column }) => (
+ column.toggleSorting(column.getIsSorted() === "asc")}
+ className="p-0 hover:bg-transparent flex items-center"
+ >
+ Name
+ {column.getIsSorted() === "asc" ? (
+
+ ) : column.getIsSorted() === "desc" ? (
+
+ ) : (
+
+ )}
+
+ ),
+ cell: ({ row }) => {row.getValue("full_name")}
,
+ },
+ {
+ accessorKey: "date_of_birth",
+ header: ({ column }) => (
+ column.toggleSorting(column.getIsSorted() === "asc")}
+ className="p-0 hover:bg-transparent flex items-center"
+ >
+ Birth Date
+ {column.getIsSorted() === "asc" ? (
+
+ ) : column.getIsSorted() === "desc" ? (
+
+ ) : (
+
+ )}
+
+ ),
+ cell: ({ row }) => {
+ const date = row.getValue("date_of_birth") as string
+ return {date ? new Date(date).toLocaleDateString() : "-"}
+ },
+ },
+ {
+ accessorKey: "place_of_birth",
+ header: "Birth Place",
+ cell: ({ row }) => {row.getValue("place_of_birth") || "-"}
,
+ },
+ {
+ id: "date_of_arrival_aus",
+ header: ({ column }) => (
+ column.toggleSorting(column.getIsSorted() === "asc")}
+ className="p-0 hover:bg-transparent flex items-center"
+ >
+ Arrival Date
+ {column.getIsSorted() === "asc" ? (
+
+ ) : column.getIsSorted() === "desc" ? (
+
+ ) : (
+
+ )}
+
+ ),
+ accessorFn: (row) => row.migration?.date_of_arrival_aus,
+ cell: ({ row }) =>
+ row.original.migration?.date_of_arrival_aus
+ ? new Date(row.original.migration.date_of_arrival_aus).toLocaleDateString()
+ : "-",
+ },
+ {
+ accessorKey: "occupation",
+ header: "Occupation",
+ cell: ({ row }) => {row.getValue("occupation") || "-"}
,
+ },
+ {
+ id: "has_photos",
+ header: "Photos",
+ accessorFn: (row) => row.has_photos,
+ cell: ({ row }) => {
+ const hasPhotos = row.original.has_photos
+ return hasPhotos ? (
+
+ Yes
+
+ ) : (
+
+ No
+
+ )
+ },
+ },
+ {
+ id: "actions",
+ cell: ({ row }) => {
+ const migrant = row.original
+
+ return (
+
+
+
+
+ Open menu
+
+
+
+
+
+ Edit
+
+
+ {
+ e.preventDefault()
+ onDeleteMigrant(migrant)
+ }}
+ className="text-red-600"
+ >
+ Delete
+
+
+
+ )
+ },
+ },
+ ],
+ [onDeleteMigrant]
+ )
+
+ return columns
+}
diff --git a/src/components/common/RecentActivity.tsx b/src/components/common/RecentActivity.tsx
index e3572ae..a051794 100644
--- a/src/components/common/RecentActivity.tsx
+++ b/src/components/common/RecentActivity.tsx
@@ -1,68 +1,111 @@
-import { motion } from "framer-motion"
-import { PlusCircle, FileText, Users, BarChart2, Database } from "lucide-react"
+"use client"
+
+import { useEffect, useState } from 'react';
+import ApiService from '@/services/apiService';
+import type { ActivityLog } from '@/types/api';
+import { motion } from 'framer-motion';
+import { PlusCircle, FileText, Users, BarChart2, Database } from 'lucide-react';
export default function RecentActivity() {
+ const [logs, setLogs] = useState([]);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ const fetchLogs = async () => {
+ try {
+ const data = await ApiService.getRecentActivityLogs();
+ setLogs(data);
+ } catch (err) {
+ console.error('Error fetching activity logs:', err);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ fetchLogs();
+ }, []);
+
+ const getType = (log: ActivityLog) => {
+ const action = log.description.toLowerCase()
+ if (action.includes("add")) return "add"
+ if (action.includes("update")) return "update"
+ if (action.includes("delete")) return "delete"
+ if (action.includes("report")) return "report"
+ return "import"
+ }
+
+ const getIcon = (type: string) => {
+ switch (type) {
+ case "add":
+ return
+ case "update":
+ return
+ case "delete":
+ return
+ case "report":
+ return
+ default:
+ return
+ }
+ }
+
+ const getColorClass = (type: string) => {
+ switch (type) {
+ case "add":
+ return "bg-green-100 text-green-600"
+ case "update":
+ return "bg-blue-100 text-blue-600"
+ case "delete":
+ return "bg-red-100 text-red-600"
+ case "report":
+ return "bg-purple-100 text-purple-600"
+ default:
+ return "bg-yellow-100 text-yellow-600"
+ }
+ }
+
return (
-
-
Recent Activity
-
-
-
- {[
- { action: "Added new migrant", user: "Admin", time: "10 minutes ago", type: "add" },
- { action: "Updated record #1248", user: "Admin", time: "2 hours ago", type: "update" },
- { action: "Generated monthly report", user: "System", time: "Yesterday", type: "report" },
- { action: "Deleted duplicate record", user: "Admin", time: "2 days ago", type: "delete" },
- { action: "Imported 15 new records", user: "Admin", time: "1 week ago", type: "import" },
- ].map((activity, index) => (
-
-
- {activity.type === "add" ? (
-
- ) : activity.type === "update" ? (
-
- ) : activity.type === "delete" ? (
-
- ) : activity.type === "report" ? (
-
- ) : (
-
- )}
-
-
-
{activity.action}
-
- By {activity.user} • {activity.time}
-
-
-
- ))}
+ className="bg-white rounded-lg shadow"
+ initial={{ opacity: 0, y: 20 }}
+ animate={{ opacity: 1, y: 0 }}
+ transition={{ duration: 0.5, delay: 0.4 }}
+ >
+
+
Recent Activity
-
-
+
+ {loading ? (
+
Loading...
+ ) : logs.length === 0 ? (
+
No recent activity found.
+ ) : (
+
+ {logs.map((log, index) => {
+ const type = getType(log)
+ return (
+
+
+ {getIcon(type)}
+
+
+
{log.description}
+
+ By {log.causer_name} • {new Date(log.created_at).toLocaleString()}
+
+
+
+ )
+ })}
+
+ )}
+
+
)
-}
\ No newline at end of file
+}
diff --git a/src/components/common/StatCard.tsx b/src/components/common/StatCard.tsx
index 23b9f44..25ca1c9 100644
--- a/src/components/common/StatCard.tsx
+++ b/src/components/common/StatCard.tsx
@@ -4,7 +4,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com
interface StatCardProps {
title: string
- value: string
+ value: number
description: string
icon: ReactNode
}
diff --git a/src/components/home/SearchForm.tsx b/src/components/home/SearchForm.tsx
index 000527a..6fd63fe 100644
--- a/src/components/home/SearchForm.tsx
+++ b/src/components/home/SearchForm.tsx
@@ -1,134 +1,196 @@
-"use client";
-
-import { useState, useEffect, type ChangeEvent, type FormEvent } from "react";
-import type { SearchParams } from "@/types/search";
-import { Button } from "@/components/ui/button";
-import { Search } from "lucide-react";
+import React, { useState } from 'react';
+import historicalSearchService, { type SearchParams, type SearchResponse } from '@/services/historicalService';
+import { Button } from '@/components/ui/button';
+import { Search } from 'lucide-react';
+import SearchResults from './SearchResults';
interface SearchFormProps {
- onSearch: (params: SearchParams) => void;
- onReset?: () => void;
+ initialQuery?: string;
}
-// Default form data
-const defaultData: SearchParams = {
- firstName: "",
- lastName: "",
- ageAtMigration: "",
- yearOfArrival: "",
- regionOfOrigin: "all",
- settlementLocation: "all",
-};
+const SearchForm: React.FC
= ({ initialQuery = '' }) => {
+ // State for search
+ const [formData, setFormData] = useState({
+ query: initialQuery,
+ page: 1,
+ per_page: 10
+ });
+ const [searchResponse, setSearchResponse] = useState(null);
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState(null);
+ const [hasSearched, setHasSearched] = useState(false);
-// Form field definitions to make the JSX cleaner
-const textFields = [
- { id: "firstName", label: "First Name (Christian Name)", type: "text" },
- { id: "lastName", label: "Last Name (Surname)", type: "text" },
- { id: "ageAtMigration", label: "Age at Migration", type: "number", min: 0, max: 120 },
- {
- id: "yearOfArrival",
- label: "Date of Arrival in NT (Year)",
- type: "number",
- min: 1800,
- max: new Date().getFullYear()
- },
-];
+ // Form fields configuration
+ const textFields = [
+ { id: 'name', label: 'Name', type: 'text' },
+ { id: 'birth_year', label: 'Birth Year', type: 'number', min: 1800, max: 2000 },
+ { id: 'place_of_birth', label: 'Place of Birth', type: 'text' },
+ { id: 'arrival_year', label: 'Arrival Year', type: 'number', min: 1800, max: 2000 },
+ { id: 'occupation', label: 'Occupation', type: 'text' },
+ { id: 'residence', label: 'Place of Residence', type: 'text' }
+ ];
-const selectFields = [
- { id: "regionOfOrigin", label: "Region of Origin", options: ["all"] },
- { id: "settlementLocation", label: "Settlement Location", options: ["all"] },
-];
-
-const SearchForm = ({ onSearch, onReset }: SearchFormProps) => {
- const [formData, setFormData] = useState(defaultData);
- const [isLoading, setIsLoading] = useState(true);
-
- useEffect(() => {
- setIsLoading(false);
- }, []);
-
- const handleChange = (e: ChangeEvent) => {
+ // Handle form input changes
+ const handleChange = (e: React.ChangeEvent) => {
const { name, value } = e.target;
- setFormData(prevData => ({ ...prevData, [name]: value }));
+ setFormData(prev => ({ ...prev, [name]: value }));
};
- const handleSubmit = (e: FormEvent) => {
+ // Handle search form submission
+ const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
- onSearch(formData);
+ setIsLoading(true);
+ setError(null);
+ setHasSearched(true);
+
+ try {
+ const response = await historicalSearchService.searchRecords(formData);
+ setSearchResponse(response);
+ } catch (err) {
+ setError(err instanceof Error ? err.message : 'An unknown error occurred');
+ } finally {
+ setIsLoading(false);
+ }
};
+ // Handle form reset
const handleReset = () => {
- setFormData(defaultData);
- if (onReset) onReset();
+ setFormData({ page: 1, per_page: 10 });
+ setSearchResponse(null);
+ setHasSearched(false);
+ };
+
+ // Handle pagination
+ const handlePageChange = async (newPage: number) => {
+ setIsLoading(true);
+ try {
+ const response = await historicalSearchService.searchRecords({ ...formData, page: newPage });
+ setSearchResponse(response);
+ } catch (err) {
+ setError(err instanceof Error ? err.message : 'An unknown error occurred');
+ } finally {
+ setIsLoading(false);
+ }
};
return (
-