Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
"@radix-ui/react-popover": "^1.1.4",
"@radix-ui/react-select": "^2.1.4",
"@radix-ui/react-slot": "^1.1.1",
"@refinedev/core": "^4.57.5",
"@refinedev/simple-rest": "^5.0.10",
"@tanstack/react-query": "^5.65.1",
"axios": "^1.7.9",
"class-variance-authority": "^0.7.1",
Expand Down
38 changes: 38 additions & 0 deletions src/app/(refine)/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
'use client';

import { Refine } from '@refinedev/core';
import dataProvider from '@refinedev/simple-rest';
import axios from 'axios';
import { config } from '@/app/config';

// Create axios instance with auth headers
const axiosInstance = axios.create({
baseURL: config.baseURL + '/api',
headers: {
'Content-Type': 'application/json',
},
});

axiosInstance.interceptors.request.use((config) => {
const token =
typeof window !== 'undefined' ? localStorage.getItem('auth-data') : null;
if (token) {
const parsedToken = JSON.parse(token);
config.headers.Authorization = `Bearer ${parsedToken.accessToken}`;
}
return config;
});

// Create authenticated data provider
const customDataProvider = dataProvider(config.baseURL + '/api', axiosInstance);


export default function Layout({ children }: React.PropsWithChildren) {
return (
<Refine
dataProvider={customDataProvider}
>
{children}
</Refine>
);
}
70 changes: 70 additions & 0 deletions src/app/(refine)/refine/_components/checkInList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// components/CheckInList.tsx
'use client';
import { ChevronLeft, ChevronRight } from 'lucide-react';
import dayjs from 'dayjs';
import { User } from '../libs/interface';

export const CheckInList = ({
checkIns,
page,
pageSize,
setPage,
}: {
checkIns: User[];
page: number;
pageSize: number;
setPage: (page: number) => void;
}) => {
const totalPages = Math.ceil(checkIns.length / pageSize);
const paginatedCheckIns = checkIns.slice((page - 1) * pageSize, page * pageSize);

return (
<div className="rounded-lg border border-gray-200">
<div className="border-b border-gray-200 bg-gray-50 p-4">
<span className="text-sm font-medium text-gray-700">
เช็คอินวันนี้: {checkIns.length} คน
</span>
</div>

<ul className="divide-y divide-gray-200">
{paginatedCheckIns.map(user => (
<li
key={user.id}
className="flex items-center justify-between px-4 py-3 transition-colors hover:bg-gray-50"
>
<div className="text-sm font-medium text-gray-900">{user.name}</div>
<div className="text-sm font-semibold text-pink-600">
{dayjs(user.lastEntered).format('HH:mm')}
</div>
</li>
))}
</ul>

{checkIns.length > pageSize && (
<div className="border-t border-gray-200 p-4">
<div className="flex items-center justify-between">
<div className="text-sm text-gray-500">
หน้า {page} จาก {totalPages}
</div>
<div className="flex gap-2">
<button
onClick={() => setPage(Math.max(1, page - 1))}
disabled={page === 1}
className="rounded-lg p-2 text-gray-500 transition-colors hover:bg-gray-100 disabled:opacity-50"
>
<ChevronLeft className="h-5 w-5" />
</button>
<button
onClick={() => setPage(page + 1)}
disabled={page >= totalPages}
className="rounded-lg p-2 text-gray-500 transition-colors hover:bg-gray-100 disabled:opacity-50"
>
<ChevronRight className="h-5 w-5" />
</button>
</div>
</div>
</div>
)}
</div>
);
};
15 changes: 15 additions & 0 deletions src/app/(refine)/refine/_components/doughnutChart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
// components/DoughnutChart.tsx
'use client';
import { Doughnut } from 'react-chartjs-2';
import { ChartOptions } from 'chart.js';

export const DoughnutChart = ({
data,
options,
}: {
data: any;
options: ChartOptions<'doughnut'>;
}) => {
return <Doughnut data={data} options={options} />;
};
35 changes: 35 additions & 0 deletions src/app/(refine)/refine/_components/header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// components/DashboardHeader.tsx
'use client';
import Link from 'next/link';
import { ChevronLeft, Users } from 'lucide-react';

export const DashboardHeader = ({
total,
children,
}: {
total: number;
children: React.ReactNode;
}) => (
<div className="rounded-xl bg-white p-6 shadow-sm">
<div className="flex flex-col justify-between gap-4 md:flex-row md:items-center">
<div>
<Link
href="/staff/qr"
className="group mb-4 flex items-center gap-2 text-gray-700 hover:text-pink-600"
>
<ChevronLeft className="h-5 w-5 transition-transform group-hover:-translate-x-1" />
<h1 className="text-2xl font-semibold text-gray-900">
แดชบอร์ดลงทะเบียน
</h1>
</Link>
<div className="flex items-center gap-2 text-sm text-gray-600">
<Users className="h-5 w-5" />
<span>
ผู้ลงทะเบียนทั้งหมด: <strong className="text-pink-600">{total}</strong> คน
</span>
</div>
</div>
<div className="mt-4 flex gap-3 md:mt-0">{children}</div>
</div>
</div>
);
15 changes: 15 additions & 0 deletions src/app/(refine)/refine/_components/lineChart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
// components/LineChart.tsx
'use client';
import { Line } from 'react-chartjs-2';
import { ChartOptions } from 'chart.js';

export const LineChart = ({
data,
options,
}: {
data: any;
options: ChartOptions<'line'>;
}) => {
return <Line data={data} options={options} />;
};
74 changes: 74 additions & 0 deletions src/app/(refine)/refine/dashboard/_components/exportButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// components/ExportButton.tsx
'use client';
import { Download } from 'lucide-react';
import { FieldEntry, User } from '../../libs/interface';
import dayjs from 'dayjs';

export const ExportButton = ({ data }: { data: User[] }) => {
// Define fields to export (all User fields except imageUrl)
const fieldEntries: FieldEntry[] = [
{ label: 'ID', field: 'id' },
{ label: 'UID', field: 'uid' },
{ label: 'Name', field: 'name' },
{ label: 'Email', field: 'email' },
{ label: 'Phone', field: 'phone' },
{ label: 'University', field: 'university' },
{ label: 'Size Jersey', field: 'sizeJersey' },
{ label: 'Food Limitation', field: 'foodLimitation' },
{ label: 'Invitation Code', field: 'invitationCode' },
{ label: 'Age', field: 'age' },
{ label: 'Chronic Disease', field: 'chronicDisease' },
{ label: 'Drug Allergy', field: 'drugAllergy' },
{ label: 'Status', field: 'status' },
{ label: 'Graduated Year', field: 'graduatedYear' },
{ label: 'Faculty', field: 'faculty' },
{ label: 'Last Entered', field: 'lastEntered' },
{ label: 'Registered At', field: 'registeredAt' },
{ label: 'Role', field: 'role' },
{ label: 'Education', field: 'education' },
{ label: 'Is Acrophobic', field: 'isAcrophobic' },
];

const handleExportCSV = () => {
const headers = fieldEntries.map(f => f.label).join(',');

const csvContent = [
headers,
...data.map(user =>
fieldEntries.map(({ field }) => {
const value = user[field];
// Handle date fields
if (field === 'lastEntered' || field === 'registeredAt') {
return value && dayjs(value.toString()).isValid()
? dayjs(value.toString()).format('YYYY-MM-DD HH:mm')
: '';
}
// Escape quotes in string fields
return typeof value === 'string'
? `"${value.replace(/"/g, '""')}"`
: value;
}).join(',')
)
].join('\n');

const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', `users_export_${dayjs().format('YYYYMMDD_HHmmss')}.csv`);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
};

return (
<button
onClick={handleExportCSV}
className="flex items-center gap-2 rounded-lg bg-pink-600 px-4 py-2 text-white transition-colors hover:bg-pink-700"
>
<Download className="h-5 w-5" />
<span className="text-sm">Export</span>
</button>
);
}
126 changes: 126 additions & 0 deletions src/app/(refine)/refine/dashboard/_components/pagination.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { ChevronLeft, ChevronRight } from 'lucide-react';

export const Pagination = ({
current,
totalPages,
pageSize,
totalItems,
setCurrent,
setPageSize,
}: {
current: number;
totalPages: number;
pageSize: number;
totalItems: number;
setCurrent: (page: number) => void;
setPageSize: (size: number) => void;
}) => {
const hasNext = current < totalPages;
const hasPrev = current > 1;

return (
<div className="mt-6 flex flex-col items-center justify-between gap-4 sm:flex-row">
<div className="text-sm text-gray-500">
แสดง {(current - 1) * pageSize + 1} -{' '}
{Math.min(current * pageSize, totalItems)} จาก {totalItems}
</div>

<div className="flex items-center gap-2">
<PaginationControls
current={current}
totalPages={totalPages}
hasPrev={hasPrev}
hasNext={hasNext}
setCurrent={setCurrent}
/>
</div>

<PageSizeSelect pageSize={pageSize} setPageSize={setPageSize} />
</div>
);
};

const PaginationControls = ({
current,
totalPages,
hasPrev,
hasNext,
setCurrent,
}: {
current: number;
totalPages: number;
hasPrev: boolean;
hasNext: boolean;
setCurrent: (page: number) => void;
}) => (
<>
<button
onClick={() => setCurrent(1)}
disabled={!hasPrev}
className="rounded-lg bg-white p-2 text-gray-500 hover:bg-gray-100 disabled:opacity-50"
>
First
</button>
<button
onClick={() => setCurrent(current - 1)}
disabled={!hasPrev}
className="rounded-lg bg-white p-2 text-gray-500 hover:bg-gray-100 disabled:opacity-50"
>
<ChevronLeft className="h-5 w-5" />
</button>

<div className="flex gap-1">
{Array.from({ length: totalPages }, (_, i) => (
<button
key={i + 1}
onClick={() => setCurrent(i + 1)}
className={`rounded-lg px-3 py-1 text-sm ${
current === i + 1
? 'bg-pink-600 text-white'
: 'text-gray-500 hover:bg-gray-100'
}`}
>
{i + 1}
</button>
))}
</div>

<button
onClick={() => setCurrent(current + 1)}
disabled={!hasNext}
className="rounded-lg bg-white p-2 text-gray-500 hover:bg-gray-100 disabled:opacity-50"
>
<ChevronRight className="h-5 w-5" />
</button>
<button
onClick={() => setCurrent(totalPages)}
disabled={!hasNext}
className="rounded-lg bg-white p-2 text-gray-500 hover:bg-gray-100 disabled:opacity-50"
>
Last
</button>
</>
);

const PageSizeSelect = ({
pageSize,
setPageSize,
}: {
pageSize: number;
setPageSize: (size: number) => void;
}) => (
<div className="flex items-center gap-2">
<span>แสดงต่อหน้า:</span>
<select
value={pageSize}
onChange={(e) => setPageSize(Number(e.target.value))}
className="rounded-lg border border-gray-200 bg-gray-50 px-2 py-1 text-sm"
>
{[10, 20, 30, 40, 50].map((size) => (
<option key={size} value={size}>
{size}
</option>
))}
</select>
</div>
);
Loading
Loading