Initial commit from Create Next App
This commit is contained in:
commit
41879b49c7
|
|
@ -0,0 +1,41 @@
|
||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.*
|
||||||
|
.yarn/*
|
||||||
|
!.yarn/patches
|
||||||
|
!.yarn/plugins
|
||||||
|
!.yarn/releases
|
||||||
|
!.yarn/versions
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# next.js
|
||||||
|
/.next/
|
||||||
|
/out/
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# debug
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# env files (can opt-in for committing if needed)
|
||||||
|
.env*
|
||||||
|
|
||||||
|
# vercel
|
||||||
|
.vercel
|
||||||
|
|
||||||
|
# typescript
|
||||||
|
*.tsbuildinfo
|
||||||
|
next-env.d.ts
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
<!-- BEGIN:nextjs-agent-rules -->
|
||||||
|
# This is NOT the Next.js you know
|
||||||
|
|
||||||
|
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices.
|
||||||
|
<!-- END:nextjs-agent-rules -->
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
First, run the development server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
# or
|
||||||
|
yarn dev
|
||||||
|
# or
|
||||||
|
pnpm dev
|
||||||
|
# or
|
||||||
|
bun dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||||
|
|
||||||
|
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||||
|
|
||||||
|
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
||||||
|
|
||||||
|
## Learn More
|
||||||
|
|
||||||
|
To learn more about Next.js, take a look at the following resources:
|
||||||
|
|
||||||
|
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||||
|
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||||
|
|
||||||
|
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
||||||
|
|
||||||
|
## Deploy on Vercel
|
||||||
|
|
||||||
|
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||||
|
|
||||||
|
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { defineConfig, globalIgnores } from "eslint/config";
|
||||||
|
import nextVitals from "eslint-config-next/core-web-vitals";
|
||||||
|
import nextTs from "eslint-config-next/typescript";
|
||||||
|
|
||||||
|
const eslintConfig = defineConfig([
|
||||||
|
...nextVitals,
|
||||||
|
...nextTs,
|
||||||
|
// Override default ignores of eslint-config-next.
|
||||||
|
globalIgnores([
|
||||||
|
// Default ignores of eslint-config-next:
|
||||||
|
".next/**",
|
||||||
|
"out/**",
|
||||||
|
"build/**",
|
||||||
|
"next-env.d.ts",
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
|
||||||
|
export default eslintConfig;
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
|
const nextConfig: NextConfig = {
|
||||||
|
/* config options here */
|
||||||
|
};
|
||||||
|
|
||||||
|
export default nextConfig;
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"name": "reactapp",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "eslint"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"next": "16.2.7",
|
||||||
|
"react": "19.2.4",
|
||||||
|
"react-dom": "19.2.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tailwindcss/postcss": "^4",
|
||||||
|
"@types/node": "^20",
|
||||||
|
"@types/react": "^19",
|
||||||
|
"@types/react-dom": "^19",
|
||||||
|
"eslint": "^9",
|
||||||
|
"eslint-config-next": "16.2.7",
|
||||||
|
"tailwindcss": "^4",
|
||||||
|
"typescript": "^5"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
const config = {
|
||||||
|
plugins: {
|
||||||
|
"@tailwindcss/postcss": {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
|
||||||
|
After Width: | Height: | Size: 391 B |
|
|
@ -0,0 +1 @@
|
||||||
|
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
|
||||||
|
After Width: | Height: | Size: 1.0 KiB |
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
|
|
@ -0,0 +1 @@
|
||||||
|
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
|
||||||
|
After Width: | Height: | Size: 128 B |
|
|
@ -0,0 +1 @@
|
||||||
|
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
|
||||||
|
After Width: | Height: | Size: 385 B |
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
|
|
@ -0,0 +1,244 @@
|
||||||
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
:root {
|
||||||
|
--sidebar-width: 260px;
|
||||||
|
--header-height: 64px;
|
||||||
|
--primary: 222.2 47.4% 11.2%;
|
||||||
|
--primary-foreground: 210 40% 98%;
|
||||||
|
--background: 0 0% 100%;
|
||||||
|
--foreground: 222.2 84% 4.9%;
|
||||||
|
--muted: 210 40% 96.1%;
|
||||||
|
--muted-foreground: 215.4 16.3% 46.9%;
|
||||||
|
--border: 214.3 31.8% 91.4%;
|
||||||
|
--ring: 222.2 84% 4.9%;
|
||||||
|
--radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--primary: 210 40% 98%;
|
||||||
|
--primary-foreground: 222.2 47.4% 11.2%;
|
||||||
|
--background: 222.2 84% 4.9%;
|
||||||
|
--foreground: 210 40% 98%;
|
||||||
|
--muted: 217.2 32.6% 17.5%;
|
||||||
|
--muted-foreground: 215 20.2% 65.1%;
|
||||||
|
--border: 217.2 32.6% 17.5%;
|
||||||
|
--ring: 212.7 26.8% 83.9%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-gray-200 dark:border-gray-700;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
@apply bg-gray-50 text-gray-900 dark:bg-gray-950 dark:text-gray-100;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom scrollbar */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
@apply bg-transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
@apply bg-gray-300 dark:bg-gray-600 rounded-full;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
@apply bg-gray-400 dark:bg-gray-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animation for page transitions */
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(4px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-enter {
|
||||||
|
animation: fadeIn 0.2s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Card hover effect */
|
||||||
|
.card-hover {
|
||||||
|
@apply transition-all duration-200;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-hover:hover {
|
||||||
|
@apply shadow-lg -translate-y-0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar link styles */
|
||||||
|
.sidebar-link {
|
||||||
|
@apply flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition-all duration-200;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-link:hover {
|
||||||
|
@apply bg-gray-100 dark:bg-gray-800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-link.active {
|
||||||
|
@apply bg-primary/10 text-primary dark:bg-primary/20;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Table styles */
|
||||||
|
.table-container {
|
||||||
|
@apply overflow-x-auto rounded-xl border border-gray-200 dark:border-gray-700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-container table {
|
||||||
|
@apply w-full text-sm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-container th {
|
||||||
|
@apply px-4 py-3 text-left font-semibold text-gray-600 dark:text-gray-400 bg-gray-50 dark:bg-gray-800/50 border-b border-gray-200 dark:border-gray-700 whitespace-nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-container td {
|
||||||
|
@apply px-4 py-3 border-b border-gray-100 dark:border-gray-800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-container tbody tr {
|
||||||
|
@apply transition-colors duration-150;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-container tbody tr:hover {
|
||||||
|
@apply bg-gray-50 dark:bg-gray-800/30;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stats card */
|
||||||
|
.stat-card {
|
||||||
|
@apply bg-white dark:bg-gray-900 rounded-xl border border-gray-200 dark:border-gray-700 p-6 card-hover;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Button styles */
|
||||||
|
.btn {
|
||||||
|
@apply inline-flex items-center justify-center gap-2 rounded-lg px-4 py-2.5 text-sm font-medium transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
@apply btn bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
@apply btn bg-gray-100 text-gray-700 hover:bg-gray-200 focus:ring-gray-500 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
@apply btn bg-red-600 text-white hover:bg-red-700 focus:ring-red-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-success {
|
||||||
|
@apply btn bg-green-600 text-white hover:bg-green-700 focus:ring-green-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-warning {
|
||||||
|
@apply btn bg-yellow-500 text-white hover:bg-yellow-600 focus:ring-yellow-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline {
|
||||||
|
@apply btn border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sm {
|
||||||
|
@apply px-3 py-1.5 text-xs;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-lg {
|
||||||
|
@apply px-6 py-3 text-base;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form styles */
|
||||||
|
.form-input {
|
||||||
|
@apply w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 px-3 py-2.5 text-sm placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label {
|
||||||
|
@apply block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-select {
|
||||||
|
@apply form-input appearance-none cursor-pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-textarea {
|
||||||
|
@apply form-input resize-none min-h-[100px];
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Badge */
|
||||||
|
.badge {
|
||||||
|
@apply inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Card */
|
||||||
|
.card {
|
||||||
|
@apply bg-white dark:bg-gray-900 rounded-xl border border-gray-200 dark:border-gray-700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
@apply px-6 py-4 border-b border-gray-200 dark:border-gray-700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-body {
|
||||||
|
@apply p-6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-footer {
|
||||||
|
@apply px-6 py-4 border-t border-gray-200 dark:border-gray-700;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading spinner */
|
||||||
|
.spinner {
|
||||||
|
@apply animate-spin h-5 w-5 border-2 border-blue-600 border-t-transparent rounded-full;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner-sm {
|
||||||
|
@apply h-4 w-4 border;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner-lg {
|
||||||
|
@apply h-8 w-8 border-3;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modal overlay */
|
||||||
|
.modal-overlay {
|
||||||
|
@apply fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center p-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
@apply bg-white dark:bg-gray-900 rounded-xl shadow-2xl max-w-lg w-full max-h-[90vh] overflow-y-auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Empty state */
|
||||||
|
.empty-state {
|
||||||
|
@apply flex flex-col items-center justify-center py-16 text-center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state-icon {
|
||||||
|
@apply text-5xl mb-4 opacity-50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state-text {
|
||||||
|
@apply text-gray-500 dark:text-gray-400 text-lg font-medium;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state-description {
|
||||||
|
@apply text-gray-400 dark:text-gray-500 text-sm mt-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Grid layout helpers */
|
||||||
|
.grid-cards {
|
||||||
|
@apply grid gap-6;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
import type { Metadata } from "next";
|
||||||
|
import { Geist, Geist_Mono } from "next/font/google";
|
||||||
|
import "./globals.css";
|
||||||
|
|
||||||
|
const geistSans = Geist({
|
||||||
|
variable: "--font-geist-sans",
|
||||||
|
subsets: ["latin"],
|
||||||
|
});
|
||||||
|
|
||||||
|
const geistMono = Geist_Mono({
|
||||||
|
variable: "--font-geist-mono",
|
||||||
|
subsets: ["latin"],
|
||||||
|
});
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Create Next App",
|
||||||
|
description: "Generated by create next app",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function RootLayout({
|
||||||
|
children,
|
||||||
|
}: Readonly<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
}>) {
|
||||||
|
return (
|
||||||
|
<html
|
||||||
|
lang="en"
|
||||||
|
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
|
||||||
|
>
|
||||||
|
<body className="min-h-full flex flex-col">{children}</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
import Image from "next/image";
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col flex-1 items-center justify-center bg-zinc-50 font-sans dark:bg-black">
|
||||||
|
<main className="flex flex-1 w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
|
||||||
|
<Image
|
||||||
|
className="dark:invert"
|
||||||
|
src="/next.svg"
|
||||||
|
alt="Next.js logo"
|
||||||
|
width={100}
|
||||||
|
height={20}
|
||||||
|
priority
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
|
||||||
|
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
|
||||||
|
To get started, edit the page.tsx file.
|
||||||
|
</h1>
|
||||||
|
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
|
||||||
|
Looking for a starting point or more instructions? Head over to{" "}
|
||||||
|
<a
|
||||||
|
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||||
|
className="font-medium text-zinc-950 dark:text-zinc-50"
|
||||||
|
>
|
||||||
|
Templates
|
||||||
|
</a>{" "}
|
||||||
|
or the{" "}
|
||||||
|
<a
|
||||||
|
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||||
|
className="font-medium text-zinc-950 dark:text-zinc-50"
|
||||||
|
>
|
||||||
|
Learning
|
||||||
|
</a>{" "}
|
||||||
|
center.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
|
||||||
|
<a
|
||||||
|
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
|
||||||
|
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
className="dark:invert"
|
||||||
|
src="/vercel.svg"
|
||||||
|
alt="Vercel logomark"
|
||||||
|
width={16}
|
||||||
|
height={16}
|
||||||
|
/>
|
||||||
|
Deploy Now
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
|
||||||
|
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
Documentation
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,230 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { usePathname } from 'next/navigation';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { getUserRole } from '@/lib/api';
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
|
interface NavItem {
|
||||||
|
label: string;
|
||||||
|
href: string;
|
||||||
|
icon: string;
|
||||||
|
roles?: string[];
|
||||||
|
children?: { label: string; href: string }[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const navItems: NavItem[] = [
|
||||||
|
{
|
||||||
|
label: 'Dashboard',
|
||||||
|
href: '/',
|
||||||
|
icon: '📊',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Benefactors',
|
||||||
|
href: '/admin/benefactors',
|
||||||
|
icon: '👥',
|
||||||
|
roles: ['super-admin', 'admin'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Agents',
|
||||||
|
href: '/admin/agents',
|
||||||
|
icon: '🤝',
|
||||||
|
roles: ['super-admin', 'admin'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Kind Boxes',
|
||||||
|
href: '/admin/kind-boxes',
|
||||||
|
icon: '📦',
|
||||||
|
roles: ['super-admin', 'admin'],
|
||||||
|
children: [
|
||||||
|
{ label: 'All Boxes', href: '/admin/kind-boxes' },
|
||||||
|
{ label: 'Create', href: '/admin/kind-boxes/create' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Kind Box Requests',
|
||||||
|
href: '/admin/kind-box-requests',
|
||||||
|
icon: '📋',
|
||||||
|
roles: ['super-admin', 'admin'],
|
||||||
|
children: [
|
||||||
|
{ label: 'All Requests', href: '/admin/kind-box-requests' },
|
||||||
|
{ label: 'Create', href: '/admin/kind-box-requests/create' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Refer Times',
|
||||||
|
href: '/admin/refer-times',
|
||||||
|
icon: '⏰',
|
||||||
|
roles: ['super-admin', 'admin'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'My Kind Boxes',
|
||||||
|
href: '/benefactor/kind-boxes',
|
||||||
|
icon: '📦',
|
||||||
|
roles: ['benefactor'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'My Requests',
|
||||||
|
href: '/benefactor/kind-box-requests',
|
||||||
|
icon: '📋',
|
||||||
|
roles: ['benefactor'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'My Addresses',
|
||||||
|
href: '/benefactor/addresses',
|
||||||
|
icon: '📍',
|
||||||
|
roles: ['benefactor'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Refer Times',
|
||||||
|
href: '/benefactor/refer-times',
|
||||||
|
icon: '⏰',
|
||||||
|
roles: ['benefactor'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Agent Dashboard',
|
||||||
|
href: '/agent/dashboard',
|
||||||
|
icon: '🎯',
|
||||||
|
roles: ['agent'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Return Awaiting',
|
||||||
|
href: '/agent/kind-boxes',
|
||||||
|
icon: '📥',
|
||||||
|
roles: ['agent'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Delivery Awaiting',
|
||||||
|
href: '/agent/kind-box-requests',
|
||||||
|
icon: '📤',
|
||||||
|
roles: ['agent'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Products',
|
||||||
|
href: '/products',
|
||||||
|
icon: '🛍️',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Cart',
|
||||||
|
href: '/cart',
|
||||||
|
icon: '🛒',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Orders',
|
||||||
|
href: '/orders',
|
||||||
|
icon: '📦',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Campaigns',
|
||||||
|
href: '/campaigns',
|
||||||
|
icon: '🎯',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Wallet',
|
||||||
|
href: '/wallet',
|
||||||
|
icon: '💳',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Patients',
|
||||||
|
href: '/patients',
|
||||||
|
icon: '🏥',
|
||||||
|
roles: ['super-admin', 'admin'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Drivers',
|
||||||
|
href: '/drivers',
|
||||||
|
icon: '🚗',
|
||||||
|
roles: ['super-admin', 'admin'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Staff',
|
||||||
|
href: '/staff',
|
||||||
|
icon: '👔',
|
||||||
|
roles: ['super-admin', 'admin'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Sales Reports',
|
||||||
|
href: '/sales-reports',
|
||||||
|
icon: '📈',
|
||||||
|
roles: ['super-admin', 'admin'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Gamification',
|
||||||
|
href: '/gamification',
|
||||||
|
icon: '🏆',
|
||||||
|
roles: ['super-admin', 'admin'],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function Sidebar() {
|
||||||
|
const pathname = usePathname();
|
||||||
|
const [role, setRole] = useState<string | null>(null);
|
||||||
|
const [collapsed, setCollapsed] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setRole(getUserRole());
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const filteredItems = navItems.filter((item) => {
|
||||||
|
if (!item.roles || item.roles.length === 0) return true;
|
||||||
|
return role && item.roles.includes(role);
|
||||||
|
});
|
||||||
|
|
||||||
|
const isActive = (href: string) => {
|
||||||
|
if (href === '/') return pathname === '/';
|
||||||
|
return pathname.startsWith(href);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<aside
|
||||||
|
className={cn(
|
||||||
|
'fixed left-0 top-0 h-screen bg-white dark:bg-gray-900 border-r border-gray-200 dark:border-gray-700 z-30 transition-all duration-300 flex flex-col',
|
||||||
|
collapsed ? 'w-[60px]' : 'w-[260px]'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{/* Logo */}
|
||||||
|
<div className="flex items-center gap-3 px-4 h-16 border-b border-gray-200 dark:border-gray-700">
|
||||||
|
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center text-white font-bold text-sm shrink-0">
|
||||||
|
N
|
||||||
|
</div>
|
||||||
|
{!collapsed && (
|
||||||
|
<span className="font-bold text-lg bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent">
|
||||||
|
Niki
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Navigation */}
|
||||||
|
<nav className="flex-1 overflow-y-auto p-3 space-y-1">
|
||||||
|
{filteredItems.map((item) => {
|
||||||
|
const active = isActive(item.href);
|
||||||
|
return (
|
||||||
|
<div key={item.href}>
|
||||||
|
<Link
|
||||||
|
href={item.href}
|
||||||
|
className={cn(
|
||||||
|
'sidebar-link',
|
||||||
|
active && 'active',
|
||||||
|
collapsed && 'justify-center px-2'
|
||||||
|
)}
|
||||||
|
title={collapsed ? item.label : undefined}
|
||||||
|
>
|
||||||
|
<span className="text-lg shrink-0">{item.icon}</span>
|
||||||
|
{!collapsed && <span>{item.label}</span>}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{/* Collapse toggle */}
|
||||||
|
<button
|
||||||
|
onClick={() => setCollapsed(!collapsed)}
|
||||||
|
className="p-3 border-t border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors flex items-center justify-center text-gray-400"
|
||||||
|
>
|
||||||
|
<span className="text-sm">{collapsed ? '→' : '←'}</span>
|
||||||
|
</button>
|
||||||
|
</aside>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,503 @@
|
||||||
|
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8080';
|
||||||
|
|
||||||
|
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
||||||
|
|
||||||
|
interface ApiOptions {
|
||||||
|
method?: HttpMethod;
|
||||||
|
body?: unknown;
|
||||||
|
params?: Record<string, string | number | undefined>;
|
||||||
|
headers?: Record<string, string>;
|
||||||
|
isFormData?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function request<T>(endpoint: string, options: ApiOptions = {}): Promise<T> {
|
||||||
|
const { method = 'GET', body, params, headers = {}, isFormData = false } = options;
|
||||||
|
|
||||||
|
let url = `${API_BASE_URL}${endpoint}`;
|
||||||
|
|
||||||
|
if (params) {
|
||||||
|
const searchParams = new URLSearchParams();
|
||||||
|
Object.entries(params).forEach(([key, value]) => {
|
||||||
|
if (value !== undefined) {
|
||||||
|
searchParams.append(key, String(value));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const queryString = searchParams.toString();
|
||||||
|
if (queryString) {
|
||||||
|
url += `?${queryString}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = getToken();
|
||||||
|
const authHeaders: Record<string, string> = {};
|
||||||
|
if (token) {
|
||||||
|
authHeaders['Authorization'] = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchOptions: RequestInit = {
|
||||||
|
method,
|
||||||
|
headers: {
|
||||||
|
...authHeaders,
|
||||||
|
...(isFormData ? {} : { 'Content-Type': 'application/json' }),
|
||||||
|
...headers,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (body) {
|
||||||
|
fetchOptions.body = isFormData ? (body as FormData) : JSON.stringify(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(url, fetchOptions);
|
||||||
|
|
||||||
|
if (response.status === 401) {
|
||||||
|
// Try refresh token
|
||||||
|
const refreshed = await tryRefreshToken();
|
||||||
|
if (refreshed) {
|
||||||
|
authHeaders['Authorization'] = `Bearer ${getToken()}`;
|
||||||
|
fetchOptions.headers = {
|
||||||
|
...authHeaders,
|
||||||
|
...(isFormData ? {} : { 'Content-Type': 'application/json' }),
|
||||||
|
...headers,
|
||||||
|
};
|
||||||
|
const retryResponse = await fetch(url, fetchOptions);
|
||||||
|
return retryResponse.json();
|
||||||
|
}
|
||||||
|
throw new Error('Unauthorized');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json().catch(() => ({ message: 'Request failed' }));
|
||||||
|
throw new Error(error.message || `HTTP ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Auth Token Management ====================
|
||||||
|
|
||||||
|
export function getToken(): string | null {
|
||||||
|
if (typeof window === 'undefined') return null;
|
||||||
|
return localStorage.getItem('niki_token');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRefreshToken(): string | null {
|
||||||
|
if (typeof window === 'undefined') return null;
|
||||||
|
return localStorage.getItem('niki_refresh_token');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setTokens(token: string, refreshToken: string): void {
|
||||||
|
localStorage.setItem('niki_token', token);
|
||||||
|
localStorage.setItem('niki_refresh_token', refreshToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearTokens(): void {
|
||||||
|
localStorage.removeItem('niki_token');
|
||||||
|
localStorage.removeItem('niki_refresh_token');
|
||||||
|
localStorage.removeItem('niki_user');
|
||||||
|
localStorage.removeItem('niki_role');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setUser(user: unknown, role: string): void {
|
||||||
|
localStorage.setItem('niki_user', JSON.stringify(user));
|
||||||
|
localStorage.setItem('niki_role', role);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getUser(): unknown {
|
||||||
|
if (typeof window === 'undefined') return null;
|
||||||
|
const user = localStorage.getItem('niki_user');
|
||||||
|
return user ? JSON.parse(user) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getUserRole(): string | null {
|
||||||
|
if (typeof window === 'undefined') return null;
|
||||||
|
return localStorage.getItem('niki_role');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function tryRefreshToken(): Promise<boolean> {
|
||||||
|
const refreshToken = getRefreshToken();
|
||||||
|
if (!refreshToken) return false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/benefactors/refresh-access`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ refreshToken }),
|
||||||
|
});
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
setTokens(data.token, data.refreshToken);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Refresh failed
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Auth API ====================
|
||||||
|
|
||||||
|
export const authApi = {
|
||||||
|
// Benefactor auth
|
||||||
|
benefactorSendOtp: (data: { phoneNumber: string }) =>
|
||||||
|
request<{ message: string }>('/benefactors/send-otp', { method: 'POST', body: data }),
|
||||||
|
|
||||||
|
benefactorLoginRegister: (data: { phoneNumber: string; code: string }) =>
|
||||||
|
request<{ token: string; refreshToken: string; user: import('./types').Benefactor }>(
|
||||||
|
'/benefactors/login-register',
|
||||||
|
{ method: 'POST', body: data }
|
||||||
|
),
|
||||||
|
|
||||||
|
benefactorRefreshAccess: (data: { refreshToken: string }) =>
|
||||||
|
request<{ token: string; refreshToken: string }>('/benefactors/refresh-access', {
|
||||||
|
method: 'POST',
|
||||||
|
body: data,
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Admin auth
|
||||||
|
adminLogin: (data: { phoneNumber: string; password: string }) =>
|
||||||
|
request<{ token: string; refreshToken: string; user: import('./types').Admin }>('/admins/login-by-phone', {
|
||||||
|
method: 'POST',
|
||||||
|
body: data,
|
||||||
|
}),
|
||||||
|
|
||||||
|
adminRegister: (data: {
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
phoneNumber: string;
|
||||||
|
password: string;
|
||||||
|
email?: string;
|
||||||
|
}) =>
|
||||||
|
request<{ token: string; refreshToken: string; user: import('./types').Admin }>('/admins/register', {
|
||||||
|
method: 'POST',
|
||||||
|
body: data,
|
||||||
|
}),
|
||||||
|
|
||||||
|
adminRefreshAccess: (data: { refreshToken: string }) =>
|
||||||
|
request<{ token: string; refreshToken: string }>('/admins/refresh-access', {
|
||||||
|
method: 'POST',
|
||||||
|
body: data,
|
||||||
|
}),
|
||||||
|
|
||||||
|
adminProfile: () => request<import('./types').Admin>('/admins/profile'),
|
||||||
|
};
|
||||||
|
|
||||||
|
// ==================== Benefactor API ====================
|
||||||
|
|
||||||
|
export const benefactorApi = {
|
||||||
|
getAll: (params?: import('./types').PaginationParams) =>
|
||||||
|
request<import('./types').PaginatedResponse<import('./types').Benefactor>>('/admins/benefactors', {
|
||||||
|
params: params as Record<string, string | number | undefined>,
|
||||||
|
}),
|
||||||
|
|
||||||
|
getById: (id: number) =>
|
||||||
|
request<import('./types').Benefactor>(`/admins/benefactors/${id}`),
|
||||||
|
|
||||||
|
update: (id: number, data: import('./types').BenefactorUpdateRequest) =>
|
||||||
|
request<import('./types').Benefactor>(`/admins/benefactors/${id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: data,
|
||||||
|
}),
|
||||||
|
|
||||||
|
updateStatus: (id: number, status: import('./types').BenefactorStatus) =>
|
||||||
|
request<void>(`/admins/benefactors/${id}/status`, { method: 'PUT', body: { status } }),
|
||||||
|
|
||||||
|
// Self-service
|
||||||
|
getProfile: () => request<import('./types').Benefactor>('/benefactors/profile'),
|
||||||
|
|
||||||
|
updateProfile: (data: import('./types').BenefactorUpdateRequest) =>
|
||||||
|
request<import('./types').Benefactor>('/benefactors/profile', { method: 'PUT', body: data }),
|
||||||
|
};
|
||||||
|
|
||||||
|
// ==================== KindBox API ====================
|
||||||
|
|
||||||
|
export const kindBoxApi = {
|
||||||
|
// Admin endpoints
|
||||||
|
getAll: (params?: import('./types').PaginationParams) =>
|
||||||
|
request<import('./types').PaginatedResponse<import('./types').KindBox>>('/admins/kindboxes', {
|
||||||
|
params: params as Record<string, string | number | undefined>,
|
||||||
|
}),
|
||||||
|
|
||||||
|
getById: (id: number) =>
|
||||||
|
request<import('./types').KindBox>(`/admins/kindboxes/${id}`),
|
||||||
|
|
||||||
|
update: (id: number, data: Partial<import('./types').KindBox>) =>
|
||||||
|
request<import('./types').KindBox>(`/admins/kindboxes/update/${id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: data,
|
||||||
|
}),
|
||||||
|
|
||||||
|
enumerate: (id: number) =>
|
||||||
|
request<void>(`/admins/kindboxes/${id}/enumerate`, { method: 'PATCH' }),
|
||||||
|
|
||||||
|
assignReceiverAgent: (id: number, agentId: number) =>
|
||||||
|
request<void>(`/admins/kindboxes/${id}/assign-receiver-agent`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
body: { receiverAgentID: agentId },
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Benefactor endpoints
|
||||||
|
getMyKindBoxes: (params?: import('./types').PaginationParams) =>
|
||||||
|
request<import('./types').PaginatedResponse<import('./types').KindBox>>('/benefactors/kindboxes', {
|
||||||
|
params: params as Record<string, string | number | undefined>,
|
||||||
|
}),
|
||||||
|
|
||||||
|
getMyKindBox: (id: number) =>
|
||||||
|
request<import('./types').KindBox>(`/benefactors/kindboxes/${id}`),
|
||||||
|
|
||||||
|
registerEmptyingRequest: (id: number) =>
|
||||||
|
request<void>(`/benefactors/kindboxes/${id}/emptying-requests`, { method: 'PATCH' }),
|
||||||
|
|
||||||
|
// Agent endpoints
|
||||||
|
getReturnAwaiting: (params?: import('./types').PaginationParams) =>
|
||||||
|
request<import('./types').PaginatedResponse<import('./types').KindBox>>('/agents/kindboxes', {
|
||||||
|
params: params as Record<string, string | number | undefined>,
|
||||||
|
}),
|
||||||
|
|
||||||
|
getReturnAwaitingById: (id: number) =>
|
||||||
|
request<import('./types').KindBox>(`/agents/kindboxes/${id}`),
|
||||||
|
|
||||||
|
returnKindBox: (id: number) =>
|
||||||
|
request<void>(`/agents/kindboxes/${id}/return`, { method: 'PATCH' }),
|
||||||
|
};
|
||||||
|
|
||||||
|
// ==================== KindBoxReq API ====================
|
||||||
|
|
||||||
|
export const kindBoxReqApi = {
|
||||||
|
// Admin endpoints
|
||||||
|
getAll: (params?: import('./types').PaginationParams) =>
|
||||||
|
request<import('./types').PaginatedResponse<import('./types').KindBoxReq>>('/admins/kindboxreqs', {
|
||||||
|
params: params as Record<string, string | number | undefined>,
|
||||||
|
}),
|
||||||
|
|
||||||
|
getById: (id: number) =>
|
||||||
|
request<import('./types').KindBoxReq>(`/admins/kindboxreqs/${id}`),
|
||||||
|
|
||||||
|
create: (data: {
|
||||||
|
benefactorId: number;
|
||||||
|
kindBoxType: import('./types').KindBoxType;
|
||||||
|
countRequested: number;
|
||||||
|
description?: string;
|
||||||
|
deliverReferTimeId: number;
|
||||||
|
deliverReferDate: string;
|
||||||
|
deliverAddressId: number;
|
||||||
|
}) =>
|
||||||
|
request<import('./types').KindBoxReq>('/admins/kindboxreqs', { method: 'POST', body: data }),
|
||||||
|
|
||||||
|
update: (id: number, data: Partial<import('./types').KindBoxReq>) =>
|
||||||
|
request<import('./types').KindBoxReq>(`/admins/kindboxreqs/${id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: data,
|
||||||
|
}),
|
||||||
|
|
||||||
|
accept: (id: number) =>
|
||||||
|
request<void>(`/admins/kindboxreqs/${id}/accept-kind-box-req`, { method: 'PATCH' }),
|
||||||
|
|
||||||
|
reject: (id: number) =>
|
||||||
|
request<void>(`/admins/kindboxreqs/${id}/reject-kind-box-req`, { method: 'PATCH' }),
|
||||||
|
|
||||||
|
assignSenderAgent: (id: number, agentId: number) =>
|
||||||
|
request<void>(`/admins/kindboxreqs/${id}/assign-sender-agent`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
body: { senderAgentID: agentId },
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Benefactor endpoints
|
||||||
|
getMyRequests: (params?: import('./types').PaginationParams) =>
|
||||||
|
request<import('./types').PaginatedResponse<import('./types').KindBoxReq>>('/benefactors/kindboxreqs', {
|
||||||
|
params: params as Record<string, string | number | undefined>,
|
||||||
|
}),
|
||||||
|
|
||||||
|
getMyRequest: (id: number) =>
|
||||||
|
request<import('./types').KindBoxReq>(`/benefactors/kindboxreqs/${id}`),
|
||||||
|
|
||||||
|
createMyRequest: (data: {
|
||||||
|
kindBoxType: import('./types').KindBoxType;
|
||||||
|
countRequested: number;
|
||||||
|
description?: string;
|
||||||
|
deliverReferTimeId: number;
|
||||||
|
deliverReferDate: string;
|
||||||
|
deliverAddressId: number;
|
||||||
|
}) =>
|
||||||
|
request<import('./types').KindBoxReq>('/benefactors/kindboxreqs', { method: 'POST', body: data }),
|
||||||
|
|
||||||
|
updateMyRequest: (id: number, data: Partial<import('./types').KindBoxReq>) =>
|
||||||
|
request<import('./types').KindBoxReq>(`/benefactors/kindboxreqs/${id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: data,
|
||||||
|
}),
|
||||||
|
|
||||||
|
deleteMyRequest: (id: number) =>
|
||||||
|
request<void>(`/benefactors/kindboxreqs/${id}`, { method: 'DELETE' }),
|
||||||
|
|
||||||
|
// Agent endpoints
|
||||||
|
getDeliveryAwaiting: (params?: import('./types').PaginationParams) =>
|
||||||
|
request<import('./types').PaginatedResponse<import('./types').KindBoxReq>>(
|
||||||
|
'/agents/kindboxreqs/awaiting-delivery',
|
||||||
|
{ params: params as Record<string, string | number | undefined> }
|
||||||
|
),
|
||||||
|
|
||||||
|
getDeliveryAwaitingById: (id: number) =>
|
||||||
|
request<import('./types').KindBoxReq>(`/agents/kindboxreqs/awaiting-delivery/${id}`),
|
||||||
|
|
||||||
|
deliverKindBoxReq: (id: number) =>
|
||||||
|
request<void>(`/agents/kindboxreqs/${id}/deliver-kind-box-req`, { method: 'PATCH' }),
|
||||||
|
};
|
||||||
|
|
||||||
|
// ==================== Address API ====================
|
||||||
|
|
||||||
|
export const addressApi = {
|
||||||
|
getProvinces: () =>
|
||||||
|
request<import('./types').Province[]>('/benefactors/addresses/provinces'),
|
||||||
|
|
||||||
|
getCitiesByProvince: (provinceId: number) =>
|
||||||
|
request<import('./types').City[]>(`/benefactors/addresses/cities?provinceId=${provinceId}`),
|
||||||
|
|
||||||
|
getAll: (params?: import('./types').PaginationParams) =>
|
||||||
|
request<import('./types').PaginatedResponse<import('./types').AddressAggregated>>(
|
||||||
|
'/benefactors/addresses',
|
||||||
|
{ params: params as Record<string, string | number | undefined> }
|
||||||
|
),
|
||||||
|
|
||||||
|
getById: (id: number) =>
|
||||||
|
request<import('./types').AddressAggregated>(`/benefactors/addresses/${id}`),
|
||||||
|
|
||||||
|
create: (data: {
|
||||||
|
name: string;
|
||||||
|
address: string;
|
||||||
|
postalCode: string;
|
||||||
|
cityId: number;
|
||||||
|
provinceId: number;
|
||||||
|
lat?: number;
|
||||||
|
lon?: number;
|
||||||
|
}) =>
|
||||||
|
request<import('./types').Address>('/benefactors/addresses', { method: 'POST', body: data }),
|
||||||
|
|
||||||
|
update: (id: number, data: Partial<import('./types').Address>) =>
|
||||||
|
request<import('./types').Address>(`/benefactors/addresses/${id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: data,
|
||||||
|
}),
|
||||||
|
|
||||||
|
delete: (id: number) =>
|
||||||
|
request<void>(`/benefactors/addresses/${id}`, { method: 'DELETE' }),
|
||||||
|
};
|
||||||
|
|
||||||
|
// ==================== Refer Time API ====================
|
||||||
|
|
||||||
|
export const referTimeApi = {
|
||||||
|
getAll: (params?: import('./types').PaginationParams) =>
|
||||||
|
request<import('./types').PaginatedResponse<import('./types').ReferTime>>('/benefactors/refer-times', {
|
||||||
|
params: params as Record<string, string | number | undefined>,
|
||||||
|
}),
|
||||||
|
|
||||||
|
getById: (id: number) =>
|
||||||
|
request<import('./types').ReferTime>(`/benefactors/refer-times/${id}`),
|
||||||
|
|
||||||
|
// Admin
|
||||||
|
adminGetAll: (params?: import('./types').PaginationParams) =>
|
||||||
|
request<import('./types').PaginatedResponse<import('./types').ReferTime>>('/admins/refer-times', {
|
||||||
|
params: params as Record<string, string | number | undefined>,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
// ==================== Agent API ====================
|
||||||
|
|
||||||
|
export const agentApi = {
|
||||||
|
getAll: (params?: import('./types').PaginationParams) =>
|
||||||
|
request<import('./types').Admin[]>('/admins/agents', {
|
||||||
|
params: params as Record<string, string | number | undefined>,
|
||||||
|
}),
|
||||||
|
|
||||||
|
getById: (id: number) =>
|
||||||
|
request<import('./types').Admin>(`/admins/agents/${id}`),
|
||||||
|
};
|
||||||
|
|
||||||
|
// ==================== Product API ====================
|
||||||
|
|
||||||
|
export const productApi = {
|
||||||
|
getAll: (params?: import('./types').PaginationParams) =>
|
||||||
|
request<import('./types').PaginatedResponse<import('./types').Product>>('/products', {
|
||||||
|
params: params as Record<string, string | number | undefined>,
|
||||||
|
}),
|
||||||
|
|
||||||
|
getById: (id: number) =>
|
||||||
|
request<import('./types').Product>(`/products/${id}`),
|
||||||
|
};
|
||||||
|
|
||||||
|
// ==================== Campaign API ====================
|
||||||
|
|
||||||
|
export const campaignApi = {
|
||||||
|
getAll: (params?: import('./types').PaginationParams) =>
|
||||||
|
request<import('./types').PaginatedResponse<import('./types').Campaign>>('/campaigns', {
|
||||||
|
params: params as Record<string, string | number | undefined>,
|
||||||
|
}),
|
||||||
|
|
||||||
|
getById: (id: number) =>
|
||||||
|
request<import('./types').Campaign>(`/campaigns/${id}`),
|
||||||
|
|
||||||
|
create: (data: Partial<import('./types').Campaign>) =>
|
||||||
|
request<import('./types').Campaign>('/campaigns', { method: 'POST', body: data }),
|
||||||
|
};
|
||||||
|
|
||||||
|
// ==================== Order API ====================
|
||||||
|
|
||||||
|
export const orderApi = {
|
||||||
|
getAll: (params?: import('./types').PaginationParams) =>
|
||||||
|
request<import('./types').PaginatedResponse<import('./types').Order>>('/orders', {
|
||||||
|
params: params as Record<string, string | number | undefined>,
|
||||||
|
}),
|
||||||
|
|
||||||
|
getById: (id: number) =>
|
||||||
|
request<import('./types').Order>(`/orders/${id}`),
|
||||||
|
|
||||||
|
create: (data: {
|
||||||
|
items: { productId: number; quantity: number }[];
|
||||||
|
addressId: number;
|
||||||
|
shippingId: number;
|
||||||
|
paymentMethod: import('./types').PaymentMethodType;
|
||||||
|
}) =>
|
||||||
|
request<import('./types').Order>('/orders', { method: 'POST', body: data }),
|
||||||
|
};
|
||||||
|
|
||||||
|
// ==================== Wallet API ====================
|
||||||
|
|
||||||
|
export const walletApi = {
|
||||||
|
getWallet: () =>
|
||||||
|
request<import('./types').Wallet>('/wallet'),
|
||||||
|
|
||||||
|
getTransactions: (params?: import('./types').PaginationParams) =>
|
||||||
|
request<import('./types').PaginatedResponse<import('./types').Transaction>>('/wallet/transactions', {
|
||||||
|
params: params as Record<string, string | number | undefined>,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
// ==================== Payment API ====================
|
||||||
|
|
||||||
|
export const paymentApi = {
|
||||||
|
initiate: (data: {
|
||||||
|
amount: number;
|
||||||
|
payableType: import('./types').PayableType;
|
||||||
|
payableId: number;
|
||||||
|
methodId: number;
|
||||||
|
gatewayId: number;
|
||||||
|
currency: import('./types').Currency;
|
||||||
|
}) =>
|
||||||
|
request<{ paymentUrl: string; paymentId: number }>('/payments/initiate', {
|
||||||
|
method: 'POST',
|
||||||
|
body: data,
|
||||||
|
}),
|
||||||
|
|
||||||
|
verify: (paymentId: number) =>
|
||||||
|
request<import('./types').Payment>(`/payments/${paymentId}/verify`, { method: 'POST' }),
|
||||||
|
};
|
||||||
|
|
||||||
|
// ==================== Dashboard API ====================
|
||||||
|
|
||||||
|
export const dashboardApi = {
|
||||||
|
getStats: () =>
|
||||||
|
request<import('./types').DashboardStats>('/dashboard/stats'),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Export the API base URL for use in other contexts
|
||||||
|
export { API_BASE_URL };
|
||||||
|
export default request;
|
||||||
|
|
@ -0,0 +1,447 @@
|
||||||
|
// ==================== Entity Types ====================
|
||||||
|
|
||||||
|
export type Gender = 'male' | 'female';
|
||||||
|
export type UserRole = 'benefactor';
|
||||||
|
export type AdminRole = 'super-admin' | 'admin' | 'agent';
|
||||||
|
export type AdminStatus = 'active' | 'inactive';
|
||||||
|
export type BenefactorStatus = 'active' | 'inactive';
|
||||||
|
export type ReferTimeStatus = 'active' | 'inactive';
|
||||||
|
|
||||||
|
export type KindBoxType = 'on-table' | 'cylindrical' | 'stand-up';
|
||||||
|
export type KindBoxStatus =
|
||||||
|
| 'delivered'
|
||||||
|
| 'ready-to-return'
|
||||||
|
| 'assigned-receiver-agent'
|
||||||
|
| 'returned'
|
||||||
|
| 'enumerated';
|
||||||
|
export type KindBoxReqStatus =
|
||||||
|
| 'pending'
|
||||||
|
| 'accepted'
|
||||||
|
| 'assigned-sender-agent'
|
||||||
|
| 'rejected'
|
||||||
|
| 'delivered';
|
||||||
|
|
||||||
|
export type CartStatus = 'active' | 'expired' | 'checked_out';
|
||||||
|
export type CampaignStatus = 'draft' | 'active' | 'completed' | 'paused' | 'cancelled';
|
||||||
|
export type ProcessStatus =
|
||||||
|
| 'waiting-to-pay'
|
||||||
|
| 'processing'
|
||||||
|
| 'accepted'
|
||||||
|
| 'preparing'
|
||||||
|
| 'prepared'
|
||||||
|
| 'given-to-post'
|
||||||
|
| 'delivered'
|
||||||
|
| 'cancelled'
|
||||||
|
| 'system-cancellation';
|
||||||
|
export type PaymentStatus = 'paid' | 'unpaid';
|
||||||
|
export type PaymentTransactionStatus = 'Pending' | 'Success' | 'Failed' | 'Cancelled';
|
||||||
|
export type Currency = 'IRR' | 'USD';
|
||||||
|
export type WalletStatus = 'active' | 'frozen' | 'closed';
|
||||||
|
export type TransactionType = 'deposit' | 'withdraw' | 'refund' | 'donate';
|
||||||
|
export type NotificationType = 'email' | 'sms' | 'push';
|
||||||
|
export type PatientSex = 'unknown' | 'male' | 'female' | 'other';
|
||||||
|
export type PatientCaseStatus = 'open' | 'close' | 'inProgress';
|
||||||
|
export type PatientReferralSource = 'hospital' | 'community' | 'other';
|
||||||
|
export type DriverRole = 'driver';
|
||||||
|
|
||||||
|
// ==================== Entity Interfaces ====================
|
||||||
|
|
||||||
|
export interface Benefactor {
|
||||||
|
id: number;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
phoneNumber: string;
|
||||||
|
description: string;
|
||||||
|
email: string;
|
||||||
|
gender: Gender;
|
||||||
|
birthDate: string;
|
||||||
|
role: UserRole;
|
||||||
|
status: BenefactorStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Admin {
|
||||||
|
id: number;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
password?: string;
|
||||||
|
phoneNumber: string;
|
||||||
|
role: AdminRole;
|
||||||
|
description: string;
|
||||||
|
email: string;
|
||||||
|
gender: Gender;
|
||||||
|
status: AdminStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Address {
|
||||||
|
id: number;
|
||||||
|
postalCode: string;
|
||||||
|
address: string;
|
||||||
|
name: string;
|
||||||
|
lat: number;
|
||||||
|
lon: number;
|
||||||
|
cityId: number;
|
||||||
|
provinceId: number;
|
||||||
|
benefactorId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AddressAggregated {
|
||||||
|
address: Address;
|
||||||
|
province: Province;
|
||||||
|
city: City;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Province {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface City {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
provinceId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReferTime {
|
||||||
|
id: number;
|
||||||
|
duration: string;
|
||||||
|
status: ReferTimeStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface KindBox {
|
||||||
|
id: number;
|
||||||
|
kindBoxReqId: number;
|
||||||
|
benefactorId: number;
|
||||||
|
kindBoxType: KindBoxType;
|
||||||
|
amount: number;
|
||||||
|
serialNumber: string;
|
||||||
|
status: KindBoxStatus;
|
||||||
|
deliverReferTimeId: number;
|
||||||
|
deliverReferDate: string;
|
||||||
|
deliverAddressId: number;
|
||||||
|
senderAgentId: number;
|
||||||
|
deliveredAt: string;
|
||||||
|
returnReferTimeId: number;
|
||||||
|
returnReferDate: string;
|
||||||
|
returnAddressId: number;
|
||||||
|
receiverAgentId: number;
|
||||||
|
returnedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface KindBoxReq {
|
||||||
|
id: number;
|
||||||
|
benefactorId: number;
|
||||||
|
kindBoxType: KindBoxType;
|
||||||
|
countRequested: number;
|
||||||
|
countAccepted: number;
|
||||||
|
description: string;
|
||||||
|
status: KindBoxReqStatus;
|
||||||
|
deliverReferTimeId: number;
|
||||||
|
deliverReferDate: string;
|
||||||
|
deliverAddressId: number;
|
||||||
|
senderAgentId: number;
|
||||||
|
deliveredAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Product {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
description: string;
|
||||||
|
price: number;
|
||||||
|
stock: number;
|
||||||
|
isActive: boolean;
|
||||||
|
features: string;
|
||||||
|
createdAt: string;
|
||||||
|
images?: ProductImage[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProductImage {
|
||||||
|
id: number;
|
||||||
|
productId: number;
|
||||||
|
imagePath: string;
|
||||||
|
isPrimary: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Cart {
|
||||||
|
id: number;
|
||||||
|
userId: number;
|
||||||
|
items: CartItem[];
|
||||||
|
status: CartStatus;
|
||||||
|
totalPrice: number;
|
||||||
|
expireAt: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CartItem {
|
||||||
|
id: number;
|
||||||
|
cartId: number;
|
||||||
|
productId: number;
|
||||||
|
userId: number;
|
||||||
|
quantity: number;
|
||||||
|
price: number;
|
||||||
|
name: string;
|
||||||
|
addedAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Order {
|
||||||
|
id: number;
|
||||||
|
userId: number;
|
||||||
|
totalAmount: number;
|
||||||
|
totalDiscount: number;
|
||||||
|
shippingId: number;
|
||||||
|
paymentMethod: PaymentMethodType;
|
||||||
|
processStatus: ProcessStatus;
|
||||||
|
paymentStatus: PaymentStatus;
|
||||||
|
addressId: number;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
items?: OrderItem[];
|
||||||
|
shipping?: Shipping;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PaymentMethodType = 'online' | 'wallet' | 'cart';
|
||||||
|
|
||||||
|
export interface OrderItem {
|
||||||
|
id: number;
|
||||||
|
productId: number;
|
||||||
|
price: number;
|
||||||
|
quantity: number;
|
||||||
|
priceWithDiscount: number;
|
||||||
|
orderId: number;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Shipping {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
price: number;
|
||||||
|
isActive: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Payment {
|
||||||
|
id: number;
|
||||||
|
userId: number;
|
||||||
|
methodId: number;
|
||||||
|
gatewayId: number;
|
||||||
|
payableType: PayableType;
|
||||||
|
payableId: number;
|
||||||
|
totalAmount: number;
|
||||||
|
paidAmount: number;
|
||||||
|
currency: Currency;
|
||||||
|
status: PaymentTransactionStatus;
|
||||||
|
description: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
paidAt: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PayableType = 'Donate' | 'Order' | 'WaletCharge';
|
||||||
|
|
||||||
|
export interface PaymentGateway {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
code: string;
|
||||||
|
isActive: boolean;
|
||||||
|
config: Record<string, unknown>;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PaymentMethod {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
isActive: boolean;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Wallet {
|
||||||
|
id: number;
|
||||||
|
userId: number;
|
||||||
|
balance: string;
|
||||||
|
currency: Currency;
|
||||||
|
status: WalletStatus;
|
||||||
|
updatedAt: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Transaction {
|
||||||
|
id: number;
|
||||||
|
userId: number;
|
||||||
|
amount: string;
|
||||||
|
currency: Currency;
|
||||||
|
actionType: TransactionType;
|
||||||
|
timestamp: string;
|
||||||
|
idempotencyKey: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Campaign {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
link: string;
|
||||||
|
slogan: string;
|
||||||
|
goalAmount: number;
|
||||||
|
raisedAmount: number;
|
||||||
|
status: CampaignStatus;
|
||||||
|
createdAt: string;
|
||||||
|
deadlineAt: string | null;
|
||||||
|
creatorId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Donation {
|
||||||
|
id: number;
|
||||||
|
campaignId: number;
|
||||||
|
userId?: number;
|
||||||
|
sourceType: string;
|
||||||
|
sourceName: string;
|
||||||
|
referralCode: string;
|
||||||
|
link: string;
|
||||||
|
clicks: number;
|
||||||
|
conversions: number;
|
||||||
|
donationsTotal: number;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Driver {
|
||||||
|
id: number;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
phoneNumber: string;
|
||||||
|
nationalCode: string;
|
||||||
|
licenseNumber: string;
|
||||||
|
birthDate: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Patient {
|
||||||
|
id: number;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
dateOfBirth: string;
|
||||||
|
sex: PatientSex;
|
||||||
|
phone: string;
|
||||||
|
address: PatientAddress;
|
||||||
|
caseStatus: PatientCaseStatus;
|
||||||
|
referralSource: PatientReferralSource;
|
||||||
|
assignedStaffId: number;
|
||||||
|
startDate: string;
|
||||||
|
endDate: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PatientAddress {
|
||||||
|
id: number;
|
||||||
|
postalCode: string;
|
||||||
|
address: string;
|
||||||
|
name: string;
|
||||||
|
lat: number;
|
||||||
|
lon: number;
|
||||||
|
cityId: number;
|
||||||
|
provinceId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Staff {
|
||||||
|
id: number;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
phoneNumber: string;
|
||||||
|
email: string;
|
||||||
|
role: string;
|
||||||
|
isActive: boolean;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Notification {
|
||||||
|
id: number;
|
||||||
|
type: NotificationType;
|
||||||
|
recipient: string;
|
||||||
|
body: string;
|
||||||
|
status: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Permission {
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Authenticable {
|
||||||
|
id: number;
|
||||||
|
role: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== API Types ====================
|
||||||
|
|
||||||
|
export interface ApiResponse<T> {
|
||||||
|
data: T;
|
||||||
|
message?: string;
|
||||||
|
success: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PaginatedResponse<T> {
|
||||||
|
data: T[];
|
||||||
|
total: number;
|
||||||
|
page: number;
|
||||||
|
limit: number;
|
||||||
|
totalPages: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoginRequest {
|
||||||
|
phoneNumber: string;
|
||||||
|
password?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoginResponse {
|
||||||
|
token: string;
|
||||||
|
refreshToken: string;
|
||||||
|
user: Benefactor | Admin;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SendOtpRequest {
|
||||||
|
phoneNumber: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VerifyOtpRequest {
|
||||||
|
phoneNumber: string;
|
||||||
|
code: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RegisterRequest {
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
phoneNumber: string;
|
||||||
|
email?: string;
|
||||||
|
gender: Gender;
|
||||||
|
password?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BenefactorUpdateRequest {
|
||||||
|
firstName?: string;
|
||||||
|
lastName?: string;
|
||||||
|
description?: string;
|
||||||
|
email?: string;
|
||||||
|
gender?: Gender;
|
||||||
|
birthDate?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PaginationParams {
|
||||||
|
page?: number;
|
||||||
|
limit?: number;
|
||||||
|
sort?: string;
|
||||||
|
order?: 'asc' | 'desc';
|
||||||
|
search?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DashboardStats {
|
||||||
|
totalBenefactors: number;
|
||||||
|
totalKindBoxes: number;
|
||||||
|
totalKindBoxRequests: number;
|
||||||
|
totalCampaigns: number;
|
||||||
|
totalDonations: number;
|
||||||
|
totalOrders: number;
|
||||||
|
totalRevenue: number;
|
||||||
|
activeBenefactors: number;
|
||||||
|
pendingRequests: number;
|
||||||
|
returnedKindBoxes: number;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,123 @@
|
||||||
|
export function formatCurrency(amount: number, currency: string = 'IRR'): string {
|
||||||
|
if (currency === 'IRR') {
|
||||||
|
return `${amount.toLocaleString('fa-IR')} ریال`;
|
||||||
|
}
|
||||||
|
return `$${amount.toFixed(2)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatDate(dateString: string): string {
|
||||||
|
if (!dateString) return '-';
|
||||||
|
const date = new Date(dateString);
|
||||||
|
return date.toLocaleDateString('fa-IR', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatDateTime(dateString: string): string {
|
||||||
|
if (!dateString) return '-';
|
||||||
|
const date = new Date(dateString);
|
||||||
|
return date.toLocaleDateString('fa-IR', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getStatusColor(status: string): string {
|
||||||
|
const colorMap: Record<string, string> = {
|
||||||
|
active: 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400',
|
||||||
|
inactive: 'bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-400',
|
||||||
|
pending: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400',
|
||||||
|
accepted: 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400',
|
||||||
|
rejected: 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400',
|
||||||
|
delivered: 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400',
|
||||||
|
'assigned-sender-agent': 'bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-400',
|
||||||
|
'assigned-receiver-agent': 'bg-indigo-100 text-indigo-800 dark:bg-indigo-900/30 dark:text-indigo-400',
|
||||||
|
'ready-to-return': 'bg-orange-100 text-orange-800 dark:bg-orange-900/30 dark:text-orange-400',
|
||||||
|
returned: 'bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-400',
|
||||||
|
enumerated: 'bg-teal-100 text-teal-800 dark:bg-teal-900/30 dark:text-teal-400',
|
||||||
|
draft: 'bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-400',
|
||||||
|
completed: 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400',
|
||||||
|
paused: 'bg-orange-100 text-orange-800 dark:bg-orange-900/30 dark:text-orange-400',
|
||||||
|
cancelled: 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400',
|
||||||
|
paid: 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400',
|
||||||
|
unpaid: 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400',
|
||||||
|
'waiting-to-pay': 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400',
|
||||||
|
processing: 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400',
|
||||||
|
preparing: 'bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-400',
|
||||||
|
prepared: 'bg-indigo-100 text-indigo-800 dark:bg-indigo-900/30 dark:text-indigo-400',
|
||||||
|
'given-to-post': 'bg-cyan-100 text-cyan-800 dark:bg-cyan-900/30 dark:text-cyan-400',
|
||||||
|
'system-cancellation': 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400',
|
||||||
|
Success: 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400',
|
||||||
|
Failed: 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400',
|
||||||
|
frozen: 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400',
|
||||||
|
closed: 'bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-400',
|
||||||
|
};
|
||||||
|
return colorMap[status] || 'bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-400';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getStatusLabel(status: string): string {
|
||||||
|
const labelMap: Record<string, string> = {
|
||||||
|
'on-table': 'On Table',
|
||||||
|
cylindrical: 'Cylindrical',
|
||||||
|
'stand-up': 'Stand Up',
|
||||||
|
'assigned-sender-agent': 'Assigned Sender',
|
||||||
|
'assigned-receiver-agent': 'Assigned Receiver',
|
||||||
|
'ready-to-return': 'Ready to Return',
|
||||||
|
'given-to-post': 'Given to Post',
|
||||||
|
'system-cancellation': 'System Cancellation',
|
||||||
|
'waiting-to-pay': 'Waiting to Pay',
|
||||||
|
};
|
||||||
|
return labelMap[status] || status.charAt(0).toUpperCase() + status.slice(1).replace(/-/g, ' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getKindBoxTypeIcon(type: string): string {
|
||||||
|
const icons: Record<string, string> = {
|
||||||
|
'on-table': '📦',
|
||||||
|
cylindrical: '🥫',
|
||||||
|
'stand-up': '🗄️',
|
||||||
|
};
|
||||||
|
return icons[type] || '📦';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cn(...classes: (string | undefined | null | false)[]): string {
|
||||||
|
return classes.filter(Boolean).join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function truncateText(text: string, maxLength: number): string {
|
||||||
|
if (text.length <= maxLength) return text;
|
||||||
|
return text.substring(0, maxLength) + '...';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generatePagination(currentPage: number, totalPages: number): (number | '...')[] {
|
||||||
|
if (totalPages <= 7) {
|
||||||
|
return Array.from({ length: totalPages }, (_, i) => i + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const pages: (number | '...')[] = [];
|
||||||
|
|
||||||
|
pages.push(1);
|
||||||
|
|
||||||
|
if (currentPage > 3) {
|
||||||
|
pages.push('...');
|
||||||
|
}
|
||||||
|
|
||||||
|
const start = Math.max(2, currentPage - 1);
|
||||||
|
const end = Math.min(totalPages - 1, currentPage + 1);
|
||||||
|
|
||||||
|
for (let i = start; i <= end; i++) {
|
||||||
|
pages.push(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentPage < totalPages - 2) {
|
||||||
|
pages.push('...');
|
||||||
|
}
|
||||||
|
|
||||||
|
pages.push(totalPages);
|
||||||
|
|
||||||
|
return pages;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2017",
|
||||||
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"incremental": true,
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "next"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"next-env.d.ts",
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.tsx",
|
||||||
|
".next/types/**/*.ts",
|
||||||
|
".next/dev/types/**/*.ts",
|
||||||
|
"**/*.mts"
|
||||||
|
],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue