first commit
41
.gitignore
vendored
Normal file
@@ -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
|
||||||
36
README.md
Normal file
@@ -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.
|
||||||
18
eslint.config.mjs
Normal file
@@ -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;
|
||||||
7
next.config.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
|
const nextConfig: NextConfig = {
|
||||||
|
/* config options here */
|
||||||
|
};
|
||||||
|
|
||||||
|
export default nextConfig;
|
||||||
6149
package-lock.json
generated
Normal file
26
package.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"name": "pennyzo",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "eslint"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"jspdf": "^3.0.4",
|
||||||
|
"jspdf-autotable": "^5.0.2",
|
||||||
|
"next": "16.1.0",
|
||||||
|
"react": "19.2.3",
|
||||||
|
"react-dom": "19.2.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^20",
|
||||||
|
"@types/react": "^19",
|
||||||
|
"@types/react-dom": "^19",
|
||||||
|
"eslint": "^9",
|
||||||
|
"eslint-config-next": "16.1.0",
|
||||||
|
"typescript": "^5"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
public/bg_pennyzo.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
1
public/file.svg
Normal file
@@ -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 |
1
public/globe.svg
Normal file
@@ -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 |
BIN
public/login-bg.png
Normal file
|
After Width: | Height: | Size: 733 KiB |
BIN
public/logo_full.png
Normal file
|
After Width: | Height: | Size: 119 KiB |
BIN
public/logo_symbol.png
Normal file
|
After Width: | Height: | Size: 673 KiB |
BIN
public/startup-bg.png
Normal file
|
After Width: | Height: | Size: 568 KiB |
1
public/vercel.svg
Normal file
@@ -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 |
1
public/window.svg
Normal file
@@ -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 |
338
src/app/book/page.tsx
Normal file
@@ -0,0 +1,338 @@
|
|||||||
|
'use client';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import styles from '../dashboard/appointments/appointments.module.css';
|
||||||
|
|
||||||
|
// Mock appointment types
|
||||||
|
const APPOINTMENT_TYPES = [
|
||||||
|
{ id: 1, title: 'Initial Consultation', duration: '30 min', description: 'First-time meeting to discuss your needs', price: 0 },
|
||||||
|
{ id: 2, title: 'Follow-up Meeting', duration: '20 min', description: 'Review progress and next steps', price: 0 },
|
||||||
|
{ id: 3, title: 'Technical Support', duration: '45 min', description: 'Get help with technical issues', price: 2500 },
|
||||||
|
{ id: 4, title: 'Strategy Session', duration: '60 min', description: 'In-depth planning and strategy discussion', price: 5000 }
|
||||||
|
];
|
||||||
|
|
||||||
|
// Mock time slots
|
||||||
|
const TIME_SLOTS = [
|
||||||
|
'09:00 AM', '09:30 AM', '10:00 AM', '10:30 AM', '11:00 AM', '11:30 AM',
|
||||||
|
'02:00 PM', '02:30 PM', '03:00 PM', '03:30 PM', '04:00 PM', '04:30 PM'
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function PublicBookingPage() {
|
||||||
|
const router = useRouter();
|
||||||
|
const [currentStep, setCurrentStep] = useState(0);
|
||||||
|
const [showSuccess, setShowSuccess] = useState(false);
|
||||||
|
const [appointmentId, setAppointmentId] = useState('');
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
typeId: null,
|
||||||
|
date: '',
|
||||||
|
timeSlot: '',
|
||||||
|
name: '',
|
||||||
|
email: '',
|
||||||
|
phone: '',
|
||||||
|
reason: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedType = APPOINTMENT_TYPES.find(t => t.id === formData.typeId);
|
||||||
|
|
||||||
|
const handleNext = () => {
|
||||||
|
// Validation for each step
|
||||||
|
if (currentStep === 0 && !formData.typeId) {
|
||||||
|
alert('Please select an appointment type');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (currentStep === 1 && (!formData.date || !formData.timeSlot)) {
|
||||||
|
alert('Please select date and time');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (currentStep === 2) {
|
||||||
|
if (!formData.name || !formData.email) {
|
||||||
|
alert('Please fill in all required fields');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||||
|
if (!emailRegex.test(formData.email)) {
|
||||||
|
alert('Please enter a valid email address');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setCurrentStep(currentStep + 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBack = () => {
|
||||||
|
setCurrentStep(currentStep - 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
const newAppointmentId = `A-${String(Math.floor(Math.random() * 1000)).padStart(3, '0')}`;
|
||||||
|
setAppointmentId(newAppointmentId);
|
||||||
|
setShowSuccess(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const steps = ['Select Type', 'Date & Time', 'Your Info', 'Confirm'];
|
||||||
|
|
||||||
|
if (showSuccess) {
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
minHeight: '100vh',
|
||||||
|
background: 'linear-gradient(135deg, #0a0e1a 0%, #1a1f2e 100%)',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
padding: '2rem'
|
||||||
|
}}>
|
||||||
|
<div className={styles.confirmationScreen}>
|
||||||
|
<div className={styles.confirmationIcon}>✅</div>
|
||||||
|
<h1 className={styles.confirmationTitle}>Appointment Confirmed!</h1>
|
||||||
|
<div className={styles.confirmationId}>Booking ID: {appointmentId}</div>
|
||||||
|
|
||||||
|
<div className={styles.confirmationDetails}>
|
||||||
|
<div className={styles.confirmationRow}>
|
||||||
|
<span className={styles.confirmationLabel}>Appointment Type:</span>
|
||||||
|
<span className={styles.confirmationValue}>{selectedType?.title}</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.confirmationRow}>
|
||||||
|
<span className={styles.confirmationLabel}>Duration:</span>
|
||||||
|
<span className={styles.confirmationValue}>{selectedType?.duration}</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.confirmationRow}>
|
||||||
|
<span className={styles.confirmationLabel}>Date:</span>
|
||||||
|
<span className={styles.confirmationValue}>{formData.date}</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.confirmationRow}>
|
||||||
|
<span className={styles.confirmationLabel}>Time:</span>
|
||||||
|
<span className={styles.confirmationValue}>{formData.timeSlot}</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.confirmationRow}>
|
||||||
|
<span className={styles.confirmationLabel}>Name:</span>
|
||||||
|
<span className={styles.confirmationValue}>{formData.name}</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.confirmationRow}>
|
||||||
|
<span className={styles.confirmationLabel}>Email:</span>
|
||||||
|
<span className={styles.confirmationValue}>{formData.email}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p style={{ color: '#888', marginTop: '2rem', fontSize: '0.95rem' }}>
|
||||||
|
A confirmation email has been sent to <strong style={{ color: '#ffd700' }}>{formData.email}</strong>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
minHeight: '100vh',
|
||||||
|
background: 'linear-gradient(135deg, #0a0e1a 0%, #1a1f2e 100%)',
|
||||||
|
padding: '2rem'
|
||||||
|
}}>
|
||||||
|
<div className={styles.hamContainer}>
|
||||||
|
{/* Header */}
|
||||||
|
<div style={{ textAlign: 'center', marginBottom: '3rem' }}>
|
||||||
|
<h1 style={{
|
||||||
|
fontSize: '2.5rem',
|
||||||
|
fontWeight: 700,
|
||||||
|
background: 'linear-gradient(135deg, #ffd700 0%, #ffed4e 100%)',
|
||||||
|
WebkitBackgroundClip: 'text',
|
||||||
|
WebkitTextFillColor: 'transparent',
|
||||||
|
marginBottom: '0.5rem'
|
||||||
|
}}>
|
||||||
|
Book Your Appointment
|
||||||
|
</h1>
|
||||||
|
<p style={{ color: '#888', fontSize: '1.1rem' }}>
|
||||||
|
Schedule your appointment in a few simple steps
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Wizard Steps */}
|
||||||
|
<div className={styles.wizard}>
|
||||||
|
{steps.map((step, index) => (
|
||||||
|
<div key={index} className={`${styles.wizardStep} ${index === currentStep ? styles.active : ''} ${index < currentStep ? styles.completed : ''}`}>
|
||||||
|
<div className={styles.wizardStepCircle}>
|
||||||
|
{index < currentStep ? '✓' : index + 1}
|
||||||
|
</div>
|
||||||
|
<div className={styles.wizardStepLabel}>{step}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Step Content */}
|
||||||
|
<div className={styles.section}>
|
||||||
|
{/* Step 0: Select Type */}
|
||||||
|
{currentStep === 0 && (
|
||||||
|
<div>
|
||||||
|
<h3 style={{ color: '#ffd700', marginBottom: '1.5rem' }}>Select Appointment Type</h3>
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))', gap: '1.5rem' }}>
|
||||||
|
{APPOINTMENT_TYPES.map(type => (
|
||||||
|
<div
|
||||||
|
key={type.id}
|
||||||
|
className={`${styles.typeCard} ${formData.typeId === type.id ? styles.selected : ''}`}
|
||||||
|
onClick={() => setFormData({ ...formData, typeId: type.id })}
|
||||||
|
>
|
||||||
|
<div className={styles.typeCardTitle}>{type.title}</div>
|
||||||
|
<div className={styles.typeCardDuration}>⏱️ {type.duration}</div>
|
||||||
|
<div className={styles.typeCardDescription}>{type.description}</div>
|
||||||
|
<div className={styles.typeCardPrice}>
|
||||||
|
{type.price === 0 ? 'Free' : `₹${type.price.toLocaleString('en-IN')}`}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Step 1: Date & Time */}
|
||||||
|
{currentStep === 1 && (
|
||||||
|
<div>
|
||||||
|
<h3 style={{ color: '#ffd700', marginBottom: '1.5rem' }}>Select Date & Time</h3>
|
||||||
|
<div style={{ marginBottom: '2rem' }}>
|
||||||
|
<label className={styles.filterLabel}>Select Date</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
className={styles.inputField}
|
||||||
|
value={formData.date}
|
||||||
|
onChange={(e) => setFormData({ ...formData, date: e.target.value })}
|
||||||
|
min={new Date().toISOString().split('T')[0]}
|
||||||
|
style={{ maxWidth: '300px' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{formData.date && (
|
||||||
|
<div>
|
||||||
|
<label className={styles.filterLabel}>Available Time Slots</label>
|
||||||
|
<div className={styles.slotPicker}>
|
||||||
|
{TIME_SLOTS.map(slot => (
|
||||||
|
<div
|
||||||
|
key={slot}
|
||||||
|
className={`${styles.slot} ${formData.timeSlot === slot ? styles.slotSelected : ''}`}
|
||||||
|
onClick={() => setFormData({ ...formData, timeSlot: slot })}
|
||||||
|
>
|
||||||
|
{slot}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Step 2: Customer Info */}
|
||||||
|
{currentStep === 2 && (
|
||||||
|
<div>
|
||||||
|
<h3 style={{ color: '#ffd700', marginBottom: '1.5rem' }}>Your Information</h3>
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem', maxWidth: '500px' }}>
|
||||||
|
<div>
|
||||||
|
<label className={styles.filterLabel}>Full Name *</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className={styles.inputField}
|
||||||
|
value={formData.name}
|
||||||
|
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||||
|
placeholder="John Doe"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={styles.filterLabel}>Email Address *</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
className={styles.inputField}
|
||||||
|
value={formData.email}
|
||||||
|
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
||||||
|
placeholder="john@example.com"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={styles.filterLabel}>Phone Number</label>
|
||||||
|
<input
|
||||||
|
type="tel"
|
||||||
|
className={styles.inputField}
|
||||||
|
value={formData.phone}
|
||||||
|
onChange={(e) => setFormData({ ...formData, phone: e.target.value })}
|
||||||
|
placeholder="+91 98765 43210"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={styles.filterLabel}>Reason for Appointment (Optional)</label>
|
||||||
|
<textarea
|
||||||
|
className={styles.inputField}
|
||||||
|
value={formData.reason}
|
||||||
|
onChange={(e) => setFormData({ ...formData, reason: e.target.value })}
|
||||||
|
placeholder="Brief description..."
|
||||||
|
rows={4}
|
||||||
|
style={{ resize: 'vertical' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Step 3: Confirmation */}
|
||||||
|
{currentStep === 3 && (
|
||||||
|
<div>
|
||||||
|
<h3 style={{ color: '#ffd700', marginBottom: '1.5rem' }}>Confirm Your Appointment</h3>
|
||||||
|
<div className={styles.confirmationDetails}>
|
||||||
|
<div className={styles.confirmationRow}>
|
||||||
|
<span className={styles.confirmationLabel}>Appointment Type:</span>
|
||||||
|
<span className={styles.confirmationValue}>{selectedType?.title}</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.confirmationRow}>
|
||||||
|
<span className={styles.confirmationLabel}>Duration:</span>
|
||||||
|
<span className={styles.confirmationValue}>{selectedType?.duration}</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.confirmationRow}>
|
||||||
|
<span className={styles.confirmationLabel}>Date:</span>
|
||||||
|
<span className={styles.confirmationValue}>{formData.date}</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.confirmationRow}>
|
||||||
|
<span className={styles.confirmationLabel}>Time:</span>
|
||||||
|
<span className={styles.confirmationValue}>{formData.timeSlot}</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.confirmationRow}>
|
||||||
|
<span className={styles.confirmationLabel}>Name:</span>
|
||||||
|
<span className={styles.confirmationValue}>{formData.name}</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.confirmationRow}>
|
||||||
|
<span className={styles.confirmationLabel}>Email:</span>
|
||||||
|
<span className={styles.confirmationValue}>{formData.email}</span>
|
||||||
|
</div>
|
||||||
|
{formData.phone && (
|
||||||
|
<div className={styles.confirmationRow}>
|
||||||
|
<span className={styles.confirmationLabel}>Phone:</span>
|
||||||
|
<span className={styles.confirmationValue}>{formData.phone}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{selectedType && selectedType.price > 0 && (
|
||||||
|
<div className={styles.confirmationRow} style={{ borderTop: '2px solid rgba(255,215,0,0.3)', marginTop: '1rem', paddingTop: '1rem' }}>
|
||||||
|
<span className={styles.confirmationLabel}>Total:</span>
|
||||||
|
<span className={styles.confirmationValue} style={{ color: '#ffd700', fontSize: '1.5rem' }}>
|
||||||
|
₹{selectedType.price.toLocaleString('en-IN')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Navigation Buttons */}
|
||||||
|
<div style={{ display: 'flex', gap: '1rem', justifyContent: 'flex-end', marginTop: '2rem' }}>
|
||||||
|
{currentStep > 0 && (
|
||||||
|
<button className={styles.btnSecondary} onClick={handleBack}>
|
||||||
|
← Back
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{currentStep < 3 && (
|
||||||
|
<button className={styles.btnPrimary} onClick={handleNext}>
|
||||||
|
Next →
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{currentStep === 3 && (
|
||||||
|
<button className={styles.btnPrimary} onClick={handleSubmit}>
|
||||||
|
Confirm Booking
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
410
src/app/dashboard/accounts/accounts.module.css
Normal file
@@ -0,0 +1,410 @@
|
|||||||
|
/* Shared Accounting Container */
|
||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
animation: fadeIn 0.5s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Page Header */
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.titleGroup h1 {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 0.2rem;
|
||||||
|
background: linear-gradient(135deg, #fff, #ccc);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subTitle {
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Common Components */
|
||||||
|
.card {
|
||||||
|
background: rgba(255, 255, 255, 0.03);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
transition: transform 0.2s, border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:hover {
|
||||||
|
border-color: rgba(255, 215, 0, 0.3);
|
||||||
|
/* Gold tint on hover */
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Buttons */
|
||||||
|
.btnPrimary {
|
||||||
|
background: linear-gradient(135deg, var(--color-gold), #b8860b);
|
||||||
|
color: #000;
|
||||||
|
border: none;
|
||||||
|
padding: 0.6rem 1.2rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btnPrimary:hover {
|
||||||
|
box-shadow: 0 0 15px rgba(255, 215, 0, 0.3);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btnSecondary {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
color: #fff;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
padding: 0.6rem 1.2rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btnSecondary:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Grid Layouts */
|
||||||
|
.gridFour {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||||||
|
gap: 1.5rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gridTwo {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
|
||||||
|
gap: 1.5rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar Layout */
|
||||||
|
.gridWithSidebar {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 300px 1fr;
|
||||||
|
gap: 2rem;
|
||||||
|
height: calc(100vh - 200px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebarList {
|
||||||
|
background: rgba(255, 255, 255, 0.02);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchBox {
|
||||||
|
padding: 1rem;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.accountList {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accountItem {
|
||||||
|
padding: 1rem;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accountItem:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.accountItem.active {
|
||||||
|
background: rgba(var(--color-gold-rgb), 0.1);
|
||||||
|
border-left: 3px solid var(--color-gold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.accName {
|
||||||
|
display: block;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #fff;
|
||||||
|
margin-bottom: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accType {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mainContent {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contentHeader {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.periodBadge {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
padding: 0.4rem 0.8rem;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #aaa;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Metrics */
|
||||||
|
.metricLabel {
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metricValue {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metricTrend {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trendUp {
|
||||||
|
color: #4caf50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trendDown {
|
||||||
|
color: #f44336;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trendNeutral {
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tables */
|
||||||
|
.tableContainer {
|
||||||
|
width: 100%;
|
||||||
|
overflow-x: auto;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table th {
|
||||||
|
text-align: left;
|
||||||
|
padding: 1rem;
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
font-weight: 600;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table td {
|
||||||
|
padding: 1rem;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table tr:hover td {
|
||||||
|
background: rgba(255, 255, 255, 0.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Alerts */
|
||||||
|
.alertCard {
|
||||||
|
background: rgba(244, 67, 54, 0.1);
|
||||||
|
border: 1px solid rgba(244, 67, 54, 0.3);
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: #ff8a80;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alertIcon {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Forms */
|
||||||
|
.formGroup {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--color-gold);
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input,
|
||||||
|
.select {
|
||||||
|
background: rgba(2, 26, 21, 0.6);
|
||||||
|
border: 1px solid rgba(255, 215, 0, 0.2);
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
color: #fff;
|
||||||
|
font-family: inherit;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input:focus,
|
||||||
|
.select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--color-gold);
|
||||||
|
background: rgba(2, 26, 21, 0.8);
|
||||||
|
box-shadow: 0 0 15px rgba(255, 215, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.select option {
|
||||||
|
background-color: #051a15;
|
||||||
|
/* Deep Green Tone */
|
||||||
|
color: var(--color-gold);
|
||||||
|
padding: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select option:hover {
|
||||||
|
background-color: #0a2f25;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modal Styles */
|
||||||
|
.modalOverlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.85);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 1000;
|
||||||
|
animation: fadeIn 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modalContent {
|
||||||
|
background: linear-gradient(145deg, rgba(5, 26, 21, 0.95), rgba(0, 0, 0, 0.98));
|
||||||
|
border: 1px solid rgba(255, 215, 0, 0.3);
|
||||||
|
border-radius: 24px;
|
||||||
|
width: 90%;
|
||||||
|
max-width: 600px;
|
||||||
|
max-height: 90vh;
|
||||||
|
padding: 2.5rem;
|
||||||
|
box-shadow:
|
||||||
|
0 20px 50px rgba(0, 0, 0, 0.5),
|
||||||
|
0 0 30px rgba(255, 215, 0, 0.1),
|
||||||
|
inset 0 0 20px rgba(255, 255, 255, 0.02);
|
||||||
|
position: relative;
|
||||||
|
animation: scaleIn 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||||
|
color: #fff;
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modalHeader {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
border-bottom: 1px solid rgba(255, 215, 0, 0.1);
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modalHeader h3 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
background: linear-gradient(135deg, #fff, var(--color-gold));
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.closeButton {
|
||||||
|
position: absolute;
|
||||||
|
top: 1rem;
|
||||||
|
right: 1rem;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #aaa;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.closeButton:hover {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invoicePreview {
|
||||||
|
background: #fff;
|
||||||
|
color: #000;
|
||||||
|
padding: 2rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin: 1.5rem 0;
|
||||||
|
min-height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes scaleIn {
|
||||||
|
from {
|
||||||
|
transform: scale(0.95);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
285
src/app/dashboard/accounts/components/AddCustomerModal.tsx
Normal file
@@ -0,0 +1,285 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import styles from '../accounts.module.css'; // Using shared styles for consistency
|
||||||
|
import { useToast } from '../../../../context/ToastContext';
|
||||||
|
|
||||||
|
interface AddCustomerModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onSave: (customerData: any) => void;
|
||||||
|
initialName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AddCustomerModal({ isOpen, onClose, onSave, initialName = '' }: AddCustomerModalProps) {
|
||||||
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
const { showToast } = useToast();
|
||||||
|
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
ledgerName: initialName,
|
||||||
|
group: 'Sundry Debtors', // Default for Customers
|
||||||
|
gstin: '',
|
||||||
|
addressLine1: '',
|
||||||
|
addressLine2: '',
|
||||||
|
city: '',
|
||||||
|
state: '',
|
||||||
|
pincode: '',
|
||||||
|
contactPerson: '',
|
||||||
|
mobile: '',
|
||||||
|
email: '',
|
||||||
|
pan: '',
|
||||||
|
openingBalance: 0,
|
||||||
|
balanceType: 'Debit'
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleChange = (field: string, value: any) => {
|
||||||
|
setFormData(prev => ({ ...prev, [field]: value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
// Basic validation
|
||||||
|
if (!formData.ledgerName) {
|
||||||
|
showToast('Ledger Name is required', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onSave(formData);
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.modalOverlay} style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||||
|
<div className={styles.modalContent} style={{
|
||||||
|
maxWidth: '900px',
|
||||||
|
width: '90%',
|
||||||
|
maxHeight: '90vh',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
overflow: 'hidden', // Prevents double scroll
|
||||||
|
borderRadius: '16px',
|
||||||
|
border: '1px solid rgba(255, 215, 0, 0.2)',
|
||||||
|
background: '#0a1a15' // Deep green background
|
||||||
|
}}>
|
||||||
|
<div className={styles.modalHeader} style={{
|
||||||
|
flexShrink: 0,
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: '1.5rem 2rem',
|
||||||
|
borderBottom: '1px solid rgba(255, 255, 255, 0.05)'
|
||||||
|
}}>
|
||||||
|
<h2 style={{ fontSize: '1.5rem', margin: 0, color: '#fff', fontWeight: 600 }}>Add Customer</h2>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
style={{
|
||||||
|
background: 'rgba(255, 255, 255, 0.05)',
|
||||||
|
border: '1px solid rgba(255, 255, 255, 0.1)',
|
||||||
|
borderRadius: '50%',
|
||||||
|
width: '36px',
|
||||||
|
height: '36px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
cursor: 'pointer',
|
||||||
|
color: '#e0e0e0',
|
||||||
|
transition: 'all 0.2s ease',
|
||||||
|
outline: 'none'
|
||||||
|
}}
|
||||||
|
onMouseEnter={e => {
|
||||||
|
e.currentTarget.style.background = 'rgba(244, 67, 54, 0.2)';
|
||||||
|
e.currentTarget.style.color = '#ff8a80';
|
||||||
|
e.currentTarget.style.borderColor = 'rgba(244, 67, 54, 0.4)';
|
||||||
|
}}
|
||||||
|
onMouseLeave={e => {
|
||||||
|
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.05)';
|
||||||
|
e.currentTarget.style.color = '#e0e0e0';
|
||||||
|
e.currentTarget.style.borderColor = 'rgba(255, 255, 255, 0.1)';
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||||
|
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.modalBody} style={{
|
||||||
|
padding: '2rem',
|
||||||
|
overflowY: 'auto',
|
||||||
|
flex: 1 // Takes remaining space
|
||||||
|
}}>
|
||||||
|
|
||||||
|
{/* Top Section */}
|
||||||
|
<div className={styles.gridTwo}>
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<label className={styles.label}>Ledger Name <span style={{ color: 'red' }}>*</span></label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className={styles.input}
|
||||||
|
value={formData.ledgerName}
|
||||||
|
onChange={e => handleChange('ledgerName', e.target.value)}
|
||||||
|
placeholder="e.g. Acme Corp"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<label className={styles.label}>Group</label>
|
||||||
|
<select
|
||||||
|
className={styles.select}
|
||||||
|
value={formData.group}
|
||||||
|
onChange={e => handleChange('group', e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="Sundry Debtors">Sundry Debtors (Customers)</option>
|
||||||
|
<option value="Sundry Creditors">Sundry Creditors (Suppliers)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Party Details */}
|
||||||
|
<h3 style={{ color: 'var(--color-gold)', marginTop: '1.5rem', marginBottom: '1rem', borderBottom: '1px solid rgba(255,255,255,0.1)', paddingBottom: '0.5rem' }}>Party Details</h3>
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<label className={styles.label}>GSTIN</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className={styles.input}
|
||||||
|
value={formData.gstin}
|
||||||
|
onChange={e => handleChange('gstin', e.target.value)}
|
||||||
|
placeholder="27ABCDE1234F1Z5"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Billing Address */}
|
||||||
|
<h3 style={{ color: 'var(--color-gold)', marginTop: '1.5rem', marginBottom: '1rem', borderBottom: '1px solid rgba(255,255,255,0.1)', paddingBottom: '0.5rem' }}>Billing Address</h3>
|
||||||
|
<div className={styles.gridTwo}>
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<label className={styles.label}>Address Line 1</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className={styles.input}
|
||||||
|
value={formData.addressLine1}
|
||||||
|
onChange={e => handleChange('addressLine1', e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<label className={styles.label}>Address Line 2</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className={styles.input}
|
||||||
|
value={formData.addressLine2}
|
||||||
|
onChange={e => handleChange('addressLine2', e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.gridThree}>
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<label className={styles.label}>City</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className={styles.input}
|
||||||
|
value={formData.city}
|
||||||
|
onChange={e => handleChange('city', e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<label className={styles.label}>State</label>
|
||||||
|
<select
|
||||||
|
className={styles.select}
|
||||||
|
value={formData.state}
|
||||||
|
onChange={e => handleChange('state', e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="">Select State</option>
|
||||||
|
<option value="Maharashtra">Maharashtra</option>
|
||||||
|
<option value="Delhi">Delhi</option>
|
||||||
|
<option value="Karnataka">Karnataka</option>
|
||||||
|
<option value="Rajasthan">Rajasthan</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<label className={styles.label}>Pincode</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className={styles.input}
|
||||||
|
value={formData.pincode}
|
||||||
|
onChange={e => handleChange('pincode', e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Contact Details */}
|
||||||
|
<h3 style={{ color: 'var(--color-gold)', marginTop: '1.5rem', marginBottom: '1rem', borderBottom: '1px solid rgba(255,255,255,0.1)', paddingBottom: '0.5rem' }}>Contact Details</h3>
|
||||||
|
<div className={styles.gridThree}>
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<label className={styles.label}>Contact Person</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className={styles.input}
|
||||||
|
value={formData.contactPerson}
|
||||||
|
onChange={e => handleChange('contactPerson', e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<label className={styles.label}>Mobile No</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className={styles.input}
|
||||||
|
value={formData.mobile}
|
||||||
|
onChange={e => handleChange('mobile', e.target.value)}
|
||||||
|
placeholder="+91"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<label className={styles.label}>Email</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
className={styles.input}
|
||||||
|
value={formData.email}
|
||||||
|
onChange={e => handleChange('email', e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tax & Other Details */}
|
||||||
|
<h3 style={{ color: 'var(--color-gold)', marginTop: '1.5rem', marginBottom: '1rem', borderBottom: '1px solid rgba(255,255,255,0.1)', paddingBottom: '0.5rem' }}>Tax & Financials</h3>
|
||||||
|
<div className={styles.gridTwo}>
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<label className={styles.label}>PAN No</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className={styles.input}
|
||||||
|
value={formData.pan}
|
||||||
|
onChange={e => handleChange('pan', e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<label className={styles.label}>Opening Balance</label>
|
||||||
|
<div style={{ display: 'flex', gap: '10px' }}>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
className={styles.input}
|
||||||
|
value={formData.openingBalance}
|
||||||
|
onChange={e => handleChange('openingBalance', e.target.value)}
|
||||||
|
/>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', color: '#fff' }}>
|
||||||
|
<label><input type="radio" name="balType" checked={formData.balanceType === 'Debit'} onChange={() => handleChange('balanceType', 'Debit')} /> Dr</label>
|
||||||
|
<label><input type="radio" name="balType" checked={formData.balanceType === 'Credit'} onChange={() => handleChange('balanceType', 'Credit')} /> Cr</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.modalFooter} style={{
|
||||||
|
borderTop: '1px solid rgba(255,215,0,0.1)',
|
||||||
|
padding: '1.5rem',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
gap: '1rem',
|
||||||
|
flexShrink: 0
|
||||||
|
}}>
|
||||||
|
<button className={styles.btnSecondary} onClick={onClose}>Cancel</button>
|
||||||
|
<button className={styles.btnPrimary} onClick={handleSubmit}>Save Customer</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
258
src/app/dashboard/accounts/components/AddItemModal.tsx
Normal file
@@ -0,0 +1,258 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import styles from '../accounts.module.css'; // Using shared styles
|
||||||
|
import { useToast } from '../../../../context/ToastContext';
|
||||||
|
|
||||||
|
interface AddItemModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onSave: (itemData: any) => void;
|
||||||
|
initialName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AddItemModal({ isOpen, onClose, onSave, initialName = '' }: AddItemModalProps) {
|
||||||
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
const { showToast } = useToast();
|
||||||
|
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
itemName: initialName,
|
||||||
|
category: 'Default Group',
|
||||||
|
itemType: 'Goods', // Goods or Service
|
||||||
|
unit: 'PCS',
|
||||||
|
decimalPlaces: 2,
|
||||||
|
barcode: '',
|
||||||
|
description: '',
|
||||||
|
mrp: 0,
|
||||||
|
salePrice: 0,
|
||||||
|
saleDiscount: 0,
|
||||||
|
purchasePrice: 0,
|
||||||
|
purchaseDiscount: 0,
|
||||||
|
openingQty: 0,
|
||||||
|
openingRate: 0,
|
||||||
|
taxRate: 18
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleChange = (field: string, value: any) => {
|
||||||
|
setFormData(prev => ({ ...prev, [field]: value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
if (!formData.itemName) {
|
||||||
|
showToast('Item Name is required', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onSave(formData);
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.modalOverlay} style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||||
|
<div className={styles.modalContent} style={{
|
||||||
|
maxWidth: '1000px',
|
||||||
|
width: '95%',
|
||||||
|
maxHeight: '90vh',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
overflow: 'hidden', // Prevents double scroll
|
||||||
|
borderRadius: '16px',
|
||||||
|
border: '1px solid rgba(255, 215, 0, 0.2)',
|
||||||
|
background: '#0a1a15' // Deep green background
|
||||||
|
}}>
|
||||||
|
<div className={styles.modalHeader} style={{
|
||||||
|
flexShrink: 0,
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: '1.5rem 2rem',
|
||||||
|
borderBottom: '1px solid rgba(255, 255, 255, 0.05)'
|
||||||
|
}}>
|
||||||
|
<h2 style={{ fontSize: '1.5rem', margin: 0, color: '#fff', fontWeight: 600 }}>Add Item / Service</h2>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
style={{
|
||||||
|
background: 'rgba(255, 255, 255, 0.05)',
|
||||||
|
border: '1px solid rgba(255, 255, 255, 0.1)',
|
||||||
|
borderRadius: '50%',
|
||||||
|
width: '36px',
|
||||||
|
height: '36px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
cursor: 'pointer',
|
||||||
|
color: '#e0e0e0',
|
||||||
|
transition: 'all 0.2s ease',
|
||||||
|
outline: 'none'
|
||||||
|
}}
|
||||||
|
onMouseEnter={e => {
|
||||||
|
e.currentTarget.style.background = 'rgba(244, 67, 54, 0.2)';
|
||||||
|
e.currentTarget.style.color = '#ff8a80';
|
||||||
|
e.currentTarget.style.borderColor = 'rgba(244, 67, 54, 0.4)';
|
||||||
|
}}
|
||||||
|
onMouseLeave={e => {
|
||||||
|
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.05)';
|
||||||
|
e.currentTarget.style.color = '#e0e0e0';
|
||||||
|
e.currentTarget.style.borderColor = 'rgba(255, 255, 255, 0.1)';
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||||
|
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.modalBody} style={{
|
||||||
|
padding: '2rem',
|
||||||
|
overflowY: 'auto',
|
||||||
|
flex: 1, // Takes remaining space
|
||||||
|
}}>
|
||||||
|
|
||||||
|
{/* Top Section: Basic Info */}
|
||||||
|
<div className={styles.gridThree}>
|
||||||
|
<div className={styles.formGroup} style={{ gridColumn: 'span 2' }}>
|
||||||
|
<label className={styles.label}>Item Name <span style={{ color: 'red' }}>*</span></label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className={styles.input}
|
||||||
|
value={formData.itemName}
|
||||||
|
onChange={e => handleChange('itemName', e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<label className={styles.label}>Item Type</label>
|
||||||
|
<div style={{ display: 'flex', gap: '20px', marginTop: '10px', color: '#fff' }}>
|
||||||
|
<label style={{ display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer', fontSize: '0.95rem' }}>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="itemType"
|
||||||
|
checked={formData.itemType === 'Goods'}
|
||||||
|
onChange={() => handleChange('itemType', 'Goods')}
|
||||||
|
style={{ accentColor: 'var(--color-gold)' }}
|
||||||
|
/> Goods
|
||||||
|
</label>
|
||||||
|
<label style={{ display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer', fontSize: '0.95rem' }}>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="itemType"
|
||||||
|
checked={formData.itemType === 'Service'}
|
||||||
|
onChange={() => handleChange('itemType', 'Service')}
|
||||||
|
style={{ accentColor: 'var(--color-gold)' }}
|
||||||
|
/> Service
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.gridThree}>
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<label className={styles.label}>Item Category</label>
|
||||||
|
<select className={styles.select} value={formData.category} onChange={e => handleChange('category', e.target.value)}>
|
||||||
|
<option value="Default Group">Default Group</option>
|
||||||
|
<option value="Electronics">Electronics</option>
|
||||||
|
<option value="Services">Services</option>
|
||||||
|
<option value="Raw Material">Raw Material</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<label className={styles.label}>Unit</label>
|
||||||
|
<select className={styles.select} value={formData.unit} onChange={e => handleChange('unit', e.target.value)}>
|
||||||
|
<option value="PCS">PCS - Pieces</option>
|
||||||
|
<option value="KG">KG - Kilograms</option>
|
||||||
|
<option value="NOS">NOS - Numbers</option>
|
||||||
|
<option value="BOX">BOX - Boxes</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<label className={styles.label}>Tax Rate (%)</label>
|
||||||
|
<select className={styles.select} value={formData.taxRate} onChange={e => handleChange('taxRate', Number(e.target.value))}>
|
||||||
|
<option value={5}>5%</option>
|
||||||
|
<option value={12}>12%</option>
|
||||||
|
<option value={18}>18%</option>
|
||||||
|
<option value={28}>28%</option>
|
||||||
|
<option value={0}>0%</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<label className={styles.label}>Description</label>
|
||||||
|
<textarea
|
||||||
|
className={styles.input}
|
||||||
|
style={{ minHeight: '80px', resize: 'vertical' }}
|
||||||
|
value={formData.description}
|
||||||
|
onChange={e => handleChange('description', e.target.value)}
|
||||||
|
placeholder="Add product description..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Pricing Details */}
|
||||||
|
<h3 style={{ color: 'var(--color-gold)', marginTop: '1.5rem', marginBottom: '1rem', borderBottom: '1px solid rgba(255,255,255,0.1)', paddingBottom: '0.5rem' }}>Pricing Details</h3>
|
||||||
|
|
||||||
|
<div className={styles.gridTwo} style={{ gap: '2rem' }}>
|
||||||
|
{/* Sale Info */}
|
||||||
|
<div style={{ background: 'rgba(255,255,255,0.02)', padding: '1rem', borderRadius: '8px' }}>
|
||||||
|
<h4 style={{ color: '#aaa', marginBottom: '1rem' }}>Sale Information</h4>
|
||||||
|
<div className={styles.gridTwo}>
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<label className={styles.label}>MRP</label>
|
||||||
|
<input type="number" className={styles.input} value={formData.mrp} onChange={e => handleChange('mrp', Number(e.target.value))} />
|
||||||
|
</div>
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<label className={styles.label}>Sale Price</label>
|
||||||
|
<input type="number" className={styles.input} value={formData.salePrice} onChange={e => handleChange('salePrice', Number(e.target.value))} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Purchase Info */}
|
||||||
|
<div style={{ background: 'rgba(255,255,255,0.02)', padding: '1rem', borderRadius: '8px' }}>
|
||||||
|
<h4 style={{ color: '#aaa', marginBottom: '1rem' }}>Purchase Information</h4>
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<label className={styles.label}>Purchase Price</label>
|
||||||
|
<input type="number" className={styles.input} value={formData.purchasePrice} onChange={e => handleChange('purchasePrice', Number(e.target.value))} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Opening Stock */}
|
||||||
|
{formData.itemType === 'Goods' && (
|
||||||
|
<>
|
||||||
|
<h3 style={{ color: 'var(--color-gold)', marginTop: '1.5rem', marginBottom: '1rem', borderBottom: '1px solid rgba(255,255,255,0.1)', paddingBottom: '0.5rem' }}>Opening Stock Details</h3>
|
||||||
|
<div className={styles.gridThree}>
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<label className={styles.label}>Quantity</label>
|
||||||
|
<input type="number" className={styles.input} value={formData.openingQty} onChange={e => handleChange('openingQty', Number(e.target.value))} />
|
||||||
|
</div>
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<label className={styles.label}>Rate per Unit</label>
|
||||||
|
<input type="number" className={styles.input} value={formData.openingRate} onChange={e => handleChange('openingRate', Number(e.target.value))} />
|
||||||
|
</div>
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<label className={styles.label}>Total Value</label>
|
||||||
|
<div className={styles.input} style={{ background: 'rgba(0,0,0,0.2)', border: 'none' }}>
|
||||||
|
₹ {(formData.openingQty * formData.openingRate).toFixed(2)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.modalFooter} style={{
|
||||||
|
borderTop: '1px solid rgba(255,215,0,0.1)',
|
||||||
|
padding: '1.5rem',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
gap: '1rem',
|
||||||
|
flexShrink: 0
|
||||||
|
}}>
|
||||||
|
<button className={styles.btnSecondary} onClick={onClose}>Cancel</button>
|
||||||
|
<button className={styles.btnPrimary} onClick={handleSubmit}>Save Item</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
460
src/app/dashboard/accounts/components/InvoiceSettingsModal.tsx
Normal file
@@ -0,0 +1,460 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
// Define the comprehensive settings type
|
||||||
|
export interface InvoiceSettings {
|
||||||
|
general: {
|
||||||
|
transactionType: 'income' | 'expense';
|
||||||
|
pdfFormat: 'a4' | 'letter';
|
||||||
|
fitToPage: boolean;
|
||||||
|
colorTheme: string;
|
||||||
|
};
|
||||||
|
header: {
|
||||||
|
companyNameSize: number;
|
||||||
|
companyAddressSize: number;
|
||||||
|
headerLabelsSize: number;
|
||||||
|
headerContentsSize: number;
|
||||||
|
showCompanyInfo: boolean;
|
||||||
|
};
|
||||||
|
table: {
|
||||||
|
tableHeadingsSize: number;
|
||||||
|
tableContentsSize: number;
|
||||||
|
descriptionSize: number;
|
||||||
|
totalSize: number;
|
||||||
|
showCurrencySymbol: boolean;
|
||||||
|
showItemImage: boolean;
|
||||||
|
showSKU: boolean;
|
||||||
|
labels: {
|
||||||
|
sn: string;
|
||||||
|
itemName: string;
|
||||||
|
qty: string;
|
||||||
|
rate: string;
|
||||||
|
discount: string;
|
||||||
|
taxableValue: string;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
footer: {
|
||||||
|
footerHeadingsSize: number;
|
||||||
|
footerContentsSize: number;
|
||||||
|
termsSize: number;
|
||||||
|
noteSize: number;
|
||||||
|
showSignature: boolean;
|
||||||
|
showBankDetails: boolean;
|
||||||
|
showTerms: boolean;
|
||||||
|
showNotes: boolean;
|
||||||
|
showAuthorizedSignatory: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DEFAULT_INVOICE_SETTINGS: InvoiceSettings = {
|
||||||
|
general: {
|
||||||
|
transactionType: 'income',
|
||||||
|
pdfFormat: 'a4',
|
||||||
|
fitToPage: true,
|
||||||
|
colorTheme: '#4f158c'
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
companyNameSize: 24,
|
||||||
|
companyAddressSize: 13,
|
||||||
|
headerLabelsSize: 13,
|
||||||
|
headerContentsSize: 13,
|
||||||
|
showCompanyInfo: true
|
||||||
|
},
|
||||||
|
table: {
|
||||||
|
tableHeadingsSize: 13,
|
||||||
|
tableContentsSize: 12,
|
||||||
|
descriptionSize: 12,
|
||||||
|
totalSize: 13,
|
||||||
|
showCurrencySymbol: true,
|
||||||
|
showItemImage: true,
|
||||||
|
showSKU: true,
|
||||||
|
labels: {
|
||||||
|
sn: 'SN',
|
||||||
|
itemName: 'Item Name',
|
||||||
|
qty: 'Qty',
|
||||||
|
rate: 'Rate',
|
||||||
|
discount: 'Discount',
|
||||||
|
taxableValue: 'Taxable Value'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
footerHeadingsSize: 13,
|
||||||
|
footerContentsSize: 13,
|
||||||
|
termsSize: 12,
|
||||||
|
noteSize: 12,
|
||||||
|
showSignature: false,
|
||||||
|
showBankDetails: true,
|
||||||
|
showTerms: true,
|
||||||
|
showNotes: true,
|
||||||
|
showAuthorizedSignatory: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
interface InvoiceSettingsModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
currentSettings: InvoiceSettings;
|
||||||
|
onSave: (settings: InvoiceSettings) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Tab = 'general' | 'header' | 'table' | 'footer';
|
||||||
|
|
||||||
|
export default function InvoiceSettingsModal({ isOpen, onClose, currentSettings, onSave }: InvoiceSettingsModalProps) {
|
||||||
|
const [settings, setSettings] = useState<InvoiceSettings>(currentSettings);
|
||||||
|
const [activeTab, setActiveTab] = useState<Tab>('general');
|
||||||
|
|
||||||
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
onSave(settings);
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper to update state deeply
|
||||||
|
const updateSetting = (section: keyof InvoiceSettings, key: string, value: any) => {
|
||||||
|
setSettings(prev => ({
|
||||||
|
...prev,
|
||||||
|
[section]: {
|
||||||
|
...prev[section],
|
||||||
|
[key]: value
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper for nested labels update
|
||||||
|
const updateTableLabel = (key: string, value: string) => {
|
||||||
|
setSettings(prev => ({
|
||||||
|
...prev,
|
||||||
|
table: {
|
||||||
|
...prev.table,
|
||||||
|
labels: {
|
||||||
|
...prev.table.labels,
|
||||||
|
[key]: value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
position: 'fixed',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
background: 'rgba(0, 0, 0, 0.8)',
|
||||||
|
backdropFilter: 'blur(8px)',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
zIndex: 2000
|
||||||
|
}} onClick={onClose}>
|
||||||
|
<div style={{
|
||||||
|
width: '800px',
|
||||||
|
height: '80vh',
|
||||||
|
background: 'linear-gradient(135deg, #051a15 0%, #0a2e25 100%)',
|
||||||
|
borderRadius: '16px',
|
||||||
|
boxShadow: '0 25px 50px -12px rgba(0, 0, 0, 0.5)',
|
||||||
|
border: '1px solid rgba(255, 215, 0, 0.1)',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
overflow: 'hidden',
|
||||||
|
color: '#e0e0e0'
|
||||||
|
}} onClick={e => e.stopPropagation()}>
|
||||||
|
|
||||||
|
{/* Header */}
|
||||||
|
<div style={{
|
||||||
|
padding: '1.25rem 2rem',
|
||||||
|
borderBottom: '1px solid rgba(255, 255, 255, 0.05)',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
background: 'rgba(0,0,0,0.2)'
|
||||||
|
}}>
|
||||||
|
<h2 style={{ margin: 0, fontSize: '1.4rem', color: 'var(--color-gold)', textShadow: '0 2px 4px rgba(0,0,0,0.3)' }}>
|
||||||
|
{activeTab.charAt(0).toUpperCase() + activeTab.slice(1)} Settings
|
||||||
|
</h2>
|
||||||
|
<div style={{ display: 'flex', gap: '1rem' }}>
|
||||||
|
<button onClick={handleSave} style={{
|
||||||
|
background: 'linear-gradient(135deg, #FFD700 0%, #D4AF37 100%)',
|
||||||
|
color: '#051a15',
|
||||||
|
border: 'none',
|
||||||
|
padding: '10px 24px',
|
||||||
|
borderRadius: '8px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
boxShadow: '0 4px 6px rgba(0,0,0,0.2)',
|
||||||
|
transition: 'transform 0.2s',
|
||||||
|
fontSize: '0.95rem'
|
||||||
|
}}>Save Changes</button>
|
||||||
|
<button onClick={onClose} style={{
|
||||||
|
background: 'rgba(255, 255, 255, 0.05)',
|
||||||
|
border: '1px solid rgba(255, 255, 255, 0.1)',
|
||||||
|
borderRadius: '50%',
|
||||||
|
width: '40px',
|
||||||
|
height: '40px',
|
||||||
|
fontSize: '1.5rem',
|
||||||
|
cursor: 'pointer',
|
||||||
|
color: '#e0e0e0',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
transition: 'background 0.2s'
|
||||||
|
}}>×</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Body */}
|
||||||
|
<div style={{ flex: 1, display: 'flex', overflow: 'hidden' }}>
|
||||||
|
|
||||||
|
{/* Sidebar */}
|
||||||
|
<div style={{
|
||||||
|
width: '220px',
|
||||||
|
background: 'rgba(0,0,0,0.2)',
|
||||||
|
borderRight: '1px solid rgba(255, 255, 255, 0.05)',
|
||||||
|
padding: '1.5rem 0'
|
||||||
|
}}>
|
||||||
|
{(['general', 'header', 'table', 'footer'] as Tab[]).map(tab => (
|
||||||
|
<div
|
||||||
|
key={tab}
|
||||||
|
onClick={() => setActiveTab(tab)}
|
||||||
|
style={{
|
||||||
|
padding: '16px 24px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
color: activeTab === tab ? '#FFD700' : '#888',
|
||||||
|
background: activeTab === tab ? 'rgba(255, 215, 0, 0.05)' : 'transparent',
|
||||||
|
borderLeft: activeTab === tab ? '3px solid #FFD700' : '3px solid transparent',
|
||||||
|
fontWeight: activeTab === tab ? '600' : 'normal',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
transition: 'all 0.2s'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span style={{ fontSize: '1rem' }}>{tab.charAt(0).toUpperCase() + tab.slice(1)}</span>
|
||||||
|
{activeTab === tab && <span style={{ fontSize: '1.2rem' }}>›</span>}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div style={{ flex: 1, padding: '2.5rem', overflowY: 'auto' }}>
|
||||||
|
|
||||||
|
{/* GENERAL TAB */}
|
||||||
|
{activeTab === 'general' && (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '2rem' }}>
|
||||||
|
<div className="setting-group">
|
||||||
|
<label style={{ display: 'block', fontSize: '0.9rem', color: '#888', marginBottom: '8px' }}>Transaction Type</label>
|
||||||
|
<div style={{ border: '1px solid rgba(255,255,255,0.1)', borderRadius: '8px', padding: '10px', background: 'rgba(255,255,255,0.02)' }}>
|
||||||
|
<select
|
||||||
|
value={settings.general.transactionType}
|
||||||
|
onChange={(e) => updateSetting('general', 'transactionType', e.target.value)}
|
||||||
|
style={{ width: '100%', border: 'none', outline: 'none', background: 'transparent', color: '#e0e0e0', fontSize: '1rem' }}
|
||||||
|
>
|
||||||
|
<option value="income" style={{ color: '#000' }}>Income Transaction</option>
|
||||||
|
<option value="expense" style={{ color: '#000' }}>Expense Transaction</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="setting-group">
|
||||||
|
<label style={{ display: 'block', fontSize: '0.9rem', color: '#888', marginBottom: '8px' }}>PDF Format</label>
|
||||||
|
<div style={{ border: '1px solid rgba(255,255,255,0.1)', borderRadius: '8px', padding: '10px', background: 'rgba(255,255,255,0.02)' }}>
|
||||||
|
<select
|
||||||
|
value={settings.general.pdfFormat}
|
||||||
|
onChange={(e) => updateSetting('general', 'pdfFormat', e.target.value)}
|
||||||
|
style={{ width: '100%', border: 'none', outline: 'none', background: 'transparent', color: '#e0e0e0', fontSize: '1rem' }}
|
||||||
|
>
|
||||||
|
<option value="a4" style={{ color: '#000' }}>A4</option>
|
||||||
|
<option value="letter" style={{ color: '#000' }}>Letter</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', background: 'rgba(255,255,255,0.03)', padding: '1.2rem', borderRadius: '12px', border: '1px solid rgba(255,255,255,0.05)' }}>
|
||||||
|
<span style={{ fontWeight: 500, color: '#e0e0e0' }}>Fit To Page</span>
|
||||||
|
<ToggleSwitch
|
||||||
|
checked={settings.general.fitToPage}
|
||||||
|
onChange={(v) => updateSetting('general', 'fitToPage', v)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="setting-group">
|
||||||
|
<label style={{ display: 'block', fontSize: '0.9rem', color: '#888', marginBottom: '8px' }}>Color Theme</label>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '8px', padding: '10px', background: 'rgba(255,255,255,0.02)' }}>
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
value={settings.general.colorTheme}
|
||||||
|
onChange={(e) => updateSetting('general', 'colorTheme', e.target.value)}
|
||||||
|
style={{ border: 'none', width: '32px', height: '32px', padding: 0, background: 'none', cursor: 'pointer' }}
|
||||||
|
/>
|
||||||
|
<span style={{ fontSize: '0.9rem', fontFamily: 'monospace', color: '#aaa' }}>{settings.general.colorTheme}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* HEADER TAB */}
|
||||||
|
{activeTab === 'header' && (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}>
|
||||||
|
<FontSizeControl label="Company Name" value={settings.header.companyNameSize} onChange={(v) => updateSetting('header', 'companyNameSize', v)} />
|
||||||
|
<FontSizeControl label="Company Address" value={settings.header.companyAddressSize} onChange={(v) => updateSetting('header', 'companyAddressSize', v)} />
|
||||||
|
<FontSizeControl label="Header Labels" value={settings.header.headerLabelsSize} onChange={(v) => updateSetting('header', 'headerLabelsSize', v)} />
|
||||||
|
<FontSizeControl label="Header Contents" value={settings.header.headerContentsSize} onChange={(v) => updateSetting('header', 'headerContentsSize', v)} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* TABLE TAB */}
|
||||||
|
{activeTab === 'table' && (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '2rem' }}>
|
||||||
|
<div style={{ marginBottom: '1rem' }}>
|
||||||
|
<h3 style={{ fontSize: '1rem', color: 'var(--color-gold)', marginBottom: '1.2rem', fontWeight: 600, borderBottom: '1px solid rgba(255,215,0,0.1)', paddingBottom: '0.5rem', display: 'inline-block' }}>Show / Hide</h3>
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
|
||||||
|
<ToggleRow label="Currency Symbol" checked={settings.table.showCurrencySymbol} onChange={(v) => updateSetting('table', 'showCurrencySymbol', v)} />
|
||||||
|
<ToggleRow label="Item Image" checked={settings.table.showItemImage} onChange={(v) => updateSetting('table', 'showItemImage', v)} />
|
||||||
|
<ToggleRow label="SKU (Item Code)" checked={settings.table.showSKU} onChange={(v) => updateSetting('table', 'showSKU', v)} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ marginBottom: '1rem' }}>
|
||||||
|
<h3 style={{ fontSize: '1rem', color: 'var(--color-gold)', marginBottom: '1.2rem', fontWeight: 600, borderBottom: '1px solid rgba(255,215,0,0.1)', paddingBottom: '0.5rem', display: 'inline-block' }}>Table Headers</h3>
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1.5rem' }}>
|
||||||
|
<InputBox value={settings.table.labels.sn} onChange={(v) => updateTableLabel('sn', v)} label="SN Label" />
|
||||||
|
<InputBox value={settings.table.labels.itemName} onChange={(v) => updateTableLabel('itemName', v)} label="Item Name Label" />
|
||||||
|
<InputBox value={settings.table.labels.qty} onChange={(v) => updateTableLabel('qty', v)} label="Quantity Label" />
|
||||||
|
<InputBox value={settings.table.labels.rate} onChange={(v) => updateTableLabel('rate', v)} label="Rate Label" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 style={{ fontSize: '1rem', color: 'var(--color-gold)', marginBottom: '1.2rem', fontWeight: 600, borderBottom: '1px solid rgba(255,215,0,0.1)', paddingBottom: '0.5rem', display: 'inline-block' }}>Font Sizes</h3>
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}>
|
||||||
|
<FontSizeControl label="Table Headings" value={settings.table.tableHeadingsSize} onChange={(v) => updateSetting('table', 'tableHeadingsSize', v)} />
|
||||||
|
<FontSizeControl label="Table Contents" value={settings.table.tableContentsSize} onChange={(v) => updateSetting('table', 'tableContentsSize', v)} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* FOOTER TAB */}
|
||||||
|
{activeTab === 'footer' && (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '2rem' }}>
|
||||||
|
<div style={{ marginBottom: '1rem' }}>
|
||||||
|
<h3 style={{ fontSize: '1rem', color: 'var(--color-gold)', marginBottom: '1.2rem', fontWeight: 600, borderBottom: '1px solid rgba(255,215,0,0.1)', paddingBottom: '0.5rem', display: 'inline-block' }}>Show / Hide</h3>
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
|
||||||
|
<ToggleRow label="Signature" checked={settings.footer.showSignature} onChange={(v) => updateSetting('footer', 'showSignature', v)} />
|
||||||
|
<ToggleRow label="Bank Details" checked={settings.footer.showBankDetails} onChange={(v) => updateSetting('footer', 'showBankDetails', v)} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ marginBottom: '1rem' }}>
|
||||||
|
<h3 style={{ fontSize: '1rem', color: 'var(--color-gold)', marginBottom: '1.2rem', fontWeight: 600, borderBottom: '1px solid rgba(255,215,0,0.1)', paddingBottom: '0.5rem', display: 'inline-block' }}>Footer Labels</h3>
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1.5rem' }}>
|
||||||
|
<div style={{ border: '1px solid rgba(255,255,255,0.1)', background: 'rgba(255,255,255,0.02)', borderRadius: '8px', padding: '12px 16px', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
|
<span style={{ color: '#e0e0e0' }}>Terms And Conditions</span>
|
||||||
|
<input type="checkbox" checked={settings.footer.showTerms} onChange={(e) => updateSetting('footer', 'showTerms', e.target.checked)} style={{ accentColor: '#FFD700' }} />
|
||||||
|
</div>
|
||||||
|
<div style={{ border: '1px solid rgba(255,255,255,0.1)', background: 'rgba(255,255,255,0.02)', borderRadius: '8px', padding: '12px 16px', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
|
<span style={{ color: '#e0e0e0' }}>Notes</span>
|
||||||
|
<input type="checkbox" checked={settings.footer.showNotes} onChange={(e) => updateSetting('footer', 'showNotes', e.target.checked)} style={{ accentColor: '#FFD700' }} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 style={{ fontSize: '1rem', color: 'var(--color-gold)', marginBottom: '1.2rem', fontWeight: 600, borderBottom: '1px solid rgba(255,215,0,0.1)', paddingBottom: '0.5rem', display: 'inline-block' }}>Font Sizes</h3>
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}>
|
||||||
|
<FontSizeControl label="Footer Headings" value={settings.footer.footerHeadingsSize} onChange={(v) => updateSetting('footer', 'footerHeadingsSize', v)} />
|
||||||
|
<FontSizeControl label="Footer Contents" value={settings.footer.footerContentsSize} onChange={(v) => updateSetting('footer', 'footerContentsSize', v)} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Sub Components ---
|
||||||
|
|
||||||
|
const ToggleSwitch = ({ checked, onChange }: { checked: boolean; onChange: (v: boolean) => void }) => (
|
||||||
|
<div
|
||||||
|
onClick={() => onChange(!checked)}
|
||||||
|
style={{
|
||||||
|
width: '44px',
|
||||||
|
height: '24px',
|
||||||
|
background: checked ? 'linear-gradient(135deg, #FFD700 0%, #B8860B 100%)' : 'rgba(255,255,255,0.1)',
|
||||||
|
borderRadius: '24px',
|
||||||
|
position: 'relative',
|
||||||
|
cursor: 'pointer',
|
||||||
|
transition: 'background 0.3s',
|
||||||
|
border: checked ? 'none' : '1px solid rgba(255,255,255,0.2)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: '3px',
|
||||||
|
left: checked ? '23px' : '3px',
|
||||||
|
width: '18px',
|
||||||
|
height: '18px',
|
||||||
|
background: '#fff',
|
||||||
|
borderRadius: '50%',
|
||||||
|
transition: 'left 0.3s cubic-bezier(0.4, 0.0, 0.2, 1)',
|
||||||
|
boxShadow: '0 2px 4px rgba(0,0,0,0.2)'
|
||||||
|
}} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const ToggleRow = ({ label, checked, onChange }: { label: string; checked: boolean; onChange: (v: boolean) => void }) => (
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '0.5rem 0' }}>
|
||||||
|
<span style={{ fontWeight: 500, color: '#e0e0e0' }}>{label}</span>
|
||||||
|
<ToggleSwitch checked={checked} onChange={onChange} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const FontSizeControl = ({ label, value, onChange }: { label: string; value: number; onChange: (v: number) => void }) => (
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem', background: 'rgba(255,255,255,0.02)', padding: '0.8rem', borderRadius: '8px', border: '1px solid rgba(255,255,255,0.05)' }}>
|
||||||
|
<span style={{ flex: 1, fontWeight: 500, color: '#e0e0e0' }}>{label}</span>
|
||||||
|
<div style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
border: '1px solid rgba(255,215,0,0.3)',
|
||||||
|
borderRadius: '6px',
|
||||||
|
padding: '6px 12px',
|
||||||
|
gap: '8px',
|
||||||
|
background: 'rgba(5, 26, 21, 0.5)'
|
||||||
|
}}>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => onChange(Number(e.target.value))}
|
||||||
|
style={{ width: '40px', border: 'none', outline: 'none', background: 'transparent', color: '#FFD700', textAlign: 'right', fontWeight: 'bold' }}
|
||||||
|
/>
|
||||||
|
<span style={{ color: '#888', fontSize: '0.8rem', borderLeft: '1px solid rgba(255,255,255,0.2)', paddingLeft: '8px' }}>PX</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const InputBox = ({ value, onChange, label }: { value: string; onChange: (v: string) => void, label?: string }) => (
|
||||||
|
<div style={{ width: '100%' }}>
|
||||||
|
{label && <label style={{ display: 'block', fontSize: '0.8rem', color: '#999', marginBottom: '6px' }}>{label}</label>}
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
padding: '10px 12px',
|
||||||
|
border: '1px solid rgba(136, 136, 136, 0.49)',
|
||||||
|
borderRadius: '6px',
|
||||||
|
outline: 'none',
|
||||||
|
background: 'rgba(255, 255, 255, 0.05)',
|
||||||
|
color: '#fff',
|
||||||
|
transition: 'border-color 0.2s',
|
||||||
|
fontFamily: 'inherit'
|
||||||
|
}}
|
||||||
|
onFocus={(e) => e.target.style.borderColor = 'var(--color-gold)'}
|
||||||
|
onBlur={(e) => e.target.style.borderColor = 'rgba(255, 255, 255, 0.2)'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
842
src/app/dashboard/accounts/invoices/page.tsx
Normal file
@@ -0,0 +1,842 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import styles from '../accounts.module.css';
|
||||||
|
import { generateInvoicePDF } from '@/utils/pdfGenerator';
|
||||||
|
import AddCustomerModal from '../components/AddCustomerModal';
|
||||||
|
import AddItemModal from '../components/AddItemModal';
|
||||||
|
import InvoiceSettingsModal, { DEFAULT_INVOICE_SETTINGS, InvoiceSettings } from '../components/InvoiceSettingsModal';
|
||||||
|
import Toast from '@/components/dms/Toast';
|
||||||
|
|
||||||
|
// --- Constants & Helpers ---
|
||||||
|
const INDIAN_STATES = [
|
||||||
|
"Andhra Pradesh", "Arunachal Pradesh", "Assam", "Bihar", "Chhattisgarh", "Goa", "Gujarat",
|
||||||
|
"Haryana", "Himachal Pradesh", "Jharkhand", "Karnataka", "Kerala", "Madhya Pradesh", "Maharashtra",
|
||||||
|
"Manipur", "Meghalaya", "Mizoram", "Nagaland", "Odisha", "Punjab", "Rajasthan", "Sikkim",
|
||||||
|
"Tamil Nadu", "Telangana", "Tripura", "Uttar Pradesh", "Uttarakhand", "West Bengal",
|
||||||
|
"Andaman and Nicobar Islands", "Chandigarh", "Dadra and Nagar Haveli and Daman and Diu",
|
||||||
|
"Delhi", "Jammu and Kashmir", "Ladakh", "Lakshadweep", "Puducherry"
|
||||||
|
];
|
||||||
|
|
||||||
|
const GST_RATES = [0, 5, 12, 18, 28];
|
||||||
|
|
||||||
|
const numberToWords = (num: number): string => {
|
||||||
|
const a = ['', 'one ', 'two ', 'three ', 'four ', 'five ', 'six ', 'seven ', 'eight ', 'nine ', 'ten ', 'eleven ', 'twelve ', 'thirteen ', 'fourteen ', 'fifteen ', 'sixteen ', 'seventeen ', 'eighteen ', 'nineteen '];
|
||||||
|
const b = ['', '', 'twenty', 'thirty', 'forty', 'fifty', 'sixty', 'seventy', 'eighty', 'ninety'];
|
||||||
|
|
||||||
|
const val = num.toString();
|
||||||
|
if (val.length > 9) return 'overflow';
|
||||||
|
const n = ('000000000' + val).substr(-9).match(/^(\d{2})(\d{2})(\d{2})(\d{1})(\d{2})$/);
|
||||||
|
if (!n) return '';
|
||||||
|
let str = '';
|
||||||
|
str += (n[1] != '00') ? (a[Number(n[1])] || b[Number(n[1][0])] + ' ' + a[Number(n[1][1])]) + 'crore ' : '';
|
||||||
|
str += (n[2] != '00') ? (a[Number(n[2])] || b[Number(n[2][0])] + ' ' + a[Number(n[2][1])]) + 'lakh ' : '';
|
||||||
|
str += (n[3] != '00') ? (a[Number(n[3])] || b[Number(n[3][0])] + ' ' + a[Number(n[3][1])]) + 'thousand ' : '';
|
||||||
|
str += (n[4] != '0') ? (a[Number(n[4])] || b[Number(n[4][0])] + ' ' + a[Number(n[4][1])]) + 'hundred ' : '';
|
||||||
|
str += (n[5] != '00') ? ((str != '') ? 'and ' : '') + (a[Number(n[5])] || b[Number(n[5][0])] + ' ' + a[Number(n[5][1])]) + '' : '';
|
||||||
|
return str.trim();
|
||||||
|
};
|
||||||
|
|
||||||
|
const COMPANY_STATE = "Maharashtra";
|
||||||
|
|
||||||
|
interface InvoiceItem {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
hsn: string;
|
||||||
|
quantity: number;
|
||||||
|
unit: string;
|
||||||
|
rate: number;
|
||||||
|
discount: number;
|
||||||
|
gstRate: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Invoice {
|
||||||
|
id: string;
|
||||||
|
customer: string;
|
||||||
|
date: string;
|
||||||
|
total: number;
|
||||||
|
status: string;
|
||||||
|
items?: { description: string, amount: number }[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock Data
|
||||||
|
const customers = [
|
||||||
|
{ id: '1', name: 'Alpha Corp' },
|
||||||
|
{ id: '2', name: 'Beta Ltd' },
|
||||||
|
{ id: '3', name: 'Gamma Inc' },
|
||||||
|
{ id: '4', name: 'Delta Solutions' },
|
||||||
|
{ id: '5', name: 'Zeta Industries' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const PRODUCTS_SERVICES = [
|
||||||
|
{ name: 'Consulting Services', rate: 5000, tax: 18 },
|
||||||
|
{ name: 'Web Development', rate: 15000, tax: 18 },
|
||||||
|
{ name: 'Server Maintenance', rate: 8000, tax: 18 },
|
||||||
|
{ name: 'Domain Registration', rate: 1200, tax: 18 },
|
||||||
|
{ name: 'Software License', rate: 25000, tax: 18 },
|
||||||
|
{ name: 'SEO Optimization', rate: 6000, tax: 18 },
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function InvoicesPage() {
|
||||||
|
const [view, setView] = useState<'list' | 'create'>('list');
|
||||||
|
const [toast, setToast] = useState<{ message: string; type: 'success' | 'error' | 'info' } | null>(null);
|
||||||
|
const [selectedInvoice, setSelectedInvoice] = useState<Invoice | null>(null);
|
||||||
|
const [showModal, setShowModal] = useState(false);
|
||||||
|
|
||||||
|
// Create Invoice State
|
||||||
|
const [invoiceNo, setInvoiceNo] = useState('INV-2024-001');
|
||||||
|
const [date, setDate] = useState(new Date().toISOString().split('T')[0]);
|
||||||
|
const [dueDate, setDueDate] = useState(new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString().split('T')[0]);
|
||||||
|
const [customer, setCustomer] = useState('');
|
||||||
|
const [customerGstin, setCustomerGstin] = useState('');
|
||||||
|
const [placeOfSupply, setPlaceOfSupply] = useState(COMPANY_STATE);
|
||||||
|
const [invoiceType, setInvoiceType] = useState<'B2B' | 'B2C'>('B2B');
|
||||||
|
const [items, setItems] = useState<InvoiceItem[]>([
|
||||||
|
{ id: 1, name: '', description: '', hsn: '', quantity: 1, unit: 'Nos', rate: 0, discount: 0, gstRate: 18 }
|
||||||
|
]);
|
||||||
|
const [notes, setNotes] = useState('Thank you for your business.');
|
||||||
|
const [terms, setTerms] = useState('Payment due within 15 days.');
|
||||||
|
|
||||||
|
// Advanced Fields
|
||||||
|
const [discountType, setDiscountType] = useState<'flat' | 'percent'>('flat');
|
||||||
|
const [discountValue, setDiscountValue] = useState(0);
|
||||||
|
const [tdsRate, setTdsRate] = useState<number>(0);
|
||||||
|
const [roundOff, setRoundOff] = useState(false);
|
||||||
|
|
||||||
|
// Customer Search & Modal State
|
||||||
|
const [isCustomerDropdownOpen, setIsCustomerDropdownOpen] = useState(false);
|
||||||
|
const [customerSearchTerm, setCustomerSearchTerm] = useState('');
|
||||||
|
const [showAddCustomerModal, setShowAddCustomerModal] = useState(false);
|
||||||
|
|
||||||
|
// Item Search & Modal State
|
||||||
|
const [activeItemDropdownId, setActiveItemDropdownId] = useState<number | null>(null);
|
||||||
|
const [showAddItemModal, setShowAddItemModal] = useState(false);
|
||||||
|
const [pendingItemIndex, setPendingItemIndex] = useState<number | null>(null);
|
||||||
|
|
||||||
|
// Invoice Settings
|
||||||
|
const [showSettingsModal, setShowSettingsModal] = useState(false);
|
||||||
|
const [invoiceSettings, setInvoiceSettings] = useState<InvoiceSettings>(DEFAULT_INVOICE_SETTINGS);
|
||||||
|
|
||||||
|
// Customer Handlers
|
||||||
|
const filteredCustomers = customers.filter(c =>
|
||||||
|
c.name.toLowerCase().includes(customerSearchTerm.toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSelectCustomer = (custName: string) => {
|
||||||
|
setCustomer(custName);
|
||||||
|
setCustomerSearchTerm(custName);
|
||||||
|
setIsCustomerDropdownOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddCustomer = () => {
|
||||||
|
setIsCustomerDropdownOpen(false);
|
||||||
|
setShowAddCustomerModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveNewCustomer = (customerData: any) => {
|
||||||
|
const newId = `cust${customers.length + 1}`;
|
||||||
|
customers.push({ id: newId, name: customerData.ledgerName });
|
||||||
|
handleSelectCustomer(customerData.ledgerName);
|
||||||
|
setShowAddCustomerModal(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Item Handlers
|
||||||
|
const handleSelectItem = (index: number, product: typeof PRODUCTS_SERVICES[0]) => {
|
||||||
|
const newItems = [...items];
|
||||||
|
newItems[index].name = product.name;
|
||||||
|
newItems[index].rate = product.rate;
|
||||||
|
newItems[index].gstRate = product.tax;
|
||||||
|
setItems(newItems);
|
||||||
|
setActiveItemDropdownId(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddItem = (index: number) => {
|
||||||
|
setPendingItemIndex(index);
|
||||||
|
setShowAddItemModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveNewItem = (itemData: any) => {
|
||||||
|
PRODUCTS_SERVICES.push({
|
||||||
|
name: itemData.itemName,
|
||||||
|
rate: itemData.salePrice || 0,
|
||||||
|
tax: itemData.taxRate || 18
|
||||||
|
});
|
||||||
|
|
||||||
|
if (pendingItemIndex !== null) {
|
||||||
|
handleSelectItem(pendingItemIndex, {
|
||||||
|
name: itemData.itemName,
|
||||||
|
rate: itemData.salePrice || 0,
|
||||||
|
tax: itemData.taxRate || 18
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setShowAddItemModal(false);
|
||||||
|
setPendingItemIndex(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mock Data for List
|
||||||
|
const invoices: Invoice[] = [
|
||||||
|
{
|
||||||
|
id: 'INV-001',
|
||||||
|
customer: 'Alpha Corp',
|
||||||
|
date: '2024-11-20',
|
||||||
|
total: 45000,
|
||||||
|
status: 'Paid',
|
||||||
|
items: [
|
||||||
|
{ description: 'Consulting Services (Phase 1)', amount: 15000 },
|
||||||
|
{ description: 'Consulting Services (Phase 2)', amount: 15000 },
|
||||||
|
{ description: 'Server Maintenance (Q3)', amount: 10000 },
|
||||||
|
{ description: 'Software License Fees', amount: 5000 },
|
||||||
|
{ description: 'Additional Support Hrs', amount: 5000 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'INV-002',
|
||||||
|
customer: 'Beta Ltd',
|
||||||
|
date: '2024-11-22',
|
||||||
|
total: 12000,
|
||||||
|
status: 'Pending',
|
||||||
|
items: [
|
||||||
|
{ description: 'Web Development Advance', amount: 12000 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'INV-003',
|
||||||
|
customer: 'Gamma Inc',
|
||||||
|
date: '2024-11-25',
|
||||||
|
total: 8500,
|
||||||
|
status: 'Overdue',
|
||||||
|
items: Array.from({ length: 15 }).map((_, i) => ({ description: `Monthly Retainer Service - Item ${i + 1}`, amount: 500 }))
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const calculateTotals = () => {
|
||||||
|
let subtotal = 0;
|
||||||
|
let totalTaxable = 0;
|
||||||
|
let totalIGST = 0;
|
||||||
|
let totalCGST = 0;
|
||||||
|
let totalSGST = 0;
|
||||||
|
let totalDiscount = 0;
|
||||||
|
|
||||||
|
items.forEach(item => {
|
||||||
|
const amount = item.quantity * item.rate;
|
||||||
|
const taxable = Math.max(0, amount - item.discount);
|
||||||
|
const taxAmount = (taxable * item.gstRate) / 100;
|
||||||
|
|
||||||
|
subtotal += amount;
|
||||||
|
totalDiscount += item.discount;
|
||||||
|
totalTaxable += taxable;
|
||||||
|
|
||||||
|
if (placeOfSupply !== COMPANY_STATE) {
|
||||||
|
totalIGST += taxAmount;
|
||||||
|
} else {
|
||||||
|
totalCGST += taxAmount / 2;
|
||||||
|
totalSGST += taxAmount / 2;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const totalTax = totalIGST + totalCGST + totalSGST;
|
||||||
|
const netAmount = totalTaxable + totalTax;
|
||||||
|
|
||||||
|
// TDS Calculation (Informational usually, or deducted if specified)
|
||||||
|
const tdsAmount = (totalTaxable * tdsRate) / 100;
|
||||||
|
|
||||||
|
let grandTotal = netAmount;
|
||||||
|
let roundOffAmount = 0;
|
||||||
|
|
||||||
|
if (roundOff) {
|
||||||
|
const rounded = Math.round(grandTotal);
|
||||||
|
roundOffAmount = rounded - grandTotal;
|
||||||
|
grandTotal = rounded;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
subtotal,
|
||||||
|
discount: totalDiscount,
|
||||||
|
taxable: totalTaxable,
|
||||||
|
igst: totalIGST,
|
||||||
|
cgst: totalCGST,
|
||||||
|
sgst: totalSGST,
|
||||||
|
totalTax,
|
||||||
|
tdsAmount,
|
||||||
|
roundOffAmount,
|
||||||
|
grandTotal,
|
||||||
|
amountInWords: numberToWords(Math.round(grandTotal))
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const totals = calculateTotals();
|
||||||
|
|
||||||
|
const addItemRow = () => {
|
||||||
|
setItems([...items, { id: items.length + 1, name: '', description: '', hsn: '', quantity: 1, unit: 'Nos', rate: 0, discount: 0, gstRate: 18 }]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleViewInvoice = (inv: Invoice) => {
|
||||||
|
setSelectedInvoice(inv);
|
||||||
|
setShowModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDownloadInvoice = (inv: Invoice, action: 'download' | 'print' = 'download') => {
|
||||||
|
const subtotal = inv.total / 1.18;
|
||||||
|
const tax = inv.total - subtotal;
|
||||||
|
|
||||||
|
const calculatedTotals = {
|
||||||
|
subtotal: subtotal,
|
||||||
|
discount: 0,
|
||||||
|
taxable: subtotal,
|
||||||
|
igst: 0,
|
||||||
|
cgst: tax / 2,
|
||||||
|
sgst: tax / 2,
|
||||||
|
totalTax: tax,
|
||||||
|
tdsAmount: 0,
|
||||||
|
roundOffAmount: 0,
|
||||||
|
grandTotal: inv.total,
|
||||||
|
amountInWords: numberToWords(Math.round(inv.total))
|
||||||
|
};
|
||||||
|
|
||||||
|
generateInvoicePDF({
|
||||||
|
invoiceNo: inv.id,
|
||||||
|
date: inv.date,
|
||||||
|
customer: inv.customer,
|
||||||
|
items: inv.items ? inv.items.map(i => ({
|
||||||
|
name: i.description,
|
||||||
|
quantity: 1,
|
||||||
|
unit: 'Nos',
|
||||||
|
rate: i.amount,
|
||||||
|
discount: 0,
|
||||||
|
gstRate: 18
|
||||||
|
})) : [],
|
||||||
|
totals: calculatedTotals,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
tdsRate: 0
|
||||||
|
}, action);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
|
||||||
|
{/* Header */}
|
||||||
|
<div className={styles.header}>
|
||||||
|
<div className={styles.titleGroup}>
|
||||||
|
<h1>Invoices</h1>
|
||||||
|
<p className={styles.subTitle}>Manage Sales & Purchase Invoices</p>
|
||||||
|
</div>
|
||||||
|
<div className={styles.controls}>
|
||||||
|
{view === 'list' ? (
|
||||||
|
<button className={styles.btnPrimary} onClick={() => setView('create')}>+ Create New Invoice</button>
|
||||||
|
) : (
|
||||||
|
<div style={{ display: 'flex', gap: '1rem' }}>
|
||||||
|
<button className={styles.btnSecondary} onClick={() => setShowSettingsModal(true)}>⚙ Invoice Settings</button>
|
||||||
|
<button className={styles.btnSecondary} onClick={() => setView('list')}>← Back to List</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* VIEW: LIST */}
|
||||||
|
{view === 'list' && (
|
||||||
|
<div className={styles.tableContainer}>
|
||||||
|
<table className={styles.table}>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Invoice No</th>
|
||||||
|
<th>Customer</th>
|
||||||
|
<th>Date</th>
|
||||||
|
<th>Amount</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{invoices.map(inv => (
|
||||||
|
<tr key={inv.id}>
|
||||||
|
<td>{inv.id}</td>
|
||||||
|
<td>{inv.customer}</td>
|
||||||
|
<td>{inv.date}</td>
|
||||||
|
<td>₹ {inv.total.toLocaleString()}</td>
|
||||||
|
<td>
|
||||||
|
<span style={{
|
||||||
|
padding: '4px 8px',
|
||||||
|
borderRadius: '4px',
|
||||||
|
fontSize: '0.8rem',
|
||||||
|
background: inv.status === 'Paid' ? 'rgba(76, 175, 80, 0.2)' : inv.status === 'Overdue' ? 'rgba(244, 67, 54, 0.2)' : 'rgba(255, 193, 7, 0.2)',
|
||||||
|
color: inv.status === 'Paid' ? '#81c784' : inv.status === 'Overdue' ? '#e57373' : '#ffd54f'
|
||||||
|
}}>
|
||||||
|
{inv.status}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<button
|
||||||
|
style={{ background: 'none', border: 'none', color: '#fff', cursor: 'pointer', marginRight: '10px' }}
|
||||||
|
onClick={() => handleViewInvoice(inv)}
|
||||||
|
title="View Invoice"
|
||||||
|
>👁️</button>
|
||||||
|
<button
|
||||||
|
style={{ background: 'none', border: 'none', color: '#fff', cursor: 'pointer' }}
|
||||||
|
onClick={() => handleDownloadInvoice(inv)}
|
||||||
|
title="Download PDF"
|
||||||
|
>⬇️</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Add Customer Modal */}
|
||||||
|
<AddCustomerModal
|
||||||
|
isOpen={showAddCustomerModal}
|
||||||
|
onClose={() => setShowAddCustomerModal(false)}
|
||||||
|
onSave={handleSaveNewCustomer}
|
||||||
|
initialName={customerSearchTerm}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Add Item Modal */}
|
||||||
|
<AddItemModal
|
||||||
|
isOpen={showAddItemModal}
|
||||||
|
onClose={() => setShowAddItemModal(false)}
|
||||||
|
onSave={handleSaveNewItem}
|
||||||
|
initialName={pendingItemIndex !== null ? items[pendingItemIndex].name : ''}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Invoice Settings Modal */}
|
||||||
|
<InvoiceSettingsModal
|
||||||
|
isOpen={showSettingsModal}
|
||||||
|
onClose={() => setShowSettingsModal(false)}
|
||||||
|
currentSettings={invoiceSettings}
|
||||||
|
onSave={setInvoiceSettings}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* VIEW: CREATE */}
|
||||||
|
{view === 'create' && (
|
||||||
|
<div className={styles.card}>
|
||||||
|
{/* Invoice Header Form */}
|
||||||
|
<div className={styles.gridFour}>
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<label className={styles.label}>Invoice No</label>
|
||||||
|
<input type="text" className={styles.input} value={invoiceNo} onChange={e => setInvoiceNo(e.target.value)} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Custom Searchable Dropdown for Customer */}
|
||||||
|
<div className={styles.formGroup} style={{ position: 'relative' }}>
|
||||||
|
<label className={styles.label}>Customer</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className={styles.input}
|
||||||
|
placeholder="Select or Search Customer"
|
||||||
|
value={customerSearchTerm}
|
||||||
|
onChange={(e) => {
|
||||||
|
setCustomerSearchTerm(e.target.value);
|
||||||
|
setIsCustomerDropdownOpen(true);
|
||||||
|
}}
|
||||||
|
onFocus={() => setIsCustomerDropdownOpen(true)}
|
||||||
|
onBlur={() => setTimeout(() => setIsCustomerDropdownOpen(false), 200)}
|
||||||
|
/>
|
||||||
|
{isCustomerDropdownOpen && (
|
||||||
|
<div style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: '100%',
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
background: 'rgba(5, 26, 21, 0.95)',
|
||||||
|
border: '1px solid rgba(255, 215, 0, 0.3)',
|
||||||
|
borderRadius: '0 0 12px 12px',
|
||||||
|
maxHeight: '200px',
|
||||||
|
overflowY: 'auto',
|
||||||
|
zIndex: 100,
|
||||||
|
backdropFilter: 'blur(10px)',
|
||||||
|
marginTop: '5px',
|
||||||
|
boxShadow: '0 10px 30px rgba(0,0,0,0.5)'
|
||||||
|
}}>
|
||||||
|
{filteredCustomers.length > 0 ? (
|
||||||
|
filteredCustomers.map(cust => (
|
||||||
|
<div
|
||||||
|
key={cust.id}
|
||||||
|
style={{
|
||||||
|
padding: '10px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
color: '#fff',
|
||||||
|
borderBottom: '1px solid rgba(255,255,255,0.05)',
|
||||||
|
transition: 'background 0.2s'
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => e.currentTarget.style.background = 'rgba(255, 215, 0, 0.1)'}
|
||||||
|
onMouseLeave={(e) => e.currentTarget.style.background = 'transparent'}
|
||||||
|
onClick={() => handleSelectCustomer(cust.name)}
|
||||||
|
>
|
||||||
|
{cust.name}
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div style={{ padding: '10px', color: '#aaa' }}>No customers found</div>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
padding: '10px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
color: 'var(--color-gold)',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
background: 'rgba(5, 26, 21, 0.8)',
|
||||||
|
borderTop: '1px solid rgba(255, 215, 0, 0.2)',
|
||||||
|
textAlign: 'center'
|
||||||
|
}}
|
||||||
|
onClick={handleAddCustomer}
|
||||||
|
>
|
||||||
|
+ Add New Customer
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<label className={styles.label}>Invoice Date</label>
|
||||||
|
<input type="date" className={styles.input} value={date} onChange={e => setDate(e.target.value)} />
|
||||||
|
</div>
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<label className={styles.label}>Due Date</label>
|
||||||
|
<input type="date" className={styles.input} value={dueDate} onChange={e => setDueDate(e.target.value)} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Line Items */}
|
||||||
|
<table className={styles.table} style={{ marginBottom: '1.5rem', background: 'rgba(0,0,0,0.2)' }}>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style={{ width: '40%' }}>Item / Service</th>
|
||||||
|
<th>HSN/SAC</th>
|
||||||
|
<th>Qty</th>
|
||||||
|
<th>Rate</th>
|
||||||
|
<th>Tax %</th>
|
||||||
|
<th>Total</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{items.map((item, index) => (
|
||||||
|
<tr key={item.id} style={{ verticalAlign: 'top' }}>
|
||||||
|
<td style={{ position: 'relative' }}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className={styles.input}
|
||||||
|
placeholder="Item Name"
|
||||||
|
value={item.name}
|
||||||
|
onChange={e => {
|
||||||
|
const newItems = [...items];
|
||||||
|
newItems[index].name = e.target.value;
|
||||||
|
setItems(newItems);
|
||||||
|
setActiveItemDropdownId(item.id);
|
||||||
|
}}
|
||||||
|
onFocus={() => setActiveItemDropdownId(item.id)}
|
||||||
|
onBlur={() => setTimeout(() => setActiveItemDropdownId(null), 200)}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
/>
|
||||||
|
{activeItemDropdownId === item.id && (
|
||||||
|
<div style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: '100%',
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
background: 'rgba(5, 26, 21, 0.95)',
|
||||||
|
border: '1px solid rgba(255, 215, 0, 0.3)',
|
||||||
|
borderRadius: '0 0 12px 12px',
|
||||||
|
maxHeight: '150px',
|
||||||
|
overflowY: 'auto',
|
||||||
|
zIndex: 100,
|
||||||
|
backdropFilter: 'blur(10px)',
|
||||||
|
marginTop: '5px',
|
||||||
|
boxShadow: '0 10px 30px rgba(0,0,0,0.5)'
|
||||||
|
}}>
|
||||||
|
{PRODUCTS_SERVICES
|
||||||
|
.filter(p => p.name.toLowerCase().includes(item.name.toLowerCase()))
|
||||||
|
.map((prod, idx) => (
|
||||||
|
<div
|
||||||
|
key={idx}
|
||||||
|
style={{
|
||||||
|
padding: '8px 10px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
color: '#fff',
|
||||||
|
fontSize: '0.9rem',
|
||||||
|
borderBottom: '1px solid rgba(255,255,255,0.05)',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between'
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => e.currentTarget.style.background = 'rgba(255, 215, 0, 0.1)'}
|
||||||
|
onMouseLeave={(e) => e.currentTarget.style.background = 'transparent'}
|
||||||
|
onClick={() => handleSelectItem(index, prod)}
|
||||||
|
>
|
||||||
|
<span>{prod.name}</span>
|
||||||
|
<span style={{ color: '#aaa', fontSize: '0.8rem' }}>₹{prod.rate}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
padding: '10px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
color: 'var(--color-gold)',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
background: 'rgba(5, 26, 21, 0.8)',
|
||||||
|
borderTop: '1px solid rgba(255, 215, 0, 0.2)',
|
||||||
|
textAlign: 'center',
|
||||||
|
fontSize: '0.9rem'
|
||||||
|
}}
|
||||||
|
onMouseDown={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handleAddItem(index);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
+ Add New Item
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input type="text" className={styles.input} value={item.hsn}
|
||||||
|
onChange={e => {
|
||||||
|
const newItems = [...items];
|
||||||
|
newItems[index].hsn = e.target.value;
|
||||||
|
setItems(newItems);
|
||||||
|
}}
|
||||||
|
placeholder="HSN" style={{ width: '80px' }}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input type="number" className={styles.input} value={item.quantity}
|
||||||
|
onChange={e => {
|
||||||
|
const newItems = [...items];
|
||||||
|
newItems[index].quantity = Number(e.target.value);
|
||||||
|
setItems(newItems);
|
||||||
|
}}
|
||||||
|
style={{ width: '60px' }}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input type="number" className={styles.input} value={item.rate}
|
||||||
|
onChange={e => {
|
||||||
|
const newItems = [...items];
|
||||||
|
newItems[index].rate = Number(e.target.value);
|
||||||
|
setItems(newItems);
|
||||||
|
}}
|
||||||
|
style={{ width: '100px' }}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<select className={styles.select} value={item.gstRate} onChange={e => {
|
||||||
|
const newItems = [...items];
|
||||||
|
newItems[index].gstRate = Number(e.target.value);
|
||||||
|
setItems(newItems);
|
||||||
|
}}>
|
||||||
|
{GST_RATES.map(r => <option key={r} value={r}>{r}%</option>)}
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
<td>₹ {((item.quantity * item.rate) + ((item.quantity * item.rate * item.gstRate) / 100)).toFixed(2)}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<button className={styles.btnSecondary} onClick={addItemRow} style={{ marginBottom: '2rem' }}>+ Add Row</button>
|
||||||
|
|
||||||
|
{/* Footer / Totals */}
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', marginTop: '2rem', gap: '2rem' }}>
|
||||||
|
<div style={{ flex: 1 }}>
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<label className={styles.label}>Customer Notes</label>
|
||||||
|
<textarea
|
||||||
|
value={notes}
|
||||||
|
onChange={(e) => setNotes(e.target.value)}
|
||||||
|
rows={3}
|
||||||
|
className={styles.input}
|
||||||
|
style={{ width: '100%', resize: 'none' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles.formGroup} style={{ marginTop: '1rem' }}>
|
||||||
|
<label className={styles.label}>Terms & Conditions</label>
|
||||||
|
<textarea
|
||||||
|
value={terms}
|
||||||
|
onChange={(e) => setTerms(e.target.value)}
|
||||||
|
rows={3}
|
||||||
|
className={styles.input}
|
||||||
|
style={{ width: '100%', resize: 'none' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p style={{ marginTop: '1rem', fontStyle: 'italic', color: '#aaa', fontSize: '0.9rem' }}>
|
||||||
|
Amount in Words: <span style={{ color: 'var(--color-gold)' }}>{totals.amountInWords} Rupees Only</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ width: '400px', background: 'rgba(5, 26, 21, 0.6)', padding: '1.5rem', borderRadius: '12px', border: '1px solid rgba(255, 215, 0, 0.1)' }}>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '0.5rem' }}>
|
||||||
|
<span className={styles.label}>Subtotal</span>
|
||||||
|
<span>₹ {totals.subtotal.toFixed(2)}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '0.5rem', color: '#e57373' }}>
|
||||||
|
<span>Less: Discount</span>
|
||||||
|
<span>- ₹ {totals.discount.toFixed(2)}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '0.5rem' }}>
|
||||||
|
<span className={styles.label}>Taxable Value</span>
|
||||||
|
<span>₹ {totals.taxable.toFixed(2)}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{totals.igst > 0 ? (
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '0.5rem', color: '#aaa' }}>
|
||||||
|
<span>IGST</span>
|
||||||
|
<span>₹ {totals.igst.toFixed(2)}</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '0.5rem', color: '#aaa' }}>
|
||||||
|
<span>CGST</span>
|
||||||
|
<span>₹ {totals.cgst.toFixed(2)}</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '0.5rem', color: '#aaa' }}>
|
||||||
|
<span>SGST</span>
|
||||||
|
<span>₹ {totals.sgst.toFixed(2)}</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* TDS Selection */}
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '0.5rem' }}>
|
||||||
|
<span className={styles.label}>TDS Rate</span>
|
||||||
|
<select
|
||||||
|
className={styles.select}
|
||||||
|
style={{ padding: '2px', fontSize: '0.8rem', width: '80px' }}
|
||||||
|
value={tdsRate}
|
||||||
|
onChange={(e) => setTdsRate(Number(e.target.value))}
|
||||||
|
>
|
||||||
|
<option value="0">None</option>
|
||||||
|
<option value="1">1%</option>
|
||||||
|
<option value="2">2%</option>
|
||||||
|
<option value="5">5%</option>
|
||||||
|
<option value="10">10%</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
{tdsRate > 0 && (
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '0.5rem', color: '#ffd54f' }}>
|
||||||
|
<span>TDS Amount (Info)</span>
|
||||||
|
<span>(₹ {totals.tdsAmount.toFixed(2)})</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Round Off Toggle */}
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '0.5rem' }}>
|
||||||
|
<label style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', cursor: 'pointer', fontSize: '0.85rem', color: '#aaa' }}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={roundOff}
|
||||||
|
onChange={(e) => setRoundOff(e.target.checked)}
|
||||||
|
/> Round Off
|
||||||
|
</label>
|
||||||
|
<span>{totals.roundOffAmount !== 0 ? `₹ {totals.roundOffAmount.toFixed(2)}` : '-'}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ borderTop: '1px solid rgba(255,255,255,0.1)', margin: '0.5rem 0' }}></div>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '1.2rem', fontWeight: 700, color: 'var(--color-gold)' }}>
|
||||||
|
<span>Grand Total</span>
|
||||||
|
<span>₹ {totals.grandTotal.toFixed(2)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '1rem', marginTop: '2rem' }}>
|
||||||
|
<button className={styles.btnSecondary} onClick={() => setView('list')}>Cancel</button>
|
||||||
|
<button className={styles.btnSecondary} onClick={() => setToast({ message: 'Saved as Draft!', type: 'info' })}>Save Draft</button>
|
||||||
|
<button
|
||||||
|
className={styles.btnPrimary}
|
||||||
|
onClick={() => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
generateInvoicePDF({
|
||||||
|
invoiceNo: 'INV-NEW',
|
||||||
|
date: date,
|
||||||
|
dueDate: dueDate,
|
||||||
|
customer: customer,
|
||||||
|
customerGstin: customerGstin,
|
||||||
|
placeOfSupply: placeOfSupply,
|
||||||
|
items: items,
|
||||||
|
totals: totals,
|
||||||
|
tdsRate: tdsRate
|
||||||
|
}, 'download');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Save & Post
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* MODAL: INVOICE PREVIEW */}
|
||||||
|
{showModal && selectedInvoice && (
|
||||||
|
<div className={styles.modalOverlay} onClick={() => setShowModal(false)}>
|
||||||
|
<div className={styles.modalContent} onClick={e => e.stopPropagation()}>
|
||||||
|
<button className={styles.closeButton} onClick={() => setShowModal(false)}>×</button>
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1rem' }}>
|
||||||
|
<h2>Invoice #{selectedInvoice.id}</h2>
|
||||||
|
<div style={{ display: 'flex', gap: '1rem' }}>
|
||||||
|
<button className={styles.btnSecondary} onClick={() => handleDownloadInvoice(selectedInvoice, 'print')}>Print</button>
|
||||||
|
<button className={styles.btnPrimary} onClick={() => handleDownloadInvoice(selectedInvoice, 'download')}>Download PDF</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.invoicePreview}>
|
||||||
|
<div style={{ textAlign: 'center', marginBottom: '2rem' }}>
|
||||||
|
<h1>TAX INVOICE</h1>
|
||||||
|
<p>TechFlow Solutions</p>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||||
|
<div>
|
||||||
|
<strong>Bill To:</strong><br />
|
||||||
|
{selectedInvoice.customer}<br />
|
||||||
|
101, Business Bay<br />
|
||||||
|
Pune
|
||||||
|
</div>
|
||||||
|
<div style={{ textAlign: 'right' }}>
|
||||||
|
<strong>Invoice No:</strong> {selectedInvoice.id}<br />
|
||||||
|
<strong>Date:</strong> {selectedInvoice.date}<br />
|
||||||
|
<strong>Total:</strong> ₹ {selectedInvoice.total.toLocaleString()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table className={styles.table} style={{ marginTop: '2rem', border: '1px solid #ddd' }}>
|
||||||
|
<thead>
|
||||||
|
<tr style={{ background: '#f5f5f5', color: '#000' }}>
|
||||||
|
<th>Description</th>
|
||||||
|
<th style={{ textAlign: 'right' }}>Amount</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{selectedInvoice.items ? (
|
||||||
|
selectedInvoice.items.map((item, idx) => (
|
||||||
|
<tr key={idx}>
|
||||||
|
<td style={{ color: '#000' }}>{item.description}</td>
|
||||||
|
<td style={{ textAlign: 'right', color: '#000' }}>₹ {item.amount.toLocaleString()}</td>
|
||||||
|
</tr>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<tr>
|
||||||
|
<td style={{ color: '#000' }}>Consulting Services (Demo)</td>
|
||||||
|
<td style={{ textAlign: 'right', color: '#000' }}>₹ {selectedInvoice.total.toLocaleString()}</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{toast && (
|
||||||
|
<Toast
|
||||||
|
message={toast.message}
|
||||||
|
type={toast.type}
|
||||||
|
onClose={() => setToast(null)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
190
src/app/dashboard/accounts/journal/page.tsx
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import styles from '../accounts.module.css';
|
||||||
|
|
||||||
|
export default function JournalPage() {
|
||||||
|
const [view, setView] = useState<'list' | 'create'>('list');
|
||||||
|
|
||||||
|
// Create Journal Entry State
|
||||||
|
const [date, setDate] = useState('');
|
||||||
|
const [desc, setDesc] = useState('');
|
||||||
|
const [rows, setRows] = useState([
|
||||||
|
{ id: 1, account: '', debit: 0, credit: 0 },
|
||||||
|
{ id: 2, account: '', debit: 0, credit: 0 }
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Mock List Data
|
||||||
|
const journals = [
|
||||||
|
{ id: 'JRN-001', date: '2024-11-21', desc: 'Opening Balance for Cash', debit: 50000, credit: 50000, status: 'Posted' },
|
||||||
|
{ id: 'JRN-002', date: '2024-11-23', desc: 'Office Supplies Expense', debit: 2500, credit: 2500, status: 'Posted' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const totalDebit = rows.reduce((acc, row) => acc + Number(row.debit), 0);
|
||||||
|
const totalCredit = rows.reduce((acc, row) => acc + Number(row.credit), 0);
|
||||||
|
const isBalanced = totalDebit === totalCredit && totalDebit > 0;
|
||||||
|
|
||||||
|
const addRow = () => {
|
||||||
|
setRows([...rows, { id: rows.length + 1, account: '', debit: 0, credit: 0 }]);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
|
||||||
|
<div className={styles.header}>
|
||||||
|
<div className={styles.titleGroup}>
|
||||||
|
<h1>General Journal</h1>
|
||||||
|
<p className={styles.subTitle}>Manual Double-Entry Bookkeeping</p>
|
||||||
|
</div>
|
||||||
|
<div className={styles.controls}>
|
||||||
|
{view === 'list' ? (
|
||||||
|
<button className={styles.btnPrimary} onClick={() => setView('create')}>+ New Journal Entry</button>
|
||||||
|
) : (
|
||||||
|
<button className={styles.btnSecondary} onClick={() => setView('list')}>← Back to Journal</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* VIEW: LIST */}
|
||||||
|
{view === 'list' && (
|
||||||
|
<div className={styles.tableContainer}>
|
||||||
|
<table className={styles.table}>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Journal ID</th>
|
||||||
|
<th>Date</th>
|
||||||
|
<th>Description</th>
|
||||||
|
<th>Total Debit</th>
|
||||||
|
<th>Total Credit</th>
|
||||||
|
<th>Status</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{journals.map(j => (
|
||||||
|
<tr key={j.id}>
|
||||||
|
<td>{j.id}</td>
|
||||||
|
<td>{j.date}</td>
|
||||||
|
<td>{j.desc}</td>
|
||||||
|
<td>₹ {j.debit.toLocaleString()}</td>
|
||||||
|
<td>₹ {j.credit.toLocaleString()}</td>
|
||||||
|
<td>
|
||||||
|
<span style={{
|
||||||
|
padding: '4px 8px',
|
||||||
|
borderRadius: '4px',
|
||||||
|
fontSize: '0.8rem',
|
||||||
|
background: 'rgba(76, 175, 80, 0.2)',
|
||||||
|
color: '#81c784'
|
||||||
|
}}>
|
||||||
|
{j.status}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* VIEW: CREATE */}
|
||||||
|
{view === 'create' && (
|
||||||
|
<div className={styles.card}>
|
||||||
|
<div className={styles.gridTwo}>
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<label className={styles.label}>Voucher Date</label>
|
||||||
|
<input type="date" className={styles.input} value={date} onChange={e => setDate(e.target.value)} />
|
||||||
|
</div>
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<label className={styles.label}>Narration / Description</label>
|
||||||
|
<input type="text" className={styles.input} placeholder="e.g. Bank Charges for Nov" value={desc} onChange={e => setDesc(e.target.value)} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table className={styles.table} style={{ marginBottom: '1.5rem' }}>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style={{ width: '40%' }}>Account</th>
|
||||||
|
<th>Debit (₹)</th>
|
||||||
|
<th>Credit (₹)</th>
|
||||||
|
<th>Action</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{rows.map((row, index) => (
|
||||||
|
<tr key={row.id}>
|
||||||
|
<td>
|
||||||
|
<select className={styles.select} style={{ width: '100%' }}>
|
||||||
|
<option value="">Select Account</option>
|
||||||
|
<option>Cash Account</option>
|
||||||
|
<option>Bank Account (HDFC)</option>
|
||||||
|
<option>Sales Account</option>
|
||||||
|
<option>Stationary Expense</option>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input type="number" className={styles.input} value={row.debit}
|
||||||
|
onChange={e => {
|
||||||
|
const newRows = [...rows];
|
||||||
|
newRows[index].debit = Number(e.target.value);
|
||||||
|
newRows[index].credit = 0; // Clear credit if debit entered
|
||||||
|
setRows(newRows);
|
||||||
|
}}
|
||||||
|
disabled={row.credit > 0}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input type="number" className={styles.input} value={row.credit}
|
||||||
|
onChange={e => {
|
||||||
|
const newRows = [...rows];
|
||||||
|
newRows[index].credit = Number(e.target.value);
|
||||||
|
newRows[index].debit = 0; // Clear debit if credit entered
|
||||||
|
setRows(newRows);
|
||||||
|
}}
|
||||||
|
disabled={row.debit > 0}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<button style={{ background: 'none', border: 'none', color: '#f44336', cursor: 'pointer' }}>❌</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<button className={styles.btnSecondary} onClick={addRow} style={{ marginBottom: '2rem' }}>+ Add Line</button>
|
||||||
|
|
||||||
|
{/* Footer Validation */}
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', background: 'rgba(0,0,0,0.3)', padding: '1.5rem', borderRadius: '8px' }}>
|
||||||
|
<div>
|
||||||
|
{!isBalanced && (
|
||||||
|
<div style={{ color: '#f44336', display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
|
||||||
|
⚠️ <strong>Entry Unbalanced:</strong> Difference of ₹ {Math.abs(totalDebit - totalCredit)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{isBalanced && (
|
||||||
|
<div style={{ color: '#4caf50', display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
|
||||||
|
✅ <strong>Balanced</strong>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', gap: '2rem' }}>
|
||||||
|
<div style={{ textAlign: 'right' }}>
|
||||||
|
<div className={styles.metricLabel}>Total Debit</div>
|
||||||
|
<div style={{ fontSize: '1.2rem', fontWeight: 700 }}>₹ {totalDebit}</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ textAlign: 'right' }}>
|
||||||
|
<div className={styles.metricLabel}>Total Credit</div>
|
||||||
|
<div style={{ fontSize: '1.2rem', fontWeight: 700 }}>₹ {totalCredit}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '1rem', marginTop: '2rem' }}>
|
||||||
|
<button className={styles.btnSecondary} onClick={() => setView('list')}>Cancel</button>
|
||||||
|
<button className={styles.btnPrimary} disabled={!isBalanced} style={{ opacity: !isBalanced ? 0.5 : 1, cursor: !isBalanced ? 'not-allowed' : 'pointer' }}>Post Journal Entry</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
258
src/app/dashboard/accounts/ledger/page.tsx
Normal file
@@ -0,0 +1,258 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import styles from '../accounts.module.css';
|
||||||
|
import { generatePDF } from '@/utils/pdfGenerator';
|
||||||
|
import { useToast } from '../../../../context/ToastContext';
|
||||||
|
|
||||||
|
// Mock Data
|
||||||
|
const ACCOUNTS = [
|
||||||
|
{ id: 1, name: 'Cash Account', type: 'Asset' },
|
||||||
|
{ id: 2, name: 'Bank Account (HDFC)', type: 'Asset' },
|
||||||
|
{ id: 3, name: 'Sales Account', type: 'Revenue' },
|
||||||
|
{ id: 4, name: 'Purchase Account', type: 'Expense' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const LEDGER_ENTRIES = [
|
||||||
|
{ date: '2024-11-01', particular: 'Opening Balance', vType: 'Opening', vNo: '-', debit: 50000, credit: 0 },
|
||||||
|
{ date: '2024-11-05', particular: 'Cash Sales', vType: 'Sales', vNo: 'INV-001', debit: 12000, credit: 0 },
|
||||||
|
{ date: '2024-11-10', particular: 'Petty Expenses', vType: 'Payment', vNo: 'PYT-003', debit: 0, credit: 1500 },
|
||||||
|
{ date: '2024-11-15', particular: 'Bank Deposit', vType: 'Contra', vNo: 'CNT-001', debit: 0, credit: 20000 },
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function LedgerPage() {
|
||||||
|
const [selectedAccount, setSelectedAccount] = useState(ACCOUNTS[0].id);
|
||||||
|
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||||
|
const [newLedger, setNewLedger] = useState({ name: '', type: 'Asset' });
|
||||||
|
const { showToast } = useToast();
|
||||||
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
const [startDate, setStartDate] = useState('2024-11-01');
|
||||||
|
const [endDate, setEndDate] = useState('2024-11-30');
|
||||||
|
|
||||||
|
const filteredAccounts = ACCOUNTS.filter(acc =>
|
||||||
|
acc.name.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
|
const currentAccount = ACCOUNTS.find(a => a.id === Number(selectedAccount));
|
||||||
|
|
||||||
|
// Filter Entries based on Date Range
|
||||||
|
const filteredEntries = LEDGER_ENTRIES.filter(entry => {
|
||||||
|
return entry.date >= startDate && entry.date <= endDate;
|
||||||
|
});
|
||||||
|
|
||||||
|
const totalDebit = filteredEntries.reduce((sum, entry) => sum + (entry.debit || 0), 0);
|
||||||
|
const totalCredit = filteredEntries.reduce((sum, entry) => sum + (entry.credit || 0), 0);
|
||||||
|
const closingBalance = totalDebit - totalCredit;
|
||||||
|
|
||||||
|
const handleDownloadPDF = () => {
|
||||||
|
if (!currentAccount) return;
|
||||||
|
|
||||||
|
const tableData = filteredEntries.map(entry => [
|
||||||
|
entry.date,
|
||||||
|
entry.particular,
|
||||||
|
entry.vType,
|
||||||
|
entry.vNo,
|
||||||
|
entry.debit ? entry.debit.toLocaleString() : '-',
|
||||||
|
entry.credit ? entry.credit.toLocaleString() : '-'
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Add Closing Balance Row
|
||||||
|
tableData.push([
|
||||||
|
'',
|
||||||
|
'Closing Balance',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
closingBalance > 0 ? closingBalance.toLocaleString() : '-',
|
||||||
|
closingBalance < 0 ? Math.abs(closingBalance).toLocaleString() : '-'
|
||||||
|
]);
|
||||||
|
|
||||||
|
generatePDF({
|
||||||
|
title: `Ledger: ${currentAccount.name}`,
|
||||||
|
period: `From ${startDate} to ${endDate}`,
|
||||||
|
columns: ['Date', 'Particulars', 'V. Type', 'V. No', 'Debit (Rs)', 'Credit (Rs)'],
|
||||||
|
data: tableData,
|
||||||
|
orientation: 'portrait'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreateLedger = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
showToast(`Created new ledger: ${newLedger.name} (${newLedger.type})`, 'success');
|
||||||
|
setShowCreateModal(false);
|
||||||
|
setNewLedger({ name: '', type: 'Asset' });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<div className={styles.header}>
|
||||||
|
<div className={styles.titleGroup}>
|
||||||
|
<h1>Ledger Books</h1>
|
||||||
|
<p className={styles.subTitle}>View and manage individual account ledgers</p>
|
||||||
|
</div>
|
||||||
|
<div className={styles.controls}>
|
||||||
|
<button
|
||||||
|
className={styles.btnPrimary}
|
||||||
|
onClick={() => setShowCreateModal(true)}
|
||||||
|
>
|
||||||
|
+ Create New Ledger
|
||||||
|
</button>
|
||||||
|
<button className={styles.btnSecondary} onClick={handleDownloadPDF}>
|
||||||
|
⬇ Download PDF
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.gridWithSidebar}>
|
||||||
|
{/* Sidebar: Account List */}
|
||||||
|
<div className={styles.sidebarList}>
|
||||||
|
<div className={styles.searchBox}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search Ledgers..."
|
||||||
|
className={styles.input}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<ul className={styles.accountList}>
|
||||||
|
{filteredAccounts.map(acc => (
|
||||||
|
<li
|
||||||
|
key={acc.id}
|
||||||
|
className={`${styles.accountItem} ${selectedAccount === acc.id ? styles.active : ''}`}
|
||||||
|
onClick={() => setSelectedAccount(acc.id)}
|
||||||
|
>
|
||||||
|
<span className={styles.accName}>{acc.name}</span>
|
||||||
|
<span className={styles.accType}>{acc.type}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Content: Ledger Table */}
|
||||||
|
<div className={styles.mainContent}>
|
||||||
|
{currentAccount && (
|
||||||
|
<>
|
||||||
|
<div className={styles.contentHeader} style={{ flexWrap: 'wrap', gap: '1rem' }}>
|
||||||
|
<div>
|
||||||
|
<h2>{currentAccount.name}</h2>
|
||||||
|
<span className={styles.periodBadge}>
|
||||||
|
{new Date(startDate).toLocaleDateString()} - {new Date(endDate).toLocaleDateString()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', gap: '0.5rem', alignItems: 'center' }}>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
className={styles.input}
|
||||||
|
value={startDate}
|
||||||
|
onChange={(e) => setStartDate(e.target.value)}
|
||||||
|
/>
|
||||||
|
<span style={{ color: '#aaa' }}>to</span>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
className={styles.input}
|
||||||
|
value={endDate}
|
||||||
|
onChange={(e) => setEndDate(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.tableContainer}>
|
||||||
|
<table className={styles.table}>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Date</th>
|
||||||
|
<th>Particulars</th>
|
||||||
|
<th>V. Type</th>
|
||||||
|
<th>V. No</th>
|
||||||
|
<th style={{ textAlign: 'right' }}>Debit</th>
|
||||||
|
<th style={{ textAlign: 'right' }}>Credit</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{filteredEntries.length > 0 ? (
|
||||||
|
filteredEntries.map((entry, index) => (
|
||||||
|
<tr key={index}>
|
||||||
|
<td>{entry.date}</td>
|
||||||
|
<td>{entry.particular}</td>
|
||||||
|
<td>{entry.vType}</td>
|
||||||
|
<td>{entry.vNo}</td>
|
||||||
|
<td style={{ textAlign: 'right', color: entry.debit ? '#f44336' : 'inherit' }}>
|
||||||
|
{entry.debit ? `₹ ${entry.debit.toLocaleString()}` : '-'}
|
||||||
|
</td>
|
||||||
|
<td style={{ textAlign: 'right', color: entry.credit ? '#4caf50' : 'inherit' }}>
|
||||||
|
{entry.credit ? `₹ ${entry.credit.toLocaleString()}` : '-'}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={6} style={{ textAlign: 'center', padding: '2rem', color: '#aaa' }}>
|
||||||
|
No entries found for this period
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
<tr style={{ fontWeight: 'bold', background: 'rgba(255,255,255,0.05)' }}>
|
||||||
|
<td colSpan={4} style={{ textAlign: 'right' }}>Closing Balance</td>
|
||||||
|
<td style={{ textAlign: 'right', color: closingBalance > 0 ? '#f44336' : '#aaa' }}>
|
||||||
|
{closingBalance > 0 ? `₹ ${closingBalance.toLocaleString()}` : '-'}
|
||||||
|
</td>
|
||||||
|
<td style={{ textAlign: 'right', color: closingBalance < 0 ? '#4caf50' : '#aaa' }}>
|
||||||
|
{closingBalance < 0 ? `₹ ${Math.abs(closingBalance).toLocaleString()}` : '-'}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Create Ledger Modal */}
|
||||||
|
{showCreateModal && (
|
||||||
|
<div className={styles.modalOverlay} onClick={() => setShowCreateModal(false)}>
|
||||||
|
<div className={styles.modalContent} onClick={e => e.stopPropagation()}>
|
||||||
|
<div className={styles.modalHeader}>
|
||||||
|
<h3>Create New Ledger Account</h3>
|
||||||
|
<button className={styles.closeButton} onClick={() => setShowCreateModal(false)}>×</button>
|
||||||
|
</div>
|
||||||
|
<form onSubmit={handleCreateLedger} style={{ padding: '1.5rem', display: 'flex', flexDirection: 'column', gap: '1rem' }}>
|
||||||
|
<div>
|
||||||
|
<label style={{ display: 'block', marginBottom: '0.5rem', fontSize: '0.9rem', color: '#aaa' }}>Ledger Name</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className={styles.input}
|
||||||
|
placeholder="e.g., Travel Expenses"
|
||||||
|
value={newLedger.name}
|
||||||
|
onChange={e => setNewLedger({ ...newLedger, name: e.target.value })}
|
||||||
|
required
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label style={{ display: 'block', marginBottom: '0.5rem', fontSize: '0.9rem', color: '#aaa' }}>Account Type</label>
|
||||||
|
<select
|
||||||
|
className={styles.select}
|
||||||
|
value={newLedger.type}
|
||||||
|
onChange={e => setNewLedger({ ...newLedger, type: e.target.value })}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
>
|
||||||
|
<option value="Asset">Asset</option>
|
||||||
|
<option value="Liability">Liability</option>
|
||||||
|
<option value="Equity">Equity</option>
|
||||||
|
<option value="Revenue">Revenue</option>
|
||||||
|
<option value="Expense">Expense</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '1rem', marginTop: '1rem' }}>
|
||||||
|
<button type="button" className={styles.btnSecondary} onClick={() => setShowCreateModal(false)}>Cancel</button>
|
||||||
|
<button type="submit" className={styles.btnPrimary}>Create Ledger</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
157
src/app/dashboard/accounts/page.tsx
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import styles from './accounts.module.css';
|
||||||
|
import { generatePDF } from '@/utils/pdfGenerator';
|
||||||
|
|
||||||
|
export default function AccountsOverviewPage() {
|
||||||
|
const [period, setPeriod] = useState('This Month');
|
||||||
|
|
||||||
|
const handleDownloadReport = () => {
|
||||||
|
// Helper for coloring
|
||||||
|
const green = [76, 175, 80];
|
||||||
|
const red = [244, 67, 54];
|
||||||
|
const neutral = [50, 50, 50];
|
||||||
|
|
||||||
|
generatePDF({
|
||||||
|
title: "Financial Overview Report",
|
||||||
|
period: "As of November 2024",
|
||||||
|
columns: ["Metric", "Value", "Trend / Status"],
|
||||||
|
data: [
|
||||||
|
// KPI Section
|
||||||
|
["I. Key Performance Indicators", "", ""],
|
||||||
|
["Total Receivables", "Rs. 12,45,000", { content: "+5% vs last period", styles: { textColor: green } }],
|
||||||
|
["Total Payables", "Rs. 4,32,000", { content: "-2% due within 7 days", styles: { textColor: red } }],
|
||||||
|
["Cash + Bank Balance", "Rs. 28,10,500", { content: "Stable healthy liquidity", styles: { textColor: green } }],
|
||||||
|
["GST Liability (Net)", "Rs. 85,400", { content: "Payable this month", styles: { textColor: red } }],
|
||||||
|
|
||||||
|
// Profit & Loss Summary
|
||||||
|
["II. Profit & Loss Summary", "", ""],
|
||||||
|
["Total Revenue", "Rs. 45,20,000", { content: "On Target", styles: { textColor: green } }],
|
||||||
|
["Total Expenses", "Rs. 18,50,000", { content: "Within Budget", styles: { textColor: green } }],
|
||||||
|
["Net Profit", { content: "Rs. 26,70,000", styles: { fontStyle: 'bold', textColor: green } }, { content: "+18.3% Margin", styles: { textColor: green } }],
|
||||||
|
|
||||||
|
// Inventory Health
|
||||||
|
["III. Inventory Health", "", ""],
|
||||||
|
["Total Units in Stock", "1,240 Units", "Adequate"],
|
||||||
|
["Low Stock Items", "12 Items", { content: "Action Required", styles: { textColor: red } }],
|
||||||
|
["Stock Valuation (FIFO)", "Rs. 15,40,000", "Updated today"]
|
||||||
|
],
|
||||||
|
orientation: 'portrait'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
{/* 1. Header Section */}
|
||||||
|
<div className={styles.header}>
|
||||||
|
<div className={styles.titleGroup}>
|
||||||
|
<h1>Financial Overview</h1>
|
||||||
|
<p className={styles.subTitle}>TechFlow Solutions • GSTIN: 27AABCU9603R1ZN</p>
|
||||||
|
</div>
|
||||||
|
<div className={styles.controls}>
|
||||||
|
<select
|
||||||
|
className={styles.select}
|
||||||
|
value={period}
|
||||||
|
onChange={(e) => setPeriod(e.target.value)}
|
||||||
|
style={{ minWidth: '150px' }}
|
||||||
|
>
|
||||||
|
<option>This Month</option>
|
||||||
|
<option>Last Month</option>
|
||||||
|
<option>This Quarter</option>
|
||||||
|
<option>This Year</option>
|
||||||
|
</select>
|
||||||
|
<button className={styles.btnPrimary} onClick={handleDownloadReport}>Download Report</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 2. Critical Alerts (Mock Logic) */}
|
||||||
|
<div className={styles.alertCard}>
|
||||||
|
<span className={styles.alertIcon}>⚠️</span>
|
||||||
|
<div>
|
||||||
|
<strong>Action Required:</strong> GST Filing for November is pending. Due in 3 days.
|
||||||
|
</div>
|
||||||
|
<button className={styles.btnSecondary} style={{ marginLeft: 'auto', padding: '0.3rem 0.8rem', fontSize: '0.8rem' }}>File Now</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 3. KPI Cards */}
|
||||||
|
<div className={styles.gridFour}>
|
||||||
|
<div className={styles.card}>
|
||||||
|
<div className={styles.metricLabel}>Total Receivables</div>
|
||||||
|
<div className={styles.metricValue} style={{ color: 'var(--color-cyan)' }}>₹ 12,45,000</div>
|
||||||
|
<div className={styles.metricTrend}>
|
||||||
|
<span className={styles.trendUp}>▲ 5%</span> vs last period
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.card}>
|
||||||
|
<div className={styles.metricLabel}>Total Payables</div>
|
||||||
|
<div className={styles.metricValue} style={{ color: '#ffcc80' }}>₹ 4,32,000</div>
|
||||||
|
<div className={styles.metricTrend}>
|
||||||
|
<span className={styles.trendDown}>▼ 2%</span> due within 7 days
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.card}>
|
||||||
|
<div className={styles.metricLabel}>Cash + Bank</div>
|
||||||
|
<div className={styles.metricValue} style={{ color: '#81c784' }}>₹ 28,10,500</div>
|
||||||
|
<div className={styles.metricTrend}>
|
||||||
|
<span className={styles.trendNeutral}>• Stable</span> healthy liquidity
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.card}>
|
||||||
|
<div className={styles.metricLabel}>GST Liability (Net)</div>
|
||||||
|
<div className={styles.metricValue} style={{ color: '#e57373' }}>₹ 85,400</div>
|
||||||
|
<div className={styles.metricTrend}>
|
||||||
|
<span className={styles.trendUp}>▲</span> Payable this month
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 4. Financial Summary Widgets */}
|
||||||
|
<div className={styles.gridTwo}>
|
||||||
|
{/* Profit Summary */}
|
||||||
|
<div className={styles.card}>
|
||||||
|
<h3 style={{ marginBottom: '1.5rem', borderBottom: '1px solid rgba(255,255,255,0.1)', paddingBottom: '0.5rem' }}>Profit & Loss Summary</h3>
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
|
<span style={{ color: '#aaa' }}>Revenue</span>
|
||||||
|
<span style={{ fontSize: '1.2rem', fontWeight: 600 }}>₹ 45,20,000</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
|
<span style={{ color: '#aaa' }}>Expenses</span>
|
||||||
|
<span style={{ fontSize: '1.2rem', fontWeight: 600 }}>₹ 18,50,000</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ borderTop: '1px dashed rgba(255,255,255,0.2)', margin: '0.5rem 0' }}></div>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
|
<span style={{ color: 'var(--color-gold)' }}>Net Profit</span>
|
||||||
|
<span style={{ fontSize: '1.5rem', fontWeight: 700, color: 'var(--color-gold)' }}>₹ 26,70,000</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Inventory Snapshot */}
|
||||||
|
<div className={styles.card}>
|
||||||
|
<h3 style={{ marginBottom: '1.5rem', borderBottom: '1px solid rgba(255,255,255,0.1)', paddingBottom: '0.5rem' }}>Inventory Health</h3>
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem' }}>
|
||||||
|
<div style={{ background: 'rgba(0,0,0,0.2)', padding: '1rem', borderRadius: '8px', textAlign: 'center' }}>
|
||||||
|
<div style={{ fontSize: '2rem', fontWeight: 700, color: '#4caf50' }}>1,240</div>
|
||||||
|
<div style={{ fontSize: '0.8rem', color: '#aaa' }}>Total Units</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ background: 'rgba(0,0,0,0.2)', padding: '1rem', borderRadius: '8px', textAlign: 'center' }}>
|
||||||
|
<div style={{ fontSize: '2rem', fontWeight: 700, color: '#f44336' }}>12</div>
|
||||||
|
<div style={{ fontSize: '0.8rem', color: '#aaa' }}>Low Stock Items</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ marginTop: '1.5rem' }}>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '0.5rem', fontSize: '0.9rem' }}>
|
||||||
|
<span>Stock Valuation (FIFO)</span>
|
||||||
|
<span>₹ 15,40,000</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ width: '100%', height: '6px', background: 'rgba(255,255,255,0.1)', borderRadius: '3px' }}>
|
||||||
|
<div style={{ width: '70%', height: '100%', background: 'linear-gradient(90deg, var(--color-gold), #b8860b)', borderRadius: '3px' }}></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
352
src/app/dashboard/accounts/reports/page.tsx
Normal file
@@ -0,0 +1,352 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import styles from '../accounts.module.css';
|
||||||
|
import { generatePDF } from '@/utils/pdfGenerator';
|
||||||
|
import { useToast } from '../../../../context/ToastContext';
|
||||||
|
|
||||||
|
// --- Types & Data ---
|
||||||
|
|
||||||
|
type Report = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
isNew?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ReportCategory = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
icon: string;
|
||||||
|
reports: Report[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const REPORT_CATEGORIES: ReportCategory[] = [
|
||||||
|
{
|
||||||
|
id: 'general',
|
||||||
|
title: 'General Report',
|
||||||
|
icon: '📊',
|
||||||
|
reports: [
|
||||||
|
{ id: 'ledger_report', name: 'Ledger Report' },
|
||||||
|
{ id: 'day_book', name: 'Day Book' },
|
||||||
|
{ id: 'cash_bank', name: 'Cash/Bank Report' },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'financial',
|
||||||
|
title: 'Financial Statement',
|
||||||
|
icon: '📑',
|
||||||
|
reports: [
|
||||||
|
{ id: 'tb', name: 'Trial Balance' },
|
||||||
|
{ id: 'bs', name: 'Balance Sheet (Schedule III)' },
|
||||||
|
{ id: 'pl', name: 'Statement of P&L (Schedule III)' },
|
||||||
|
{ id: 'cash_flow', name: 'Cash Flow Statement (AS-3)' },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{ id: 'sales', title: 'Sales Report', icon: '📈', reports: [{ id: 'party_sales', name: 'Party Wise Sale Report' }] },
|
||||||
|
{ id: 'purchase', title: 'Purchase Report', icon: '🛒', reports: [{ id: 'party_purchase', name: 'Party Wise Purchase Report' }] },
|
||||||
|
{ id: 'inventory', title: 'Inventory Report', icon: '📦', reports: [{ id: 'stock_summary', name: 'Stock Summary Report' }] },
|
||||||
|
{ id: 'receivable', title: 'Receivable Report', icon: '💰', reports: [{ id: 'ageing', name: 'Ageing Report' }] },
|
||||||
|
{ id: 'payable', title: 'Payable Report', icon: '💸', reports: [{ id: 'payable_ageing', name: 'Ageing Report' }] },
|
||||||
|
{ id: 'profit', title: 'Profit Report', icon: '💹', reports: [{ id: 'item_wise_profit', name: 'Item Wise Profit Report' }] },
|
||||||
|
];
|
||||||
|
|
||||||
|
// --- Statutory Mock Data Generators ---
|
||||||
|
|
||||||
|
const getTrialBalanceData = () => ({
|
||||||
|
title: "Trial Balance",
|
||||||
|
columns: ["Head of Accounts", "Debit Balance (Rs.)", "Credit Balance (Rs.)"],
|
||||||
|
data: [
|
||||||
|
["I. Share Capital", "", "15,00,000"],
|
||||||
|
["II. Reserves & Surplus", "", "5,50,000"],
|
||||||
|
["III. Long Term Borrowings", "", "25,00,000"],
|
||||||
|
["Land and Building", "45,00,000", ""],
|
||||||
|
["Plant and Machinery", "12,50,000", ""],
|
||||||
|
["Furniture and Fixtures", "3,50,000", ""],
|
||||||
|
["Cash in Hand", "45,000", ""],
|
||||||
|
["Bank Accounts", "8,20,000", ""],
|
||||||
|
["Purchases", "42,10,000", ""],
|
||||||
|
["Sales", "", "84,50,000"],
|
||||||
|
["Sundry Debtors", "12,50,000", ""],
|
||||||
|
["Sundry Creditors", "", "8,50,000"],
|
||||||
|
["Rent Expense", "2,25,000", ""],
|
||||||
|
["Salaries", "8,80,000", ""],
|
||||||
|
["Electricity Exp", "1,20,000", ""],
|
||||||
|
["Input GST", "2,40,000", ""],
|
||||||
|
["Output GST", "", "4,50,000"],
|
||||||
|
["Grand Total", "1,43,00,000", "1,43,00,000"]
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
const getBalanceSheetData = () => ({
|
||||||
|
title: "Balance Sheet",
|
||||||
|
period: "As at 31st March, 2024",
|
||||||
|
columns: ["Particulars", "Note No", "Figures as at end of\ncurrent reporting period", "Figures as at end of\nprevious reporting period"],
|
||||||
|
data: [
|
||||||
|
["I. EQUITY AND LIABILITIES", "", "", ""],
|
||||||
|
["1. Shareholders' Funds", "", "", ""],
|
||||||
|
["(a) Share Capital", "1", "15,00,000", "15,00,000"],
|
||||||
|
["(b) Reserves and Surplus", "2", "8,45,000", "5,50,000"],
|
||||||
|
["(c) Money received against share warrants", "", "-", "-"],
|
||||||
|
["2. Share application money pending allotment", "", "-", "-"],
|
||||||
|
["3. Non-Current Liabilities", "", "", ""],
|
||||||
|
["(a) Long-term borrowings", "3", "25,00,000", "28,00,000"],
|
||||||
|
["(b) Deferred tax liabilities (Net)", "", "45,000", "40,000"],
|
||||||
|
["(c) Other Long term liabilities", "", "-", "-"],
|
||||||
|
["(d) Long-term provisions", "", "1,20,000", "1,00,000"],
|
||||||
|
["4. Current Liabilities", "", "", ""],
|
||||||
|
["(a) Short-term borrowings", "", "5,00,000", "2,00,000"],
|
||||||
|
["(b) Trade payables", "4", "8,50,000", "6,50,000"],
|
||||||
|
["(c) Other current liabilities", "5", "2,40,000", "1,80,000"],
|
||||||
|
["(d) Short-term provisions", "6", "85,000", "60,000"],
|
||||||
|
["Total", "", "66,85,000", "60,80,000"],
|
||||||
|
["", "", "", ""],
|
||||||
|
["II. ASSETS", "", "", ""],
|
||||||
|
["1. Non-Current Assets", "", "", ""],
|
||||||
|
["(a) Property, Plant and Equipment and Intangible Assets", "", "", ""],
|
||||||
|
["(i) Property, Plant and Equipment", "7", "48,50,000", "45,00,000"],
|
||||||
|
["(ii) Intangible assets", "", "2,50,000", "2,80,000"],
|
||||||
|
["(iii) Capital work-in-progress", "", "-", "5,00,000"],
|
||||||
|
["(b) Non-current investments", "", "5,00,000", "2,00,000"],
|
||||||
|
["(c) Deferred tax assets (net)", "", "-", "-"],
|
||||||
|
["(d) Long-term loans and advances", "", "-", "-"],
|
||||||
|
["2. Current Assets", "", "", ""],
|
||||||
|
["(a) Current investments", "", "-", "-"],
|
||||||
|
["(b) Inventories", "8", "6,50,000", "4,20,000"],
|
||||||
|
["(c) Trade receivables", "9", "12,50,000", "8,50,000"],
|
||||||
|
["(d) Cash and cash equivalents", "10", "1,85,000", "1,30,000"],
|
||||||
|
["(e) Short-term loans and advances", "", "-", "-"],
|
||||||
|
["Total", "", "66,85,000", "60,80,000"],
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
const getPLData = () => ({
|
||||||
|
title: "Statement of Profit and Loss",
|
||||||
|
period: "For the year ended 31st March, 2024",
|
||||||
|
columns: ["Particulars", "Note No", "Figures for the\ncurrent reporting period", "Figures for the\nprevious reporting period"],
|
||||||
|
data: [
|
||||||
|
["I. Revenue from Operations", "11", "84,50,000", "72,00,000"],
|
||||||
|
["II. Other Income", "12", "1,20,000", "85,000"],
|
||||||
|
["III. Total Revenue (I + II)", "", "85,70,000", "72,85,000"],
|
||||||
|
["", "", "", ""],
|
||||||
|
["IV. Expenses:", "", "", ""],
|
||||||
|
["cost of materials consumed", "", "35,00,000", "30,00,000"],
|
||||||
|
["Purchases of Stock-in-Trade", "13", "12,00,000", "10,00,000"],
|
||||||
|
["Changes in inventories of finished goods", "14", "(2,30,000)", "50,000"],
|
||||||
|
["Employee benefits expense", "15", "18,50,000", "15,00,000"],
|
||||||
|
["Finance costs", "16", "2,40,000", "2,60,000"],
|
||||||
|
["Depreciation and amortization expense", "7", "4,50,000", "4,20,000"],
|
||||||
|
["Other expenses", "17", "8,50,000", "6,50,000"],
|
||||||
|
["Total Expenses (IV)", "", "78,60,000", "68,80,000"],
|
||||||
|
["", "", "", ""],
|
||||||
|
["V. Profit before exceptional and extraordinary items and tax (III - IV)", "", "7,10,000", "4,05,000"],
|
||||||
|
["VI. Exceptional items", "", "-", "-"],
|
||||||
|
["VII. Profit before tax (V - VI)", "", "7,10,000", "4,05,000"],
|
||||||
|
["VIII. Tax expense:", "", "", ""],
|
||||||
|
["(1) Current tax", "", "1,85,000", "1,05,000"],
|
||||||
|
["(2) Deferred tax", "", "15,000", "10,000"],
|
||||||
|
["IX. Profit (Loss) for the period (VII - VIII)", "", "5,10,000", "2,90,000"]
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
const getCashFlowData = () => ({
|
||||||
|
title: "Cash Flow Statement (Indirect Method)",
|
||||||
|
period: "For the year ended 31st March, 2024",
|
||||||
|
columns: ["Particulars", "Amount (Rs)", "Amount (Rs)"],
|
||||||
|
data: [
|
||||||
|
["A. Cash Flow from Operating Activities", "", ""],
|
||||||
|
["Net Profit before Taxation and Extraordinary Items", "", "7,10,000"],
|
||||||
|
["Adjustments for:", "", ""],
|
||||||
|
[" Depreciation", "4,50,000", ""],
|
||||||
|
[" Interest Income", "(45,000)", ""],
|
||||||
|
[" Finance Cost", "2,40,000", ""],
|
||||||
|
["Operating Profit before Working Capital Changes", "", "13,55,000"],
|
||||||
|
["Adjustments for Working Capital:", "", ""],
|
||||||
|
[" Increase in Trade Receivables", "(4,00,000)", ""],
|
||||||
|
[" Increase in Inventories", "(2,30,000)", ""],
|
||||||
|
[" Increase in Trade Payables", "2,00,000", ""],
|
||||||
|
["Cash Generated from Operations", "", "9,25,000"],
|
||||||
|
[" Income Tax Paid", "", "(1,85,000)"],
|
||||||
|
["Net Cash from Operating Activities (A)", "", "7,40,000"],
|
||||||
|
["", "", ""],
|
||||||
|
["B. Cash Flow from Investing Activities", "", ""],
|
||||||
|
[" Purchase of Fixed Assets", "(8,00,000)", ""],
|
||||||
|
[" Purchase of Investments", "(3,00,000)", ""],
|
||||||
|
[" Interest Received", "45,00,0", ""], // Intentional type check mock
|
||||||
|
["Net Cash used in Investing Activities (B)", "", "(10,55,000)"],
|
||||||
|
["", "", ""],
|
||||||
|
["C. Cash Flow from Financing Activities", "", ""],
|
||||||
|
[" Proceeds from Long Term Borrowings", "-", ""],
|
||||||
|
[" Repayment of Long Term Borrowings", "(3,00,000)", ""],
|
||||||
|
[" Short term borrowings (Net)", "3,00,000", ""],
|
||||||
|
[" Finance Cost Paid", "(2,40,000)", ""],
|
||||||
|
["Net Cash used in Financing Activities (C)", "", "(2,40,000)"],
|
||||||
|
["", "", ""],
|
||||||
|
["Net Increase / (Decrease) in Cash (A+B+C)", "", "(5,55,000)"],
|
||||||
|
["Cash and Cash Equivalents at Beginning of period", "", "7,40,000"],
|
||||||
|
["Cash and Cash Equivalents at End of period", "", "1,85,000"],
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function ReportsPage() {
|
||||||
|
const [view, setView] = useState<'dashboard' | 'report'>('dashboard');
|
||||||
|
const [selectedReport, setSelectedReport] = useState<Report | null>(null);
|
||||||
|
const { showToast } = useToast();
|
||||||
|
|
||||||
|
const handleReportClick = (report: Report) => {
|
||||||
|
setSelectedReport(report);
|
||||||
|
setView('report');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBack = () => {
|
||||||
|
setView('dashboard');
|
||||||
|
setSelectedReport(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDownload = () => {
|
||||||
|
if (!selectedReport) return;
|
||||||
|
|
||||||
|
// Data logic
|
||||||
|
let reportData;
|
||||||
|
if (selectedReport.id === 'tb') {
|
||||||
|
const d = getTrialBalanceData();
|
||||||
|
reportData = { ...d, orientation: 'portrait' as const };
|
||||||
|
} else if (selectedReport.id === 'bs') {
|
||||||
|
const d = getBalanceSheetData();
|
||||||
|
reportData = { ...d, orientation: 'portrait' as const };
|
||||||
|
} else if (selectedReport.id === 'pl') {
|
||||||
|
const d = getPLData();
|
||||||
|
reportData = { ...d, orientation: 'portrait' as const };
|
||||||
|
} else if (selectedReport.id === 'cash_flow') {
|
||||||
|
const d = getCashFlowData();
|
||||||
|
reportData = { ...d, orientation: 'portrait' as const };
|
||||||
|
} else {
|
||||||
|
// Generic Mock
|
||||||
|
reportData = {
|
||||||
|
title: selectedReport.name,
|
||||||
|
period: "As at 31st March, 2024",
|
||||||
|
columns: ["Date", "Particulars", "Voucher Type", "Amount"],
|
||||||
|
data: [
|
||||||
|
["01-04-2023", "Opening Balance", "BF", "0.00"],
|
||||||
|
["--", "No Data Available", "--", "--"],
|
||||||
|
],
|
||||||
|
orientation: 'portrait' as const
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
generatePDF(reportData);
|
||||||
|
showToast(`Downloaded ${selectedReport.name}`, 'success');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
{/* Header */}
|
||||||
|
<div className={styles.header}>
|
||||||
|
<div className={styles.titleGroup}>
|
||||||
|
{view === 'report' && (
|
||||||
|
<button onClick={handleBack} className={styles.btnSecondary} style={{ marginRight: '1rem' }}>
|
||||||
|
← Back
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
<h1>{view === 'dashboard' ? 'Financial Reports' : selectedReport?.name}</h1>
|
||||||
|
<p className={styles.subTitle}>
|
||||||
|
{view === 'dashboard'
|
||||||
|
? 'Statutory & Management Reports (Schedule III / AS-3)'
|
||||||
|
: `Formatted as per Indian Companies Act, 2013`}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{view === 'report' && (
|
||||||
|
<button className={styles.btnPrimary} onClick={handleDownload}>
|
||||||
|
⬇ Download PDF
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Dashboard View */}
|
||||||
|
{view === 'dashboard' && (
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))', gap: '1.5rem', paddingBottom: '2rem' }}>
|
||||||
|
{REPORT_CATEGORIES.map(category => (
|
||||||
|
<div key={category.id} className={styles.card} style={{ padding: '0', overflow: 'hidden', display: 'flex', flexDirection: 'column' }}>
|
||||||
|
<div style={{ padding: '1rem 1.5rem', background: 'rgba(255, 255, 255, 0.03)', borderBottom: '1px solid rgba(255, 255, 255, 0.05)', display: 'flex', alignItems: 'center', gap: '0.75rem' }}>
|
||||||
|
<span style={{ fontSize: '1.5rem' }}>{category.icon}</span>
|
||||||
|
<h3 style={{ margin: 0, fontSize: '1.1rem', color: 'var(--color-gold)' }}>{category.title}</h3>
|
||||||
|
</div>
|
||||||
|
<div style={{ padding: '0.5rem 0' }}>
|
||||||
|
{category.reports.map(report => (
|
||||||
|
<div key={report.id} onClick={() => handleReportClick(report)} className="report-item-hover" style={{ padding: '0.75rem 1.5rem', cursor: 'pointer', display: 'flex', justifyContent: 'space-between', alignItems: 'center', borderBottom: '1px solid rgba(255, 255, 255, 0.02)' }}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
|
||||||
|
<span style={{ color: '#666' }}>•</span>
|
||||||
|
<span style={{ color: '#e0e0e0', fontSize: '0.9rem' }}>{report.name}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Report Detail View */}
|
||||||
|
{view === 'report' && selectedReport && (
|
||||||
|
<div className={styles.card}>
|
||||||
|
{selectedReport.id === 'tb' && <StandardTable dataGenerator={getTrialBalanceData} />}
|
||||||
|
{selectedReport.id === 'bs' && <StandardTable dataGenerator={getBalanceSheetData} />}
|
||||||
|
{selectedReport.id === 'pl' && <StandardTable dataGenerator={getPLData} />}
|
||||||
|
{selectedReport.id === 'cash_flow' && <StandardTable dataGenerator={getCashFlowData} />}
|
||||||
|
|
||||||
|
{!['tb', 'bs', 'pl', 'cash_flow'].includes(selectedReport.id) && (
|
||||||
|
<div style={{ padding: '3rem', textAlign: 'center', color: '#666' }}>
|
||||||
|
<h2>{selectedReport.name}</h2>
|
||||||
|
<p>Standard format preview available on generation.</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Generic Table Render for Schedule III ---
|
||||||
|
function StandardTable({ dataGenerator }: { dataGenerator: () => any }) {
|
||||||
|
const data = dataGenerator();
|
||||||
|
return (
|
||||||
|
<div className={styles.tableContainer}>
|
||||||
|
<h3 style={{ textAlign: 'center', color: 'var(--color-gold)', marginBottom: '0.5rem' }}>{data.title}</h3>
|
||||||
|
<p style={{ textAlign: 'center', color: '#aaa', fontSize: '0.9rem', marginBottom: '1.5rem' }}>{data.period}</p>
|
||||||
|
<table className={styles.table}>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
{data.columns.map((col: string, i: number) => (
|
||||||
|
<th key={i} style={{ textAlign: i === 0 ? 'left' : 'right' }}>{col}</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{data.data.map((row: string[], i: number) => {
|
||||||
|
const isMainHead = row[0]?.match(/^([IVX]+\.|[0-9]+\.)\s/);
|
||||||
|
const isTotal = row[0]?.includes('Total');
|
||||||
|
const isSub = row[0]?.match(/^\([a-z]\)\s/);
|
||||||
|
|
||||||
|
const rowStyle = isTotal ? { fontWeight: 'bold', background: 'rgba(255,255,255,0.05)' } :
|
||||||
|
isMainHead ? { fontWeight: '600' } : {};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr key={i} style={rowStyle as any}>
|
||||||
|
{row.map((cell, j) => (
|
||||||
|
<td key={j} style={{
|
||||||
|
textAlign: j === 0 ? 'left' : 'right',
|
||||||
|
paddingLeft: j === 0 ? (isMainHead ? '0.5rem' : isSub ? '2rem' : '3rem') : '0.5rem',
|
||||||
|
color: (isTotal && j > 1) ? 'var(--color-gold)' : 'inherit'
|
||||||
|
}}>
|
||||||
|
{cell}
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
510
src/app/dashboard/appointments/appointments.module.css
Normal file
@@ -0,0 +1,510 @@
|
|||||||
|
/* Appointments Module - Luxury Tech Theme */
|
||||||
|
|
||||||
|
/* Base Container Styles */
|
||||||
|
.hamContainer {
|
||||||
|
padding: 2rem;
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Section Header */
|
||||||
|
.sectionHeader {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pageTitle {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
background: linear-gradient(135deg, #ffd700 0%, #ffed4e 100%);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pageSubtitle {
|
||||||
|
color: #888;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
margin: 0.5rem 0 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dashboard Widgets Grid */
|
||||||
|
.widgetsGrid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||||
|
gap: 1.5rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.widget {
|
||||||
|
background: linear-gradient(135deg, rgba(255, 255, 255, 0.05) 0%, rgba(255, 255, 255, 0.02) 100%);
|
||||||
|
background: rgba(255, 255, 255, 0.03);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
-webkit-backdrop-filter: blur(10px);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.widget:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border-color: rgba(255, 215, 0, 0.3);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.widgetIcon {
|
||||||
|
font-size: 2rem;
|
||||||
|
opacity: 0.8;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.widgetValue {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #fff;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.widgetLabel {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #888;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Section Card */
|
||||||
|
.section {
|
||||||
|
background: linear-gradient(135deg, rgba(255, 255, 255, 0.05) 0%, rgba(255, 255, 255, 0.02) 100%);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Table Styles */
|
||||||
|
.table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table thead {
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table th {
|
||||||
|
text-align: left;
|
||||||
|
padding: 1rem;
|
||||||
|
color: #888;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table td {
|
||||||
|
padding: 1rem;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table tr:hover {
|
||||||
|
background: rgba(255, 215, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Buttons */
|
||||||
|
.btnPrimary {
|
||||||
|
background: linear-gradient(135deg, #ffd700 0%, #ffed4e 100%);
|
||||||
|
color: #000;
|
||||||
|
border: none;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btnPrimary:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 8px 16px rgba(255, 215, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btnSecondary {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
color: #fff;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btnSecondary:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.15);
|
||||||
|
border-color: rgba(255, 215, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Badge */
|
||||||
|
.badge {
|
||||||
|
padding: 0.4rem 0.8rem;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Status Colors */
|
||||||
|
.statusActive {
|
||||||
|
background: rgba(0, 255, 157, 0.2);
|
||||||
|
color: #00ff9d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statusPending {
|
||||||
|
background: rgba(255, 215, 0, 0.2);
|
||||||
|
color: #ffd700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statusInactive {
|
||||||
|
background: rgba(255, 68, 68, 0.2);
|
||||||
|
color: #ff4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Filter Bar */
|
||||||
|
.filterBar {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filterGroup {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filterLabel {
|
||||||
|
color: #888;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filterSelect,
|
||||||
|
.inputField {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
color: #fff;
|
||||||
|
padding: 0.6rem 1rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filterSelect:focus,
|
||||||
|
.inputField:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: rgba(255, 215, 0, 0.5);
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Empty State */
|
||||||
|
.emptyState {
|
||||||
|
text-align: center;
|
||||||
|
padding: 4rem 2rem;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emptyStateIcon {
|
||||||
|
font-size: 4rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emptyStateTitle {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #888;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emptyStateText {
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Appointment-Specific Styles */
|
||||||
|
|
||||||
|
/* Slot Picker Specific Styles */
|
||||||
|
.slotPicker {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||||||
|
gap: 0.75rem;
|
||||||
|
margin: 1.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slot {
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 8px;
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slot:hover:not(.slotDisabled) {
|
||||||
|
background: rgba(255, 215, 0, 0.1);
|
||||||
|
border-color: rgba(255, 215, 0, 0.3);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slot.slotSelected {
|
||||||
|
background: linear-gradient(135deg, #ffd700 0%, #ffed4e 100%);
|
||||||
|
color: #000;
|
||||||
|
border-color: #ffd700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slotDisabled {
|
||||||
|
opacity: 0.3;
|
||||||
|
cursor: not-allowed;
|
||||||
|
background: rgba(255, 255, 255, 0.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Wizard Steps */
|
||||||
|
.wizard {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wizard::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 2px;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wizardStep {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wizardStepCircle {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border: 2px solid rgba(255, 255, 255, 0.2);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-weight: 700;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wizardStep.active .wizardStepCircle {
|
||||||
|
background: linear-gradient(135deg, #ffd700 0%, #ffed4e 100%);
|
||||||
|
border-color: #ffd700;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wizardStep.completed .wizardStepCircle {
|
||||||
|
background: rgba(0, 255, 157, 0.2);
|
||||||
|
border-color: #00ff9d;
|
||||||
|
color: #00ff9d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wizardStepLabel {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #888;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wizardStep.active .wizardStepLabel {
|
||||||
|
color: #ffd700;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Appointment Type Card */
|
||||||
|
.typeCard {
|
||||||
|
background: linear-gradient(135deg, rgba(255, 255, 255, 0.05) 0%, rgba(255, 255, 255, 0.02) 100%);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typeCard:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
border-color: rgba(255, 215, 0, 0.3);
|
||||||
|
box-shadow: 0 8px 24px rgba(255, 215, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.typeCard.selected {
|
||||||
|
border-color: #ffd700;
|
||||||
|
background: linear-gradient(135deg, rgba(255, 215, 0, 0.1) 0%, rgba(255, 215, 0, 0.05) 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.typeCardTitle {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #fff;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typeCardDuration {
|
||||||
|
color: #888;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typeCardDescription {
|
||||||
|
color: #aaa;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typeCardPrice {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #ffd700;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Calendar Specific */
|
||||||
|
.calendarHeader {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendarNav {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendarNavBtn {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendarNavBtn:hover {
|
||||||
|
background: rgba(255, 215, 0, 0.1);
|
||||||
|
border-color: rgba(255, 215, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendarGrid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(7, 1fr);
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendarCell {
|
||||||
|
aspect-ratio: 1;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0.5rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendarCellDate {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendarAppointment {
|
||||||
|
background: rgba(255, 215, 0, 0.2);
|
||||||
|
border-left: 3px solid #ffd700;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #fff;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Confirmation Screen */
|
||||||
|
.confirmationScreen {
|
||||||
|
text-align: center;
|
||||||
|
padding: 3rem 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirmationIcon {
|
||||||
|
font-size: 5rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirmationTitle {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #ffd700;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirmationId {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
color: #888;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirmationDetails {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 2rem;
|
||||||
|
margin: 2rem auto;
|
||||||
|
max-width: 500px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirmationRow {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0.75rem 0;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirmationRow:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirmationLabel {
|
||||||
|
color: #888;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirmationValue {
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
272
src/app/dashboard/appointments/book/page.tsx
Normal file
@@ -0,0 +1,272 @@
|
|||||||
|
'use client';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import styles from '../appointments.module.css';
|
||||||
|
import { useToast } from '@/context/ToastContext';
|
||||||
|
|
||||||
|
// Mock appointment types
|
||||||
|
const APPOINTMENT_TYPES = [
|
||||||
|
{ id: 1, title: 'Initial Consultation', duration: '30 min', description: 'First-time meeting to discuss your needs', price: 0 },
|
||||||
|
{ id: 2, title: 'Follow-up Meeting', duration: '20 min', description: 'Review progress and next steps', price: 0 },
|
||||||
|
{ id: 3, title: 'Technical Support', duration: '45 min', description: 'Get help with technical issues', price: 2500 },
|
||||||
|
{ id: 4, title: 'Strategy Session', duration: '60 min', description: 'In-depth planning and strategy discussion', price: 5000 }
|
||||||
|
];
|
||||||
|
|
||||||
|
// Mock time slots
|
||||||
|
const TIME_SLOTS = [
|
||||||
|
'09:00 AM', '09:30 AM', '10:00 AM', '10:30 AM', '11:00 AM', '11:30 AM',
|
||||||
|
'02:00 PM', '02:30 PM', '03:00 PM', '03:30 PM', '04:00 PM', '04:30 PM'
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function BookAppointmentPage() {
|
||||||
|
const router = useRouter();
|
||||||
|
const { showToast } = useToast();
|
||||||
|
const [currentStep, setCurrentStep] = useState(0);
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
typeId: null,
|
||||||
|
date: '',
|
||||||
|
timeSlot: '',
|
||||||
|
name: '',
|
||||||
|
email: '',
|
||||||
|
phone: '',
|
||||||
|
reason: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedType = APPOINTMENT_TYPES.find(t => t.id === formData.typeId);
|
||||||
|
|
||||||
|
const handleNext = () => {
|
||||||
|
// Validation for each step
|
||||||
|
if (currentStep === 0 && !formData.typeId) {
|
||||||
|
showToast('Please select an appointment type', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (currentStep === 1 && (!formData.date || !formData.timeSlot)) {
|
||||||
|
showToast('Please select date and time', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (currentStep === 2) {
|
||||||
|
if (!formData.name || !formData.email) {
|
||||||
|
showToast('Please fill in all required fields', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||||
|
if (!emailRegex.test(formData.email)) {
|
||||||
|
showToast('Please enter a valid email address', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setCurrentStep(currentStep + 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBack = () => {
|
||||||
|
setCurrentStep(currentStep - 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
const appointmentId = `A-${String(Math.floor(Math.random() * 1000)).padStart(3, '0')}`;
|
||||||
|
showToast(`✅ Appointment ${appointmentId} booked successfully!`, 'success');
|
||||||
|
router.push('/dashboard/appointments/my-appointments');
|
||||||
|
};
|
||||||
|
|
||||||
|
const steps = ['Select Type', 'Date & Time', 'Your Info', 'Confirm'];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.hamContainer}>
|
||||||
|
<div className={styles.sectionHeader}>
|
||||||
|
<div>
|
||||||
|
<h1 className={styles.pageTitle}>Book Appointment</h1>
|
||||||
|
<p className={styles.pageSubtitle}>Schedule your appointment in a few simple steps</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Wizard Steps */}
|
||||||
|
<div className={styles.wizard}>
|
||||||
|
{steps.map((step, index) => (
|
||||||
|
<div key={index} className={`${styles.wizardStep} ${index === currentStep ? styles.active : ''} ${index < currentStep ? styles.completed : ''}`}>
|
||||||
|
<div className={styles.wizardStepCircle}>
|
||||||
|
{index < currentStep ? '✓' : index + 1}
|
||||||
|
</div>
|
||||||
|
<div className={styles.wizardStepLabel}>{step}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Step Content */}
|
||||||
|
<div className={styles.section}>
|
||||||
|
{/* Step 0: Select Type */}
|
||||||
|
{currentStep === 0 && (
|
||||||
|
<div>
|
||||||
|
<h3 style={{ color: '#ffd700', marginBottom: '1.5rem' }}>Select Appointment Type</h3>
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))', gap: '1.5rem' }}>
|
||||||
|
{APPOINTMENT_TYPES.map(type => (
|
||||||
|
<div
|
||||||
|
key={type.id}
|
||||||
|
className={`${styles.typeCard} ${formData.typeId === type.id ? styles.selected : ''}`}
|
||||||
|
onClick={() => setFormData({ ...formData, typeId: type.id })}
|
||||||
|
>
|
||||||
|
<div className={styles.typeCardTitle}>{type.title}</div>
|
||||||
|
<div className={styles.typeCardDuration}>⏱️ {type.duration}</div>
|
||||||
|
<div className={styles.typeCardDescription}>{type.description}</div>
|
||||||
|
<div className={styles.typeCardPrice}>
|
||||||
|
{type.price === 0 ? 'Free' : `₹${type.price.toLocaleString('en-IN')}`}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Step 1: Date & Time */}
|
||||||
|
{currentStep === 1 && (
|
||||||
|
<div>
|
||||||
|
<h3 style={{ color: '#ffd700', marginBottom: '1.5rem' }}>Select Date & Time</h3>
|
||||||
|
<div style={{ marginBottom: '2rem' }}>
|
||||||
|
<label className={styles.filterLabel}>Select Date</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
className={styles.inputField}
|
||||||
|
value={formData.date}
|
||||||
|
onChange={(e) => setFormData({ ...formData, date: e.target.value })}
|
||||||
|
min={new Date().toISOString().split('T')[0]}
|
||||||
|
style={{ maxWidth: '300px' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{formData.date && (
|
||||||
|
<div>
|
||||||
|
<label className={styles.filterLabel}>Available Time Slots</label>
|
||||||
|
<div className={styles.slotPicker}>
|
||||||
|
{TIME_SLOTS.map(slot => (
|
||||||
|
<div
|
||||||
|
key={slot}
|
||||||
|
className={`${styles.slot} ${formData.timeSlot === slot ? styles.slotSelected : ''}`}
|
||||||
|
onClick={() => setFormData({ ...formData, timeSlot: slot })}
|
||||||
|
>
|
||||||
|
{slot}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Step 2: Customer Info */}
|
||||||
|
{currentStep === 2 && (
|
||||||
|
<div>
|
||||||
|
<h3 style={{ color: '#ffd700', marginBottom: '1.5rem' }}>Your Information</h3>
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem', maxWidth: '500px' }}>
|
||||||
|
<div>
|
||||||
|
<label className={styles.filterLabel}>Full Name *</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className={styles.inputField}
|
||||||
|
value={formData.name}
|
||||||
|
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||||
|
placeholder="John Doe"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={styles.filterLabel}>Email Address *</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
className={styles.inputField}
|
||||||
|
value={formData.email}
|
||||||
|
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
||||||
|
placeholder="john@example.com"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={styles.filterLabel}>Phone Number</label>
|
||||||
|
<input
|
||||||
|
type="tel"
|
||||||
|
className={styles.inputField}
|
||||||
|
value={formData.phone}
|
||||||
|
onChange={(e) => setFormData({ ...formData, phone: e.target.value })}
|
||||||
|
placeholder="+91 98765 43210"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={styles.filterLabel}>Reason for Appointment (Optional)</label>
|
||||||
|
<textarea
|
||||||
|
className={styles.inputField}
|
||||||
|
value={formData.reason}
|
||||||
|
onChange={(e) => setFormData({ ...formData, reason: e.target.value })}
|
||||||
|
placeholder="Brief description..."
|
||||||
|
rows={4}
|
||||||
|
style={{ resize: 'vertical' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Step 3: Confirmation */}
|
||||||
|
{currentStep === 3 && (
|
||||||
|
<div>
|
||||||
|
<h3 style={{ color: '#ffd700', marginBottom: '1.5rem' }}>Confirm Your Appointment</h3>
|
||||||
|
<div className={styles.confirmationDetails}>
|
||||||
|
<div className={styles.confirmationRow}>
|
||||||
|
<span className={styles.confirmationLabel}>Appointment Type:</span>
|
||||||
|
<span className={styles.confirmationValue}>{selectedType?.title}</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.confirmationRow}>
|
||||||
|
<span className={styles.confirmationLabel}>Duration:</span>
|
||||||
|
<span className={styles.confirmationValue}>{selectedType?.duration}</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.confirmationRow}>
|
||||||
|
<span className={styles.confirmationLabel}>Date:</span>
|
||||||
|
<span className={styles.confirmationValue}>{formData.date}</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.confirmationRow}>
|
||||||
|
<span className={styles.confirmationLabel}>Time:</span>
|
||||||
|
<span className={styles.confirmationValue}>{formData.timeSlot}</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.confirmationRow}>
|
||||||
|
<span className={styles.confirmationLabel}>Name:</span>
|
||||||
|
<span className={styles.confirmationValue}>{formData.name}</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.confirmationRow}>
|
||||||
|
<span className={styles.confirmationLabel}>Email:</span>
|
||||||
|
<span className={styles.confirmationValue}>{formData.email}</span>
|
||||||
|
</div>
|
||||||
|
{formData.phone && (
|
||||||
|
<div className={styles.confirmationRow}>
|
||||||
|
<span className={styles.confirmationLabel}>Phone:</span>
|
||||||
|
<span className={styles.confirmationValue}>{formData.phone}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{selectedType && selectedType.price > 0 && (
|
||||||
|
<div className={styles.confirmationRow} style={{ borderTop: '2px solid rgba(255,215,0,0.3)', marginTop: '1rem', paddingTop: '1rem' }}>
|
||||||
|
<span className={styles.confirmationLabel}>Total:</span>
|
||||||
|
<span className={styles.confirmationValue} style={{ color: '#ffd700', fontSize: '1.5rem' }}>
|
||||||
|
₹{selectedType.price.toLocaleString('en-IN')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Navigation Buttons */}
|
||||||
|
<div style={{ display: 'flex', gap: '1rem', justifyContent: 'flex-end', marginTop: '2rem' }}>
|
||||||
|
{currentStep > 0 && (
|
||||||
|
<button className={styles.btnSecondary} onClick={handleBack}>
|
||||||
|
← Back
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{currentStep < 3 && (
|
||||||
|
<button className={styles.btnPrimary} onClick={handleNext}>
|
||||||
|
Next →
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{currentStep === 3 && (
|
||||||
|
<button className={styles.btnPrimary} onClick={handleSubmit}>
|
||||||
|
Confirm Booking
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
271
src/app/dashboard/appointments/calendar/page.tsx
Normal file
@@ -0,0 +1,271 @@
|
|||||||
|
'use client';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import styles from '../appointments.module.css';
|
||||||
|
|
||||||
|
export default function CalendarPage() {
|
||||||
|
const [view, setView] = useState<'day' | 'week' | 'month'>('month');
|
||||||
|
const [currentDate, setCurrentDate] = useState(new Date());
|
||||||
|
|
||||||
|
// Mock appointments for calendar
|
||||||
|
const appointments = [
|
||||||
|
{ id: 1, day: 15, title: 'Consultation - John Doe', time: '10:00 AM', duration: 30 },
|
||||||
|
{ id: 2, day: 18, title: 'Follow-up - Sarah W.', time: '02:00 PM', duration: 20 },
|
||||||
|
{ id: 3, day: 22, title: 'Tech Support - Mike J.', time: '11:00 AM', duration: 45 },
|
||||||
|
{ id: 4, day: 25, title: 'Strategy - Emily C.', time: '03:00 PM', duration: 60 },
|
||||||
|
{ id: 5, day: currentDate.getDate(), title: 'Meeting - David L.', time: '09:00 AM', duration: 30 },
|
||||||
|
{ id: 6, day: currentDate.getDate(), title: 'Call - Lisa P.', time: '02:30 PM', duration: 20 }
|
||||||
|
];
|
||||||
|
|
||||||
|
const monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
|
||||||
|
const dayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
||||||
|
|
||||||
|
const generateCalendar = () => {
|
||||||
|
const year = currentDate.getFullYear();
|
||||||
|
const month = currentDate.getMonth();
|
||||||
|
const firstDay = new Date(year, month, 1).getDay();
|
||||||
|
const daysInMonth = new Date(year, month + 1, 0).getDate();
|
||||||
|
|
||||||
|
const days = [];
|
||||||
|
|
||||||
|
// Add empty cells for days before month starts
|
||||||
|
for (let i = 0; i < firstDay; i++) {
|
||||||
|
days.push({ day: null, appointments: [] });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add days of the month
|
||||||
|
for (let i = 1; i <= daysInMonth; i++) {
|
||||||
|
const dayAppointments = appointments.filter(a => a.day === i);
|
||||||
|
days.push({ day: i, appointments: dayAppointments });
|
||||||
|
}
|
||||||
|
|
||||||
|
return days;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getWeekDays = () => {
|
||||||
|
const startOfWeek = new Date(currentDate);
|
||||||
|
startOfWeek.setDate(currentDate.getDate() - currentDate.getDay());
|
||||||
|
|
||||||
|
const weekDays = [];
|
||||||
|
for (let i = 0; i < 7; i++) {
|
||||||
|
const day = new Date(startOfWeek);
|
||||||
|
day.setDate(startOfWeek.getDate() + i);
|
||||||
|
const dayAppointments = appointments.filter(a => a.day === day.getDate() && day.getMonth() === currentDate.getMonth());
|
||||||
|
weekDays.push({ date: day, appointments: dayAppointments });
|
||||||
|
}
|
||||||
|
return weekDays;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDayAppointments = () => {
|
||||||
|
return appointments.filter(a => a.day === currentDate.getDate());
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePrevious = () => {
|
||||||
|
const newDate = new Date(currentDate);
|
||||||
|
if (view === 'day') {
|
||||||
|
newDate.setDate(currentDate.getDate() - 1);
|
||||||
|
} else if (view === 'week') {
|
||||||
|
newDate.setDate(currentDate.getDate() - 7);
|
||||||
|
} else {
|
||||||
|
newDate.setMonth(currentDate.getMonth() - 1);
|
||||||
|
}
|
||||||
|
setCurrentDate(newDate);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNext = () => {
|
||||||
|
const newDate = new Date(currentDate);
|
||||||
|
if (view === 'day') {
|
||||||
|
newDate.setDate(currentDate.getDate() + 1);
|
||||||
|
} else if (view === 'week') {
|
||||||
|
newDate.setDate(currentDate.getDate() + 7);
|
||||||
|
} else {
|
||||||
|
newDate.setMonth(currentDate.getMonth() + 1);
|
||||||
|
}
|
||||||
|
setCurrentDate(newDate);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToday = () => {
|
||||||
|
setCurrentDate(new Date());
|
||||||
|
};
|
||||||
|
|
||||||
|
const calendarDays = generateCalendar();
|
||||||
|
const weekDays = getWeekDays();
|
||||||
|
const dayAppointments = getDayAppointments();
|
||||||
|
|
||||||
|
const getDateLabel = () => {
|
||||||
|
if (view === 'day') {
|
||||||
|
return `${dayNames[currentDate.getDay()]}, ${monthNames[currentDate.getMonth()]} ${currentDate.getDate()}, ${currentDate.getFullYear()}`;
|
||||||
|
} else if (view === 'week') {
|
||||||
|
const weekStart = new Date(currentDate);
|
||||||
|
weekStart.setDate(currentDate.getDate() - currentDate.getDay());
|
||||||
|
const weekEnd = new Date(weekStart);
|
||||||
|
weekEnd.setDate(weekStart.getDate() + 6);
|
||||||
|
return `${monthNames[weekStart.getMonth()]} ${weekStart.getDate()} - ${monthNames[weekEnd.getMonth()]} ${weekEnd.getDate()}, ${currentDate.getFullYear()}`;
|
||||||
|
} else {
|
||||||
|
return `${monthNames[currentDate.getMonth()]} ${currentDate.getFullYear()}`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const timeSlots = ['09:00 AM', '10:00 AM', '11:00 AM', '12:00 PM', '01:00 PM', '02:00 PM', '03:00 PM', '04:00 PM', '05:00 PM'];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.hamContainer}>
|
||||||
|
<div className={styles.sectionHeader}>
|
||||||
|
<div>
|
||||||
|
<h1 className={styles.pageTitle}>Appointment Calendar</h1>
|
||||||
|
<p className={styles.pageSubtitle}>View your schedule at a glance</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* View Selector */}
|
||||||
|
<div className={styles.section}>
|
||||||
|
<div className={styles.calendarHeader}>
|
||||||
|
<div style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
|
||||||
|
<button className={styles.btnSecondary} onClick={handlePrevious} style={{ padding: '0.5rem 1rem' }}>
|
||||||
|
← Prev
|
||||||
|
</button>
|
||||||
|
<button className={styles.btnPrimary} onClick={handleToday} style={{ padding: '0.5rem 1rem' }}>
|
||||||
|
Today
|
||||||
|
</button>
|
||||||
|
<button className={styles.btnSecondary} onClick={handleNext} style={{ padding: '0.5rem 1rem' }}>
|
||||||
|
Next →
|
||||||
|
</button>
|
||||||
|
<h3 style={{ color: '#ffd700', margin: 0, minWidth: '300px' }}>{getDateLabel()}</h3>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', gap: '1rem' }}>
|
||||||
|
{['day', 'week', 'month'].map((v) => (
|
||||||
|
<button
|
||||||
|
key={v}
|
||||||
|
className={view === v ? styles.btnPrimary : styles.btnSecondary}
|
||||||
|
onClick={() => setView(v as any)}
|
||||||
|
style={{ padding: '0.5rem 1rem', textTransform: 'capitalize' }}
|
||||||
|
>
|
||||||
|
{v}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Day View */}
|
||||||
|
{view === 'day' && (
|
||||||
|
<div>
|
||||||
|
<div style={{ background: 'rgba(255,255,255,0.03)', borderRadius: '8px', padding: '1rem' }}>
|
||||||
|
{timeSlots.map(time => {
|
||||||
|
const appointment = dayAppointments.find(a => a.time === time);
|
||||||
|
return (
|
||||||
|
<div key={time} style={{
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: '100px 1fr',
|
||||||
|
gap: '1rem',
|
||||||
|
padding: '1rem',
|
||||||
|
borderBottom: '1px solid rgba(255,255,255,0.05)'
|
||||||
|
}}>
|
||||||
|
<div style={{ color: '#888', fontWeight: 600 }}>{time}</div>
|
||||||
|
{appointment ? (
|
||||||
|
<div style={{
|
||||||
|
background: 'rgba(255,215,0,0.2)',
|
||||||
|
border: '1px solid rgba(255,215,0,0.3)',
|
||||||
|
borderLeft: '4px solid #ffd700',
|
||||||
|
padding: '1rem',
|
||||||
|
borderRadius: '8px'
|
||||||
|
}}>
|
||||||
|
<div style={{ fontWeight: 600, color: '#fff', marginBottom: '0.3rem' }}>
|
||||||
|
{appointment.title}
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: '0.85rem', color: '#888' }}>
|
||||||
|
{appointment.duration} minutes
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div style={{ color: '#666', fontStyle: 'italic' }}>No appointments</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Week View */}
|
||||||
|
{view === 'week' && (
|
||||||
|
<div>
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)', gap: '0.5rem', marginBottom: '0.5rem' }}>
|
||||||
|
{weekDays.map((day, index) => (
|
||||||
|
<div key={index} style={{
|
||||||
|
textAlign: 'center',
|
||||||
|
padding: '1rem',
|
||||||
|
background: day.date.toDateString() === currentDate.toDateString() ? 'rgba(255,215,0,0.2)' : 'rgba(255,255,255,0.03)',
|
||||||
|
borderRadius: '8px',
|
||||||
|
border: day.date.toDateString() === currentDate.toDateString() ? '1px solid rgba(255,215,0,0.3)' : '1px solid rgba(255,255,255,0.1)'
|
||||||
|
}}>
|
||||||
|
<div style={{ color: '#888', fontSize: '0.85rem', marginBottom: '0.5rem' }}>
|
||||||
|
{dayNames[day.date.getDay()].substring(0, 3)}
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: '1.5rem', fontWeight: 700, color: day.date.toDateString() === currentDate.toDateString() ? '#ffd700' : '#fff', marginBottom: '1rem' }}>
|
||||||
|
{day.date.getDate()}
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
|
||||||
|
{day.appointments.map(apt => (
|
||||||
|
<div key={apt.id} style={{
|
||||||
|
background: 'rgba(255,215,0,0.2)',
|
||||||
|
borderLeft: '3px solid #ffd700',
|
||||||
|
padding: '0.5rem',
|
||||||
|
borderRadius: '4px',
|
||||||
|
fontSize: '0.75rem',
|
||||||
|
textAlign: 'left'
|
||||||
|
}}>
|
||||||
|
<div style={{ fontWeight: 600, marginBottom: '0.2rem' }}>{apt.time}</div>
|
||||||
|
<div style={{ color: '#aaa' }}>{apt.title.split(' - ')[0]}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{day.appointments.length === 0 && (
|
||||||
|
<div style={{ color: '#666', fontSize: '0.75rem', fontStyle: 'italic' }}>No appointments</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Month View */}
|
||||||
|
{view === 'month' && (
|
||||||
|
<>
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)', gap: '0.5rem', marginBottom: '0.5rem' }}>
|
||||||
|
{['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].map(day => (
|
||||||
|
<div key={day} style={{ textAlign: 'center', color: '#888', fontSize: '0.85rem', padding: '0.5rem', fontWeight: 600 }}>
|
||||||
|
{day}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className={styles.calendarGrid}>
|
||||||
|
{calendarDays.map((item, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className={styles.calendarCell}
|
||||||
|
style={{
|
||||||
|
opacity: item.day ? 1 : 0.3,
|
||||||
|
background: item.day === currentDate.getDate() ? 'rgba(255,215,0,0.1)' : undefined,
|
||||||
|
border: item.day === currentDate.getDate() ? '1px solid rgba(255,215,0,0.3)' : '1px solid rgba(255,255,255,0.1)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.day && (
|
||||||
|
<>
|
||||||
|
<div className={styles.calendarCellDate} style={{ color: item.day === currentDate.getDate() ? '#ffd700' : '#888' }}>
|
||||||
|
{item.day}
|
||||||
|
</div>
|
||||||
|
{item.appointments.map(apt => (
|
||||||
|
<div key={apt.id} className={styles.calendarAppointment} title={`${apt.time} - ${apt.title}`}>
|
||||||
|
{apt.title}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
204
src/app/dashboard/appointments/my-appointments/page.tsx
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
'use client';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import styles from '../appointments.module.css';
|
||||||
|
import { useToast } from '@/context/ToastContext';
|
||||||
|
|
||||||
|
// Mock data
|
||||||
|
const MOCK_APPOINTMENTS = [
|
||||||
|
{ id: 'A-001', type: 'Consultation', date: '2024-12-25', time: '10:00 AM', status: 'Confirmed', customer: 'John Doe' },
|
||||||
|
{ id: 'A-002', type: 'Follow-up', date: '2024-12-26', time: '02:00 PM', status: 'Confirmed', customer: 'Sarah Williams' },
|
||||||
|
{ id: 'A-003', type: 'Technical Support', date: '2024-12-20', time: '11:00 AM', status: 'Completed', customer: 'Mike Johnson' },
|
||||||
|
{ id: 'A-004', type: 'Strategy Session', date: '2024-12-28', time: '03:00 PM', status: 'Pending', customer: 'Emily Chen' },
|
||||||
|
{ id: 'A-005', type: 'Consultation', date: '2024-12-18', time: '09:00 AM', status: 'Cancelled', customer: 'David Lee' }
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function MyAppointmentsPage() {
|
||||||
|
const { showToast } = useToast();
|
||||||
|
const [appointments, setAppointments] = useState(MOCK_APPOINTMENTS);
|
||||||
|
const [filterStatus, setFilterStatus] = useState('all');
|
||||||
|
const [selectedAppointment, setSelectedAppointment] = useState<typeof MOCK_APPOINTMENTS[0] | null>(null);
|
||||||
|
const [showCancelModal, setShowCancelModal] = useState(false);
|
||||||
|
const [cancelReason, setCancelReason] = useState('');
|
||||||
|
|
||||||
|
const filteredAppointments = appointments.filter(apt => {
|
||||||
|
if (filterStatus === 'all') return true;
|
||||||
|
if (filterStatus === 'upcoming') return apt.status === 'Confirmed' || apt.status === 'Pending';
|
||||||
|
if (filterStatus === 'past') return apt.status === 'Completed' || apt.status === 'Cancelled';
|
||||||
|
return apt.status.toLowerCase() === filterStatus;
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleCancel = (apt: typeof MOCK_APPOINTMENTS[0]) => {
|
||||||
|
setSelectedAppointment(apt);
|
||||||
|
setShowCancelModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmCancel = () => {
|
||||||
|
if (selectedAppointment) {
|
||||||
|
setAppointments(appointments.map(a =>
|
||||||
|
a.id === selectedAppointment.id ? { ...a, status: 'Cancelled' } : a
|
||||||
|
));
|
||||||
|
showToast(`Appointment ${selectedAppointment.id} cancelled`, 'success');
|
||||||
|
setShowCancelModal(false);
|
||||||
|
setCancelReason('');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusBadge = (status: string) => {
|
||||||
|
const statusMap: Record<string, string> = {
|
||||||
|
'Confirmed': styles.statusActive,
|
||||||
|
'Pending': styles.statusPending,
|
||||||
|
'Completed': styles.statusActive,
|
||||||
|
'Cancelled': styles.statusInactive
|
||||||
|
};
|
||||||
|
return <span className={`${styles.badge} ${statusMap[status]}`}>{status}</span>;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.hamContainer}>
|
||||||
|
{/* Cancel Modal */}
|
||||||
|
{showCancelModal && (
|
||||||
|
<div style={{
|
||||||
|
position: 'fixed',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
background: 'rgba(0,0,0,0.7)',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
zIndex: 9999,
|
||||||
|
backdropFilter: 'blur(4px)'
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
|
background: 'linear-gradient(135deg, rgba(20,25,35,0.95) 0%, rgba(30,35,45,0.95) 100%)',
|
||||||
|
border: '1px solid rgba(255,215,0,0.3)',
|
||||||
|
borderRadius: '12px',
|
||||||
|
padding: '2rem',
|
||||||
|
maxWidth: '500px',
|
||||||
|
width: '90%',
|
||||||
|
boxShadow: '0 20px 60px rgba(0,0,0,0.5)'
|
||||||
|
}}>
|
||||||
|
<h3 style={{ margin: '0 0 1.5rem 0', color: '#ffd700', fontSize: '1.5rem' }}>
|
||||||
|
Cancel Appointment
|
||||||
|
</h3>
|
||||||
|
<p style={{ color: '#888', marginBottom: '1.5rem' }}>
|
||||||
|
Are you sure you want to cancel appointment {selectedAppointment?.id}?
|
||||||
|
</p>
|
||||||
|
<div>
|
||||||
|
<label className={styles.filterLabel}>Reason (Optional)</label>
|
||||||
|
<textarea
|
||||||
|
className={styles.inputField}
|
||||||
|
value={cancelReason}
|
||||||
|
onChange={(e) => setCancelReason(e.target.value)}
|
||||||
|
placeholder="Please provide a reason..."
|
||||||
|
rows={3}
|
||||||
|
style={{ resize: 'vertical' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', gap: '1rem', justifyContent: 'flex-end', marginTop: '2rem' }}>
|
||||||
|
<button
|
||||||
|
className={styles.btnSecondary}
|
||||||
|
onClick={() => setShowCancelModal(false)}
|
||||||
|
>
|
||||||
|
Keep Appointment
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={styles.btnPrimary}
|
||||||
|
onClick={confirmCancel}
|
||||||
|
style={{ background: 'linear-gradient(135deg, #ff4444 0%, #ff6666 100%)' }}
|
||||||
|
>
|
||||||
|
Confirm Cancellation
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className={styles.sectionHeader}>
|
||||||
|
<div>
|
||||||
|
<h1 className={styles.pageTitle}>My Appointments</h1>
|
||||||
|
<p className={styles.pageSubtitle}>View and manage your bookings</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Filters */}
|
||||||
|
<div className={styles.filterBar}>
|
||||||
|
<div className={styles.filterGroup}>
|
||||||
|
<label className={styles.filterLabel}>Filter by Status</label>
|
||||||
|
<select
|
||||||
|
className={styles.filterSelect}
|
||||||
|
value={filterStatus}
|
||||||
|
onChange={(e) => setFilterStatus(e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="all">All Appointments</option>
|
||||||
|
<option value="upcoming">Upcoming</option>
|
||||||
|
<option value="confirmed">Confirmed</option>
|
||||||
|
<option value="pending">Pending</option>
|
||||||
|
<option value="past">Past</option>
|
||||||
|
<option value="cancelled">Cancelled</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Appointments Table */}
|
||||||
|
<div className={styles.section}>
|
||||||
|
<table className={styles.table}>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>Date</th>
|
||||||
|
<th>Time</th>
|
||||||
|
<th>Customer</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{filteredAppointments.map(apt => (
|
||||||
|
<tr key={apt.id}>
|
||||||
|
<td style={{ color: '#888', fontSize: '0.9rem', fontWeight: 600 }}>{apt.id}</td>
|
||||||
|
<td style={{ fontWeight: 600 }}>{apt.type}</td>
|
||||||
|
<td>{apt.date}</td>
|
||||||
|
<td>{apt.time}</td>
|
||||||
|
<td>{apt.customer}</td>
|
||||||
|
<td>{getStatusBadge(apt.status)}</td>
|
||||||
|
<td>
|
||||||
|
<div style={{ display: 'flex', gap: '0.5rem' }}>
|
||||||
|
{(apt.status === 'Confirmed' || apt.status === 'Pending') && (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
className={styles.btnSecondary}
|
||||||
|
style={{ padding: '0.4rem 0.8rem', fontSize: '0.8rem' }}
|
||||||
|
onClick={() => showToast('Reschedule feature coming soon', 'info')}
|
||||||
|
>
|
||||||
|
Reschedule
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={styles.btnPrimary}
|
||||||
|
style={{ padding: '0.4rem 0.8rem', fontSize: '0.8rem', background: 'linear-gradient(135deg, #ff4444 0%, #ff6666 100%)' }}
|
||||||
|
onClick={() => handleCancel(apt)}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
{filteredAppointments.length === 0 && (
|
||||||
|
<div className={styles.emptyState}>
|
||||||
|
<div className={styles.emptyStateIcon}>📅</div>
|
||||||
|
<div className={styles.emptyStateTitle}>No appointments found</div>
|
||||||
|
<div className={styles.emptyStateText}>Try adjusting your filters or book a new appointment</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
300
src/app/dashboard/appointments/page.tsx
Normal file
@@ -0,0 +1,300 @@
|
|||||||
|
'use client';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import styles from './appointments.module.css';
|
||||||
|
import shared from '../shared/components.module.css';
|
||||||
|
import { useToast } from '@/context/ToastContext';
|
||||||
|
|
||||||
|
// Mock data
|
||||||
|
const MOCK_STATS = {
|
||||||
|
todayAppointments: 8,
|
||||||
|
upcomingAppointments: 24,
|
||||||
|
completedThisWeek: 15,
|
||||||
|
pendingConfirmation: 3
|
||||||
|
};
|
||||||
|
|
||||||
|
const MOCK_TODAY_SCHEDULE = [
|
||||||
|
{ id: 'A-001', time: '09:00 AM', customer: 'John Doe', type: 'Consultation', status: 'Confirmed' },
|
||||||
|
{ id: 'A-002', time: '10:30 AM', customer: 'Sarah Williams', type: 'Follow-up', status: 'Confirmed' },
|
||||||
|
{ id: 'A-003', time: '02:00 PM', customer: 'Mike Johnson', type: 'Initial Meeting', status: 'Pending' },
|
||||||
|
{ id: 'A-004', time: '04:00 PM', customer: 'Emily Chen', type: 'Consultation', status: 'Confirmed' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const MOCK_UPCOMING = [
|
||||||
|
{ id: 'A-005', date: 'Tomorrow', time: '11:00 AM', customer: 'David Lee', type: 'Consultation' },
|
||||||
|
{ id: 'A-006', date: 'Dec 22', time: '03:00 PM', customer: 'Lisa Park', type: 'Follow-up' },
|
||||||
|
{ id: 'A-007', date: 'Dec 23', time: '10:00 AM', customer: 'Tom Brown', type: 'Initial Meeting' }
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function AppointmentsDashboard() {
|
||||||
|
const [stats] = useState(MOCK_STATS);
|
||||||
|
const [showShareModal, setShowShareModal] = useState(false);
|
||||||
|
const bookingUrl = typeof window !== 'undefined' ? `${window.location.origin}/book` : 'https://yoursite.com/book';
|
||||||
|
|
||||||
|
const handleCopyLink = () => {
|
||||||
|
navigator.clipboard.writeText(bookingUrl);
|
||||||
|
alert('Booking link copied to clipboard!');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleShareLink = () => {
|
||||||
|
setShowShareModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.hamContainer}>
|
||||||
|
{/* Share Link Modal */}
|
||||||
|
{showShareModal && (
|
||||||
|
<div style={{
|
||||||
|
position: 'fixed',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
background: 'rgba(0,0,0,0.8)',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
zIndex: 9999,
|
||||||
|
backdropFilter: 'blur(8px)'
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
|
background: 'linear-gradient(135deg, rgba(15,20,30,0.98) 0%, rgba(25,30,40,0.98) 100%)',
|
||||||
|
border: '1px solid rgba(255,215,0,0.2)',
|
||||||
|
borderRadius: '16px',
|
||||||
|
padding: '0',
|
||||||
|
maxWidth: '600px',
|
||||||
|
width: '90%',
|
||||||
|
overflow: 'hidden',
|
||||||
|
boxShadow: '0 25px 80px rgba(0,0,0,0.6), 0 0 1px rgba(255,215,0,0.3)'
|
||||||
|
}}>
|
||||||
|
{/* Modal Header */}
|
||||||
|
<div style={{
|
||||||
|
background: 'linear-gradient(135deg, rgba(255,215,0,0.15) 0%, rgba(255,215,0,0.05) 100%)',
|
||||||
|
borderBottom: '1px solid rgba(255,215,0,0.2)',
|
||||||
|
padding: '1.75rem 2rem',
|
||||||
|
}}>
|
||||||
|
<h3 style={{ margin: 0, color: '#ffd700', fontSize: '1.5rem', fontWeight: 700 }}>
|
||||||
|
🔗 Share Booking Link
|
||||||
|
</h3>
|
||||||
|
<p style={{ margin: '0.5rem 0 0 0', color: '#888', fontSize: '0.9rem' }}>
|
||||||
|
Share this link with customers to book appointments
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Modal Body */}
|
||||||
|
<div style={{ padding: '2rem' }}>
|
||||||
|
<div style={{
|
||||||
|
background: 'rgba(255,255,255,0.05)',
|
||||||
|
border: '1px solid rgba(255,215,0,0.3)',
|
||||||
|
borderRadius: '8px',
|
||||||
|
padding: '1rem',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '1rem',
|
||||||
|
marginBottom: '1.5rem'
|
||||||
|
}}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={bookingUrl}
|
||||||
|
readOnly
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
background: 'transparent',
|
||||||
|
border: 'none',
|
||||||
|
color: '#ffd700',
|
||||||
|
fontSize: '0.95rem',
|
||||||
|
outline: 'none'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className={styles.btnPrimary}
|
||||||
|
onClick={handleCopyLink}
|
||||||
|
style={{ padding: '0.5rem 1.5rem', whiteSpace: 'nowrap' }}
|
||||||
|
>
|
||||||
|
📋 Copy
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ marginBottom: '1.5rem' }}>
|
||||||
|
<p style={{ color: '#888', fontSize: '0.9rem', marginBottom: '1rem' }}>
|
||||||
|
Share via:
|
||||||
|
</p>
|
||||||
|
<div style={{ display: 'flex', gap: '1rem', flexWrap: 'wrap' }}>
|
||||||
|
<button
|
||||||
|
className={styles.btnSecondary}
|
||||||
|
onClick={() => window.open(`https://wa.me/?text=Book an appointment: ${encodeURIComponent(bookingUrl)}`, '_blank')}
|
||||||
|
style={{ padding: '0.6rem 1.2rem' }}
|
||||||
|
>
|
||||||
|
💬 WhatsApp
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={styles.btnSecondary}
|
||||||
|
onClick={() => window.open(`mailto:?subject=Book an Appointment&body=Book your appointment here: ${encodeURIComponent(bookingUrl)}`, '_blank')}
|
||||||
|
style={{ padding: '0.6rem 1.2rem' }}
|
||||||
|
>
|
||||||
|
✉️ Email
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={styles.btnSecondary}
|
||||||
|
onClick={() => window.open(`https://twitter.com/intent/tweet?text=Book an appointment: ${encodeURIComponent(bookingUrl)}`, '_blank')}
|
||||||
|
style={{ padding: '0.6rem 1.2rem' }}
|
||||||
|
>
|
||||||
|
🐦 Twitter
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{
|
||||||
|
background: 'rgba(255,215,0,0.1)',
|
||||||
|
border: '1px solid rgba(255,215,0,0.2)',
|
||||||
|
borderRadius: '8px',
|
||||||
|
padding: '1rem',
|
||||||
|
fontSize: '0.85rem',
|
||||||
|
color: '#888'
|
||||||
|
}}>
|
||||||
|
<strong style={{ color: '#ffd700' }}>💡 Tip:</strong> This is a public link that anyone can use to book appointments. No login required!
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Modal Footer */}
|
||||||
|
<div style={{
|
||||||
|
borderTop: '1px solid rgba(255,255,255,0.1)',
|
||||||
|
padding: '1.5rem 2rem',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
background: 'rgba(0,0,0,0.2)'
|
||||||
|
}}>
|
||||||
|
<button
|
||||||
|
className={styles.btnSecondary}
|
||||||
|
onClick={() => setShowShareModal(false)}
|
||||||
|
style={{ padding: '0.75rem 2rem' }}
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={styles.sectionHeader}>
|
||||||
|
<div>
|
||||||
|
<h1 className={styles.pageTitle}>Appointments Dashboard</h1>
|
||||||
|
<p className={styles.pageSubtitle}>Manage your schedule and bookings</p>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', gap: '1rem' }}>
|
||||||
|
<button className={styles.btnSecondary} onClick={handleShareLink}>
|
||||||
|
🔗 Share Link
|
||||||
|
</button>
|
||||||
|
<Link href="/dashboard/appointments/book">
|
||||||
|
<button className={styles.btnPrimary}>+ Book Appointment</button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Stats Widgets */}
|
||||||
|
<div className={styles.widgetsGrid}>
|
||||||
|
<div className={styles.widget}>
|
||||||
|
<span className={styles.widgetIcon}>📅</span>
|
||||||
|
<div className={styles.widgetValue}>{stats.todayAppointments}</div>
|
||||||
|
<div className={styles.widgetLabel}>Today's Appointments</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.widget}>
|
||||||
|
<span className={styles.widgetIcon}>🔜</span>
|
||||||
|
<div className={styles.widgetValue}>{stats.upcomingAppointments}</div>
|
||||||
|
<div className={styles.widgetLabel}>Upcoming</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.widget}>
|
||||||
|
<span className={styles.widgetIcon}>✅</span>
|
||||||
|
<div className={styles.widgetValue}>{stats.completedThisWeek}</div>
|
||||||
|
<div className={styles.widgetLabel}>Completed This Week</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.widget}>
|
||||||
|
<span className={styles.widgetIcon}>⏳</span>
|
||||||
|
<div className={styles.widgetValue}>{stats.pendingConfirmation}</div>
|
||||||
|
<div className={styles.widgetLabel}>Pending Confirmation</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Quick Actions */}
|
||||||
|
<div className={styles.section}>
|
||||||
|
<h3 style={{ color: '#ffd700', marginBottom: '1rem' }}>Quick Actions</h3>
|
||||||
|
<div style={{ display: 'flex', gap: '1rem', flexWrap: 'wrap' }}>
|
||||||
|
<Link href="/dashboard/appointments/book">
|
||||||
|
<button className={styles.btnPrimary}>Book New Appointment</button>
|
||||||
|
</Link>
|
||||||
|
<Link href="/dashboard/appointments/calendar">
|
||||||
|
<button className={styles.btnSecondary}>View Calendar</button>
|
||||||
|
</Link>
|
||||||
|
<Link href="/dashboard/appointments/my-appointments">
|
||||||
|
<button className={styles.btnSecondary}>My Appointments</button>
|
||||||
|
</Link>
|
||||||
|
<Link href="/dashboard/appointments/types">
|
||||||
|
<button className={styles.btnSecondary}>Manage Types</button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Two Column Layout */}
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(400px, 1fr))', gap: '1.5rem' }}>
|
||||||
|
{/* Today's Schedule */}
|
||||||
|
<div className={styles.section}>
|
||||||
|
<h3 style={{ color: '#ffd700', marginBottom: '1rem' }}>📋 Today's Schedule</h3>
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
|
||||||
|
{MOCK_TODAY_SCHEDULE.map(apt => (
|
||||||
|
<div key={apt.id} style={{
|
||||||
|
padding: '1rem',
|
||||||
|
background: 'rgba(255,255,255,0.03)',
|
||||||
|
borderRadius: '8px',
|
||||||
|
borderLeft: '3px solid #ffd700',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center'
|
||||||
|
}}>
|
||||||
|
<div>
|
||||||
|
<div style={{ fontWeight: 600, color: '#fff', marginBottom: '0.3rem' }}>
|
||||||
|
{apt.time} - {apt.customer}
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: '0.85rem', color: '#888' }}>
|
||||||
|
{apt.type} • {apt.id}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span className={`${styles.badge} ${apt.status === 'Confirmed' ? styles.statusActive : styles.statusPending}`}>
|
||||||
|
{apt.status}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Upcoming Appointments */}
|
||||||
|
<div className={styles.section}>
|
||||||
|
<h3 style={{ color: '#ffd700', marginBottom: '1rem' }}>🔜 Upcoming Appointments</h3>
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
|
||||||
|
{MOCK_UPCOMING.map(apt => (
|
||||||
|
<div key={apt.id} style={{
|
||||||
|
padding: '1rem',
|
||||||
|
background: 'rgba(255,255,255,0.03)',
|
||||||
|
borderRadius: '8px',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center'
|
||||||
|
}}>
|
||||||
|
<div>
|
||||||
|
<div style={{ fontWeight: 600, color: '#fff', marginBottom: '0.3rem' }}>
|
||||||
|
{apt.customer}
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: '0.85rem', color: '#888' }}>
|
||||||
|
{apt.type} • {apt.id}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ textAlign: 'right' }}>
|
||||||
|
<div style={{ color: '#ffd700', fontWeight: 600 }}>{apt.date}</div>
|
||||||
|
<div style={{ fontSize: '0.85rem', color: '#888' }}>{apt.time}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
338
src/app/dashboard/appointments/settings/page.tsx
Normal file
@@ -0,0 +1,338 @@
|
|||||||
|
'use client';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import styles from '../appointments.module.css';
|
||||||
|
import { useToast } from '@/context/ToastContext';
|
||||||
|
|
||||||
|
const DAYS_OF_WEEK = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
|
||||||
|
|
||||||
|
export default function AppointmentSettingsPage() {
|
||||||
|
const { showToast } = useToast();
|
||||||
|
const [settings, setSettings] = useState({
|
||||||
|
cancellationWindow: 24,
|
||||||
|
reminderTime: 2,
|
||||||
|
autoConfirm: true,
|
||||||
|
allowRescheduling: true,
|
||||||
|
timezone: 'Asia/Kolkata'
|
||||||
|
});
|
||||||
|
|
||||||
|
const [workingHours, setWorkingHours] = useState({
|
||||||
|
Monday: { enabled: true, start: '09:00', end: '17:00' },
|
||||||
|
Tuesday: { enabled: true, start: '09:00', end: '17:00' },
|
||||||
|
Wednesday: { enabled: true, start: '09:00', end: '17:00' },
|
||||||
|
Thursday: { enabled: true, start: '09:00', end: '17:00' },
|
||||||
|
Friday: { enabled: true, start: '09:00', end: '17:00' },
|
||||||
|
Saturday: { enabled: false, start: '09:00', end: '13:00' },
|
||||||
|
Sunday: { enabled: false, start: '09:00', end: '13:00' }
|
||||||
|
});
|
||||||
|
|
||||||
|
const [lunchTime, setLunchTime] = useState({
|
||||||
|
enabled: true,
|
||||||
|
start: '13:00',
|
||||||
|
end: '14:00'
|
||||||
|
});
|
||||||
|
|
||||||
|
const [unavailableDates, setUnavailableDates] = useState<string[]>([
|
||||||
|
'2024-12-25',
|
||||||
|
'2024-12-26',
|
||||||
|
'2025-01-01'
|
||||||
|
]);
|
||||||
|
|
||||||
|
const [newUnavailableDate, setNewUnavailableDate] = useState('');
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
showToast('✅ Settings saved successfully!', 'success');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleWorkingHoursChange = (day: string, field: string, value: string | boolean) => {
|
||||||
|
setWorkingHours({
|
||||||
|
...workingHours,
|
||||||
|
[day]: {
|
||||||
|
...workingHours[day as keyof typeof workingHours],
|
||||||
|
[field]: value
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddUnavailableDate = () => {
|
||||||
|
if (!newUnavailableDate) {
|
||||||
|
showToast('Please select a date', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (unavailableDates.includes(newUnavailableDate)) {
|
||||||
|
showToast('This date is already marked as unavailable', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setUnavailableDates([...unavailableDates, newUnavailableDate].sort());
|
||||||
|
setNewUnavailableDate('');
|
||||||
|
showToast('Date added to unavailable list', 'success');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveUnavailableDate = (date: string) => {
|
||||||
|
setUnavailableDates(unavailableDates.filter(d => d !== date));
|
||||||
|
showToast('Date removed from unavailable list', 'success');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.hamContainer}>
|
||||||
|
<div className={styles.sectionHeader}>
|
||||||
|
<div>
|
||||||
|
<h1 className={styles.pageTitle}>Appointment Settings</h1>
|
||||||
|
<p className={styles.pageSubtitle}>Configure booking rules and availability</p>
|
||||||
|
</div>
|
||||||
|
<button className={styles.btnPrimary} onClick={handleSave}>
|
||||||
|
Save Changes
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(400px, 1fr))', gap: '1.5rem' }}>
|
||||||
|
{/* Cancellation Policy */}
|
||||||
|
<div className={styles.section}>
|
||||||
|
<h3 style={{ color: '#ffd700', marginBottom: '1.5rem' }}>Cancellation Policy</h3>
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}>
|
||||||
|
<div>
|
||||||
|
<label className={styles.filterLabel}>Cancellation Window (hours)</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
className={styles.inputField}
|
||||||
|
value={settings.cancellationWindow}
|
||||||
|
onChange={(e) => setSettings({ ...settings, cancellationWindow: parseInt(e.target.value) })}
|
||||||
|
/>
|
||||||
|
<p style={{ fontSize: '0.85rem', color: '#888', marginTop: '0.5rem' }}>
|
||||||
|
Customers cannot cancel within this many hours before the appointment
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={settings.allowRescheduling}
|
||||||
|
onChange={(e) => setSettings({ ...settings, allowRescheduling: e.target.checked })}
|
||||||
|
style={{ width: '20px', height: '20px' }}
|
||||||
|
/>
|
||||||
|
<label style={{ color: '#fff' }}>Allow customers to reschedule appointments</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Reminder Settings */}
|
||||||
|
<div className={styles.section}>
|
||||||
|
<h3 style={{ color: '#ffd700', marginBottom: '1.5rem' }}>Reminder Settings</h3>
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}>
|
||||||
|
<div>
|
||||||
|
<label className={styles.filterLabel}>Send Reminder (hours before)</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
className={styles.inputField}
|
||||||
|
value={settings.reminderTime}
|
||||||
|
onChange={(e) => setSettings({ ...settings, reminderTime: parseInt(e.target.value) })}
|
||||||
|
/>
|
||||||
|
<p style={{ fontSize: '0.85rem', color: '#888', marginTop: '0.5rem' }}>
|
||||||
|
Email/SMS reminder will be sent this many hours before the appointment
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Booking Rules */}
|
||||||
|
<div className={styles.section}>
|
||||||
|
<h3 style={{ color: '#ffd700', marginBottom: '1.5rem' }}>Booking Rules</h3>
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={settings.autoConfirm}
|
||||||
|
onChange={(e) => setSettings({ ...settings, autoConfirm: e.target.checked })}
|
||||||
|
style={{ width: '20px', height: '20px' }}
|
||||||
|
/>
|
||||||
|
<label style={{ color: '#fff' }}>Auto-confirm appointments</label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={styles.filterLabel}>Default Timezone</label>
|
||||||
|
<select
|
||||||
|
className={styles.filterSelect}
|
||||||
|
value={settings.timezone}
|
||||||
|
onChange={(e) => setSettings({ ...settings, timezone: e.target.value })}
|
||||||
|
>
|
||||||
|
<option value="Asia/Kolkata">Asia/Kolkata (IST)</option>
|
||||||
|
<option value="America/New_York">America/New_York (EST)</option>
|
||||||
|
<option value="Europe/London">Europe/London (GMT)</option>
|
||||||
|
<option value="Asia/Tokyo">Asia/Tokyo (JST)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Working Hours */}
|
||||||
|
<div className={styles.section} style={{ marginTop: '1.5rem' }}>
|
||||||
|
<h3 style={{ color: '#ffd700', marginBottom: '1.5rem' }}>⏰ Working Hours</h3>
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
|
||||||
|
{DAYS_OF_WEEK.map(day => (
|
||||||
|
<div key={day} style={{
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: '150px 80px 1fr 1fr 100px',
|
||||||
|
gap: '1rem',
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: '1rem',
|
||||||
|
background: 'rgba(255,255,255,0.03)',
|
||||||
|
borderRadius: '8px',
|
||||||
|
border: '1px solid rgba(255,255,255,0.1)'
|
||||||
|
}}>
|
||||||
|
<div style={{ fontWeight: 600, color: '#fff' }}>{day}</div>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={workingHours[day as keyof typeof workingHours].enabled}
|
||||||
|
onChange={(e) => handleWorkingHoursChange(day, 'enabled', e.target.checked)}
|
||||||
|
style={{ width: '18px', height: '18px' }}
|
||||||
|
/>
|
||||||
|
<label style={{ color: '#888', fontSize: '0.85rem' }}>Open</label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label style={{ display: 'block', marginBottom: '0.3rem', color: '#888', fontSize: '0.8rem' }}>Start Time</label>
|
||||||
|
<input
|
||||||
|
type="time"
|
||||||
|
className={styles.inputField}
|
||||||
|
value={workingHours[day as keyof typeof workingHours].start}
|
||||||
|
onChange={(e) => handleWorkingHoursChange(day, 'start', e.target.value)}
|
||||||
|
disabled={!workingHours[day as keyof typeof workingHours].enabled}
|
||||||
|
style={{ padding: '0.5rem' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label style={{ display: 'block', marginBottom: '0.3rem', color: '#888', fontSize: '0.8rem' }}>End Time</label>
|
||||||
|
<input
|
||||||
|
type="time"
|
||||||
|
className={styles.inputField}
|
||||||
|
value={workingHours[day as keyof typeof workingHours].end}
|
||||||
|
onChange={(e) => handleWorkingHoursChange(day, 'end', e.target.value)}
|
||||||
|
disabled={!workingHours[day as keyof typeof workingHours].enabled}
|
||||||
|
style={{ padding: '0.5rem' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div style={{ color: '#888', fontSize: '0.85rem', textAlign: 'right' }}>
|
||||||
|
{workingHours[day as keyof typeof workingHours].enabled ? (
|
||||||
|
<>
|
||||||
|
{(() => {
|
||||||
|
const start = workingHours[day as keyof typeof workingHours].start.split(':');
|
||||||
|
const end = workingHours[day as keyof typeof workingHours].end.split(':');
|
||||||
|
const hours = parseInt(end[0]) - parseInt(start[0]);
|
||||||
|
const minutes = parseInt(end[1]) - parseInt(start[1]);
|
||||||
|
const totalHours = hours + minutes / 60;
|
||||||
|
return `${totalHours.toFixed(1)}h`;
|
||||||
|
})()}
|
||||||
|
</>
|
||||||
|
) : 'Closed'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Lunch Time */}
|
||||||
|
<div className={styles.section} style={{ marginTop: '1.5rem' }}>
|
||||||
|
<h3 style={{ color: '#ffd700', marginBottom: '1.5rem' }}>🍽️ Lunch Break</h3>
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={lunchTime.enabled}
|
||||||
|
onChange={(e) => setLunchTime({ ...lunchTime, enabled: e.target.checked })}
|
||||||
|
style={{ width: '20px', height: '20px' }}
|
||||||
|
/>
|
||||||
|
<label style={{ color: '#fff', fontWeight: 600 }}>Enable lunch break (appointments not available during this time)</label>
|
||||||
|
</div>
|
||||||
|
{lunchTime.enabled && (
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1.5rem', maxWidth: '500px' }}>
|
||||||
|
<div>
|
||||||
|
<label className={styles.filterLabel}>Lunch Start Time</label>
|
||||||
|
<input
|
||||||
|
type="time"
|
||||||
|
className={styles.inputField}
|
||||||
|
value={lunchTime.start}
|
||||||
|
onChange={(e) => setLunchTime({ ...lunchTime, start: e.target.value })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={styles.filterLabel}>Lunch End Time</label>
|
||||||
|
<input
|
||||||
|
type="time"
|
||||||
|
className={styles.inputField}
|
||||||
|
value={lunchTime.end}
|
||||||
|
onChange={(e) => setLunchTime({ ...lunchTime, end: e.target.value })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Unavailable Dates */}
|
||||||
|
<div className={styles.section} style={{ marginTop: '1.5rem' }}>
|
||||||
|
<h3 style={{ color: '#ffd700', marginBottom: '1.5rem' }}>🚫 Unavailable Dates</h3>
|
||||||
|
<p style={{ color: '#888', fontSize: '0.9rem', marginBottom: '1.5rem' }}>
|
||||||
|
Mark specific dates when appointments should not be available (holidays, vacations, etc.)
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Add New Date */}
|
||||||
|
<div style={{ display: 'flex', gap: '1rem', marginBottom: '1.5rem' }}>
|
||||||
|
<div style={{ flex: 1, maxWidth: '300px' }}>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
className={styles.inputField}
|
||||||
|
value={newUnavailableDate}
|
||||||
|
onChange={(e) => setNewUnavailableDate(e.target.value)}
|
||||||
|
min={new Date().toISOString().split('T')[0]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button className={styles.btnPrimary} onClick={handleAddUnavailableDate}>
|
||||||
|
+ Add Date
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* List of Unavailable Dates */}
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))', gap: '1rem' }}>
|
||||||
|
{unavailableDates.map(date => (
|
||||||
|
<div key={date} style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: '0.75rem 1rem',
|
||||||
|
background: 'rgba(255,68,68,0.1)',
|
||||||
|
border: '1px solid rgba(255,68,68,0.3)',
|
||||||
|
borderRadius: '8px'
|
||||||
|
}}>
|
||||||
|
<span style={{ color: '#fff', fontWeight: 600 }}>
|
||||||
|
{new Date(date + 'T00:00:00').toLocaleDateString('en-US', {
|
||||||
|
weekday: 'short',
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
year: 'numeric'
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={() => handleRemoveUnavailableDate(date)}
|
||||||
|
style={{
|
||||||
|
background: 'transparent',
|
||||||
|
border: 'none',
|
||||||
|
color: '#ff4444',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: '1.2rem',
|
||||||
|
padding: '0 0.5rem'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{unavailableDates.length === 0 && (
|
||||||
|
<div style={{ textAlign: 'center', padding: '2rem', color: '#666' }}>
|
||||||
|
<div style={{ fontSize: '2rem', marginBottom: '0.5rem' }}>📅</div>
|
||||||
|
<p>No unavailable dates set</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
398
src/app/dashboard/appointments/types/page.tsx
Normal file
@@ -0,0 +1,398 @@
|
|||||||
|
'use client';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import styles from '../appointments.module.css';
|
||||||
|
import { useToast } from '@/context/ToastContext';
|
||||||
|
|
||||||
|
const MOCK_TYPES = [
|
||||||
|
{ id: 1, title: 'Initial Consultation', duration: 30, description: 'First-time meeting', price: 0, active: true },
|
||||||
|
{ id: 2, title: 'Follow-up Meeting', duration: 20, description: 'Review progress', price: 0, active: true },
|
||||||
|
{ id: 3, title: 'Technical Support', duration: 45, description: 'Technical assistance', price: 2500, active: true },
|
||||||
|
{ id: 4, title: 'Strategy Session', duration: 60, description: 'Planning session', price: 5000, active: true }
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function AppointmentTypesPage() {
|
||||||
|
const { showToast } = useToast();
|
||||||
|
const [types, setTypes] = useState(MOCK_TYPES);
|
||||||
|
const [isAddingType, setIsAddingType] = useState(false);
|
||||||
|
const [isEditingType, setIsEditingType] = useState(false);
|
||||||
|
const [editingId, setEditingId] = useState<number | null>(null);
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
title: '',
|
||||||
|
duration: 30,
|
||||||
|
description: '',
|
||||||
|
price: 0,
|
||||||
|
active: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleAddType = () => {
|
||||||
|
setIsAddingType(true);
|
||||||
|
setIsEditingType(false);
|
||||||
|
setEditingId(null);
|
||||||
|
setFormData({
|
||||||
|
title: '',
|
||||||
|
duration: 30,
|
||||||
|
description: '',
|
||||||
|
price: 0,
|
||||||
|
active: true
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditType = (type: typeof MOCK_TYPES[0]) => {
|
||||||
|
setIsEditingType(true);
|
||||||
|
setIsAddingType(false);
|
||||||
|
setEditingId(type.id);
|
||||||
|
setFormData({
|
||||||
|
title: type.title,
|
||||||
|
duration: type.duration,
|
||||||
|
description: type.description,
|
||||||
|
price: type.price,
|
||||||
|
active: type.active
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
// Validation
|
||||||
|
if (!formData.title || !formData.duration) {
|
||||||
|
showToast('Please fill in all required fields', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (formData.duration < 5 || formData.duration > 480) {
|
||||||
|
showToast('Duration must be between 5 and 480 minutes', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEditingType && editingId) {
|
||||||
|
// Update existing type
|
||||||
|
setTypes(types.map(t => t.id === editingId ? {
|
||||||
|
...t,
|
||||||
|
title: formData.title,
|
||||||
|
duration: formData.duration,
|
||||||
|
description: formData.description,
|
||||||
|
price: formData.price,
|
||||||
|
active: formData.active
|
||||||
|
} : t));
|
||||||
|
showToast(`✅ Appointment type "${formData.title}" updated successfully!`, 'success');
|
||||||
|
} else {
|
||||||
|
// Create new type
|
||||||
|
const newType = {
|
||||||
|
id: types.length + 1,
|
||||||
|
title: formData.title,
|
||||||
|
duration: formData.duration,
|
||||||
|
description: formData.description,
|
||||||
|
price: formData.price,
|
||||||
|
active: formData.active
|
||||||
|
};
|
||||||
|
setTypes([...types, newType]);
|
||||||
|
showToast(`✅ Appointment type "${formData.title}" created successfully!`, 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsAddingType(false);
|
||||||
|
setIsEditingType(false);
|
||||||
|
setEditingId(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToggleStatus = (id: number) => {
|
||||||
|
setTypes(types.map(t => t.id === id ? { ...t, active: !t.active } : t));
|
||||||
|
const type = types.find(t => t.id === id);
|
||||||
|
showToast(`Appointment type ${type?.active ? 'deactivated' : 'activated'}`, 'success');
|
||||||
|
};
|
||||||
|
|
||||||
|
const isModalOpen = isAddingType || isEditingType;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.hamContainer}>
|
||||||
|
{/* Add/Edit Type Modal */}
|
||||||
|
{isModalOpen && (
|
||||||
|
<div style={{
|
||||||
|
position: 'fixed',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
background: 'rgba(0,0,0,0.8)',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
zIndex: 9999,
|
||||||
|
backdropFilter: 'blur(8px)'
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
|
background: 'linear-gradient(135deg, rgba(15,20,30,0.98) 0%, rgba(25,30,40,0.98) 100%)',
|
||||||
|
border: '1px solid rgba(255,215,0,0.2)',
|
||||||
|
borderRadius: '16px',
|
||||||
|
padding: '0',
|
||||||
|
maxWidth: '650px',
|
||||||
|
width: '90%',
|
||||||
|
maxHeight: '90vh',
|
||||||
|
overflow: 'hidden',
|
||||||
|
boxShadow: '0 25px 80px rgba(0,0,0,0.6), 0 0 1px rgba(255,215,0,0.3)'
|
||||||
|
}}>
|
||||||
|
{/* Modal Header */}
|
||||||
|
<div style={{
|
||||||
|
background: 'linear-gradient(135deg, rgba(255,215,0,0.15) 0%, rgba(255,215,0,0.05) 100%)',
|
||||||
|
borderBottom: '1px solid rgba(255,215,0,0.2)',
|
||||||
|
padding: '1.75rem 2rem',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center'
|
||||||
|
}}>
|
||||||
|
<div>
|
||||||
|
<h3 style={{ margin: 0, color: '#ffd700', fontSize: '1.5rem', fontWeight: 700 }}>
|
||||||
|
{isEditingType ? 'Edit Appointment Type' : 'Add Appointment Type'}
|
||||||
|
</h3>
|
||||||
|
<p style={{ margin: '0.5rem 0 0 0', color: '#888', fontSize: '0.9rem' }}>
|
||||||
|
{isEditingType ? 'Update appointment type details' : 'Configure a new appointment type for booking'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Modal Body */}
|
||||||
|
<div style={{ padding: '2rem', maxHeight: 'calc(90vh - 180px)', overflowY: 'auto' }}>
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '1.75rem' }}>
|
||||||
|
{/* Title Field */}
|
||||||
|
<div>
|
||||||
|
<label style={{
|
||||||
|
display: 'block',
|
||||||
|
marginBottom: '0.6rem',
|
||||||
|
color: '#fff',
|
||||||
|
fontSize: '0.9rem',
|
||||||
|
fontWeight: 600
|
||||||
|
}}>
|
||||||
|
Title <span style={{ color: '#ff4444' }}>*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className={styles.inputField}
|
||||||
|
value={formData.title}
|
||||||
|
onChange={(e) => setFormData({ ...formData, title: e.target.value })}
|
||||||
|
placeholder="e.g., Product Demo, Consultation"
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
fontSize: '0.95rem',
|
||||||
|
padding: '0.75rem 1rem'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Duration and Price Row */}
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1.5rem' }}>
|
||||||
|
<div>
|
||||||
|
<label style={{
|
||||||
|
display: 'block',
|
||||||
|
marginBottom: '0.6rem',
|
||||||
|
color: '#fff',
|
||||||
|
fontSize: '0.9rem',
|
||||||
|
fontWeight: 600
|
||||||
|
}}>
|
||||||
|
Duration (minutes) <span style={{ color: '#ff4444' }}>*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
className={styles.inputField}
|
||||||
|
value={formData.duration}
|
||||||
|
onChange={(e) => setFormData({ ...formData, duration: parseInt(e.target.value) || 0 })}
|
||||||
|
min="5"
|
||||||
|
max="480"
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
fontSize: '0.95rem',
|
||||||
|
padding: '0.75rem 1rem'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<p style={{ margin: '0.5rem 0 0 0', color: '#666', fontSize: '0.8rem' }}>
|
||||||
|
5-480 minutes
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label style={{
|
||||||
|
display: 'block',
|
||||||
|
marginBottom: '0.6rem',
|
||||||
|
color: '#fff',
|
||||||
|
fontSize: '0.9rem',
|
||||||
|
fontWeight: 600
|
||||||
|
}}>
|
||||||
|
Price (₹)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
className={styles.inputField}
|
||||||
|
value={formData.price}
|
||||||
|
onChange={(e) => setFormData({ ...formData, price: parseInt(e.target.value) || 0 })}
|
||||||
|
min="0"
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
fontSize: '0.95rem',
|
||||||
|
padding: '0.75rem 1rem'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<p style={{ margin: '0.5rem 0 0 0', color: '#666', fontSize: '0.8rem' }}>
|
||||||
|
Leave 0 for free
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Description Field */}
|
||||||
|
<div>
|
||||||
|
<label style={{
|
||||||
|
display: 'block',
|
||||||
|
marginBottom: '0.6rem',
|
||||||
|
color: '#fff',
|
||||||
|
fontSize: '0.9rem',
|
||||||
|
fontWeight: 600
|
||||||
|
}}>
|
||||||
|
Description
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
className={styles.inputField}
|
||||||
|
value={formData.description}
|
||||||
|
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
|
||||||
|
placeholder="Brief description of this appointment type..."
|
||||||
|
rows={3}
|
||||||
|
style={{
|
||||||
|
resize: 'vertical',
|
||||||
|
width: '100%',
|
||||||
|
fontSize: '0.95rem',
|
||||||
|
padding: '0.75rem 1rem',
|
||||||
|
lineHeight: '1.5'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Active Checkbox */}
|
||||||
|
<div style={{
|
||||||
|
background: 'rgba(255,255,255,0.03)',
|
||||||
|
border: '1px solid rgba(255,255,255,0.1)',
|
||||||
|
borderRadius: '8px',
|
||||||
|
padding: '1rem 1.25rem',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '1rem'
|
||||||
|
}}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="activeCheckbox"
|
||||||
|
checked={formData.active}
|
||||||
|
onChange={(e) => setFormData({ ...formData, active: e.target.checked })}
|
||||||
|
style={{
|
||||||
|
width: '20px',
|
||||||
|
height: '20px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
accentColor: '#ffd700'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<label htmlFor="activeCheckbox" style={{
|
||||||
|
color: '#fff',
|
||||||
|
fontSize: '0.95rem',
|
||||||
|
cursor: 'pointer',
|
||||||
|
userSelect: 'none'
|
||||||
|
}}>
|
||||||
|
Active (available for booking)
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Modal Footer */}
|
||||||
|
<div style={{
|
||||||
|
borderTop: '1px solid rgba(255,255,255,0.1)',
|
||||||
|
padding: '1.5rem 2rem',
|
||||||
|
display: 'flex',
|
||||||
|
gap: '1rem',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
background: 'rgba(0,0,0,0.2)'
|
||||||
|
}}>
|
||||||
|
<button
|
||||||
|
className={styles.btnSecondary}
|
||||||
|
onClick={() => {
|
||||||
|
setIsAddingType(false);
|
||||||
|
setIsEditingType(false);
|
||||||
|
setEditingId(null);
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
padding: '0.75rem 2rem',
|
||||||
|
fontSize: '0.95rem',
|
||||||
|
fontWeight: 600
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={styles.btnPrimary}
|
||||||
|
onClick={handleSubmit}
|
||||||
|
style={{
|
||||||
|
padding: '0.75rem 2rem',
|
||||||
|
fontSize: '0.95rem',
|
||||||
|
fontWeight: 600
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isEditingType ? 'Update Type' : 'Create Type'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className={styles.sectionHeader}>
|
||||||
|
<div>
|
||||||
|
<h1 className={styles.pageTitle}>Appointment Types</h1>
|
||||||
|
<p className={styles.pageSubtitle}>Manage available appointment types</p>
|
||||||
|
</div>
|
||||||
|
<button className={styles.btnPrimary} onClick={handleAddType}>
|
||||||
|
+ Add Type
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.section}>
|
||||||
|
<table className={styles.table}>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Title</th>
|
||||||
|
<th>Duration</th>
|
||||||
|
<th>Description</th>
|
||||||
|
<th>Price</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{types.map(type => (
|
||||||
|
<tr key={type.id}>
|
||||||
|
<td style={{ fontWeight: 600 }}>{type.title}</td>
|
||||||
|
<td>{type.duration} min</td>
|
||||||
|
<td style={{ color: '#888' }}>{type.description}</td>
|
||||||
|
<td style={{ color: '#ffd700', fontWeight: 600 }}>
|
||||||
|
{type.price === 0 ? 'Free' : `₹${type.price.toLocaleString('en-IN')}`}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span className={`${styles.badge} ${type.active ? styles.statusActive : styles.statusInactive}`}>
|
||||||
|
{type.active ? 'Active' : 'Inactive'}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div style={{ display: 'flex', gap: '0.5rem' }}>
|
||||||
|
<button
|
||||||
|
className={styles.btnSecondary}
|
||||||
|
style={{ padding: '0.4rem 0.8rem', fontSize: '0.8rem' }}
|
||||||
|
onClick={() => handleEditType(type)}
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={type.active ? styles.btnSecondary : styles.btnPrimary}
|
||||||
|
style={{ padding: '0.4rem 0.8rem', fontSize: '0.8rem' }}
|
||||||
|
onClick={() => handleToggleStatus(type.id)}
|
||||||
|
>
|
||||||
|
{type.active ? 'Deactivate' : 'Activate'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
152
src/app/dashboard/crm/analytics/page.tsx
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
'use client';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import styles from '../crm.module.css';
|
||||||
|
|
||||||
|
export default function AnalyticsPage() {
|
||||||
|
const [dateRange, setDateRange] = useState('30days');
|
||||||
|
|
||||||
|
const metrics = [
|
||||||
|
{ label: 'Total Leads', value: '247', change: '+12%', positive: true },
|
||||||
|
{ label: 'Conversion Rate', value: '18.5%', change: '+3.2%', positive: true },
|
||||||
|
{ label: 'Avg Deal Size', value: '₹4.2L', change: '+8%', positive: true },
|
||||||
|
{ label: 'Win Rate', value: '32%', change: '-2%', positive: false }
|
||||||
|
];
|
||||||
|
|
||||||
|
const leadSources = [
|
||||||
|
{ source: 'Paid Ads', count: 98, percentage: 40 },
|
||||||
|
{ source: 'Organic', count: 74, percentage: 30 },
|
||||||
|
{ source: 'Referral', count: 49, percentage: 20 },
|
||||||
|
{ source: 'Direct', count: 26, percentage: 10 }
|
||||||
|
];
|
||||||
|
|
||||||
|
const pipelineData = [
|
||||||
|
{ stage: 'New', count: 45, value: 112.5 },
|
||||||
|
{ stage: 'Qualified', count: 32, value: 160.0 },
|
||||||
|
{ stage: 'Proposal', count: 18, value: 144.0 },
|
||||||
|
{ stage: 'Negotiation', count: 12, value: 96.0 },
|
||||||
|
{ stage: 'Won', count: 8, value: 80.0 }
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.crmContainer}>
|
||||||
|
<div className={styles.sectionHeader}>
|
||||||
|
<div>
|
||||||
|
<h1 className={styles.pageTitle}>CRM Analytics</h1>
|
||||||
|
<p className={styles.pageSubtitle}>Insights and performance metrics</p>
|
||||||
|
</div>
|
||||||
|
<div className={styles.filterGroup}>
|
||||||
|
<select
|
||||||
|
className={styles.filterSelect}
|
||||||
|
value={dateRange}
|
||||||
|
onChange={(e) => setDateRange(e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="7days">Last 7 Days</option>
|
||||||
|
<option value="30days">Last 30 Days</option>
|
||||||
|
<option value="90days">Last 90 Days</option>
|
||||||
|
<option value="year">This Year</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Key Metrics */}
|
||||||
|
<div className={styles.statsGrid}>
|
||||||
|
{metrics.map((metric, index) => (
|
||||||
|
<div key={index} className={styles.statCard}>
|
||||||
|
<div className={styles.statLabel}>{metric.label}</div>
|
||||||
|
<div className={styles.statValue}>{metric.value}</div>
|
||||||
|
<div className={`${styles.statChange} ${metric.positive ? styles.positive : styles.negative}`}>
|
||||||
|
<span>{metric.positive ? '↑' : '↓'}</span>
|
||||||
|
<span>{metric.change} from last period</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Charts Grid */}
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '2rem' }}>
|
||||||
|
{/* Lead Sources */}
|
||||||
|
<div className={styles.section}>
|
||||||
|
<h2 className={styles.sectionTitle}>📊 Lead Sources</h2>
|
||||||
|
<div style={{ padding: '1rem 0' }}>
|
||||||
|
{leadSources.map((source, index) => (
|
||||||
|
<div key={index} style={{ marginBottom: '1.5rem' }}>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '0.5rem' }}>
|
||||||
|
<span style={{ color: '#fff', fontWeight: 500 }}>{source.source}</span>
|
||||||
|
<span style={{ color: '#888' }}>{source.count} leads</span>
|
||||||
|
</div>
|
||||||
|
<div style={{
|
||||||
|
width: '100%',
|
||||||
|
height: '8px',
|
||||||
|
background: 'rgba(255,255,255,0.05)',
|
||||||
|
borderRadius: '4px',
|
||||||
|
overflow: 'hidden'
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
|
width: `${source.percentage}%`,
|
||||||
|
height: '100%',
|
||||||
|
background: 'linear-gradient(90deg, #ffd700, #ffed4e)',
|
||||||
|
transition: 'width 0.5s ease'
|
||||||
|
}} />
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: '0.8rem', color: '#888', marginTop: '0.25rem' }}>
|
||||||
|
{source.percentage}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Pipeline Funnel */}
|
||||||
|
<div className={styles.section}>
|
||||||
|
<h2 className={styles.sectionTitle}>🎯 Pipeline Funnel</h2>
|
||||||
|
<div style={{ padding: '1rem 0' }}>
|
||||||
|
{pipelineData.map((stage, index) => (
|
||||||
|
<div key={index} style={{ marginBottom: '1.5rem' }}>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '0.5rem' }}>
|
||||||
|
<span style={{ color: '#fff', fontWeight: 500 }}>{stage.stage}</span>
|
||||||
|
<div style={{ display: 'flex', gap: '1rem' }}>
|
||||||
|
<span style={{ color: '#888' }}>{stage.count} deals</span>
|
||||||
|
<span style={{ color: '#ffd700', fontWeight: 600 }}>₹{stage.value}L</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style={{
|
||||||
|
width: '100%',
|
||||||
|
height: '10px',
|
||||||
|
background: 'rgba(255,255,255,0.05)',
|
||||||
|
borderRadius: '5px',
|
||||||
|
overflow: 'hidden'
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
|
width: `${(stage.count / pipelineData[0].count) * 100}%`,
|
||||||
|
height: '100%',
|
||||||
|
background: index === pipelineData.length - 1
|
||||||
|
? 'linear-gradient(90deg, #00ff9d, #00cc7d)'
|
||||||
|
: 'linear-gradient(90deg, #ffd700, #ffed4e)',
|
||||||
|
transition: 'width 0.5s ease'
|
||||||
|
}} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Recent Performance */}
|
||||||
|
<div className={styles.section}>
|
||||||
|
<h2 className={styles.sectionTitle}>📈 Performance Trends</h2>
|
||||||
|
<div style={{
|
||||||
|
padding: '3rem 2rem',
|
||||||
|
textAlign: 'center',
|
||||||
|
background: 'rgba(255,255,255,0.02)',
|
||||||
|
borderRadius: '8px',
|
||||||
|
border: '1px dashed rgba(255,255,255,0.1)'
|
||||||
|
}}>
|
||||||
|
<div style={{ fontSize: '3rem', marginBottom: '1rem', opacity: 0.5 }}>📊</div>
|
||||||
|
<div style={{ color: '#888', fontSize: '0.95rem' }}>
|
||||||
|
Interactive charts and detailed analytics coming soon
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
439
src/app/dashboard/crm/crm.module.css
Normal file
@@ -0,0 +1,439 @@
|
|||||||
|
/* CRM Module Styles - Luxury Tech Theme */
|
||||||
|
|
||||||
|
/* Page Container */
|
||||||
|
.crmContainer {
|
||||||
|
padding: 2rem;
|
||||||
|
animation: fadeIn 0.5s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Section Headers */
|
||||||
|
.sectionHeader {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pageTitle {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
background: linear-gradient(135deg, #fff 0%, #ffd700 100%);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pageSubtitle {
|
||||||
|
color: #888;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
margin: 0.5rem 0 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stats Cards - Standardized */
|
||||||
|
.statsGrid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
|
gap: 1.5rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statCard {
|
||||||
|
background: rgba(255, 255, 255, 0.03);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
-webkit-backdrop-filter: blur(10px);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statCard:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border-color: rgba(255, 215, 0, 0.3);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.statLabel {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #888;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statValue {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #fff;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statChange {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statChange.positive {
|
||||||
|
color: #00ff9d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statChange.negative {
|
||||||
|
color: #ff4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Buttons - Standardized */
|
||||||
|
.btnPrimary {
|
||||||
|
background: linear-gradient(135deg, #ffd700 0%, #b8860b 100%);
|
||||||
|
color: #000;
|
||||||
|
border: none;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btnPrimary:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 8px 20px rgba(255, 215, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btnSecondary {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
color: #fff;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btnSecondary:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border-color: rgba(255, 215, 0, 0.5);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form Inputs - Standardized */
|
||||||
|
.inputField {
|
||||||
|
width: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputField:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: rgba(255, 215, 0, 0.5);
|
||||||
|
box-shadow: 0 0 0 3px rgba(255, 215, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputField::placeholder {
|
||||||
|
color: rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Table Styles */
|
||||||
|
.table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
background: rgba(255, 255, 255, 0.02);
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table thead {
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table th {
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
text-align: left;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #ffd700;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table td {
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
color: #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table tbody tr {
|
||||||
|
transition: background 0.2s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table tbody tr:hover {
|
||||||
|
background: rgba(255, 215, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Kanban Board */
|
||||||
|
.kanbanBoard {
|
||||||
|
display: flex;
|
||||||
|
gap: 1.5rem;
|
||||||
|
overflow-x: auto;
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kanbanColumn {
|
||||||
|
min-width: 320px;
|
||||||
|
flex: 0 0 320px;
|
||||||
|
background: rgba(255, 255, 255, 0.02);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 1rem;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.kanbanColumnHeader {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
padding-bottom: 0.75rem;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.kanbanColumnTitle {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #fff;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kanbanColumnCount {
|
||||||
|
background: rgba(255, 215, 0, 0.2);
|
||||||
|
color: #ffd700;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kanbanCard {
|
||||||
|
background: linear-gradient(135deg, rgba(20, 25, 35, 0.9) 0%, rgba(30, 35, 45, 0.9) 100%);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
cursor: grab;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kanbanCard:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
border-color: rgba(255, 215, 0, 0.3);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.kanbanCard:active {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardTitle {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #fff;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardValue {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
color: #ffd700;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardMeta {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Badges */
|
||||||
|
.badge {
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badgePaid {
|
||||||
|
background: rgba(255, 215, 0, 0.2);
|
||||||
|
color: #ffd700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badgeOrganic {
|
||||||
|
background: rgba(0, 255, 157, 0.2);
|
||||||
|
color: #00ff9d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badgeReferral {
|
||||||
|
background: rgba(88, 166, 255, 0.2);
|
||||||
|
color: #58a6ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badgeNew {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badgeQualified {
|
||||||
|
background: rgba(255, 215, 0, 0.2);
|
||||||
|
color: #ffd700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badgeWon {
|
||||||
|
background: rgba(0, 255, 157, 0.2);
|
||||||
|
color: #00ff9d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badgeLost {
|
||||||
|
background: rgba(255, 68, 68, 0.2);
|
||||||
|
color: #ff4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Priority Stars */
|
||||||
|
.priorityStars {
|
||||||
|
color: #ffd700;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Filter Controls */
|
||||||
|
.filterBar {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filterGroup {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filterLabel {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #888;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filterSelect {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
color: #fff;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filterSelect:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
border-color: rgba(255, 215, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filterSelect:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #ffd700;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Input Fields */
|
||||||
|
.inputField {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
color: #fff;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
width: 100%;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputField:focus {
|
||||||
|
outline: none;
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
border-color: #ffd700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputField::placeholder {
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Section Container */
|
||||||
|
.section {
|
||||||
|
background: rgba(255, 255, 255, 0.02);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionTitle {
|
||||||
|
font-size: 1.3rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #fff;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Empty State */
|
||||||
|
.emptyState {
|
||||||
|
text-align: center;
|
||||||
|
padding: 4rem 2rem;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emptyStateIcon {
|
||||||
|
font-size: 4rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emptyStateTitle {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
color: #fff;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emptyStateText {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animations */
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
349
src/app/dashboard/crm/leads/page.tsx
Normal file
@@ -0,0 +1,349 @@
|
|||||||
|
'use client';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import styles from '../crm.module.css';
|
||||||
|
import shared from '../shared/components.module.css';
|
||||||
|
import { useToast } from '@/context/ToastContext';
|
||||||
|
|
||||||
|
// Mock data
|
||||||
|
const MOCK_LEADS = [
|
||||||
|
{ id: 'L-001', name: 'Rajesh Kumar', email: 'rajesh@example.com', phone: '+91 98765 43210', source: 'Paid', score: 85, owner: 'Vikram', status: 'New', createdAt: '2 hours ago' },
|
||||||
|
{ id: 'L-002', name: 'Priya Sharma', email: 'priya@example.com', phone: '+91 98765 43211', source: 'Organic', score: 72, owner: 'Anjali', status: 'Contacted', createdAt: '5 hours ago' },
|
||||||
|
{ id: 'L-003', name: 'Amit Patel', email: 'amit@example.com', phone: '+91 98765 43212', source: 'Referral', score: 90, owner: 'Rohan', status: 'Qualified', createdAt: '1 day ago' },
|
||||||
|
{ id: 'L-004', name: 'Sneha Reddy', email: 'sneha@example.com', phone: '+91 98765 43213', source: 'Paid', score: 65, owner: 'Vikram', status: 'New', createdAt: '2 days ago' },
|
||||||
|
{ id: 'L-005', name: 'Karthik Iyer', email: 'karthik@example.com', phone: '+91 98765 43214', source: 'Organic', score: 78, owner: 'Anjali', status: 'Contacted', createdAt: '3 days ago' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const MOCK_OWNERS = ['Vikram', 'Anjali', 'Rohan', 'Priya'];
|
||||||
|
|
||||||
|
export default function LeadsPage() {
|
||||||
|
const { showToast } = useToast();
|
||||||
|
const [leads, setLeads] = useState(MOCK_LEADS);
|
||||||
|
const [filterSource, setFilterSource] = useState('all');
|
||||||
|
const [filterStatus, setFilterStatus] = useState('all');
|
||||||
|
const [isCreatingLead, setIsCreatingLead] = useState(false);
|
||||||
|
|
||||||
|
// Form state
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
name: '',
|
||||||
|
email: '',
|
||||||
|
phone: '',
|
||||||
|
source: 'Organic',
|
||||||
|
owner: 'Vikram',
|
||||||
|
status: 'New'
|
||||||
|
});
|
||||||
|
|
||||||
|
const filteredLeads = leads.filter(lead => {
|
||||||
|
if (filterSource !== 'all' && lead.source !== filterSource) return false;
|
||||||
|
if (filterStatus !== 'all' && lead.status !== filterStatus) return false;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleCreateLead = () => {
|
||||||
|
setIsCreatingLead(true);
|
||||||
|
setFormData({
|
||||||
|
name: '',
|
||||||
|
email: '',
|
||||||
|
phone: '',
|
||||||
|
source: 'Organic',
|
||||||
|
owner: 'Vikram',
|
||||||
|
status: 'New'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmitLead = () => {
|
||||||
|
// Validation
|
||||||
|
if (!formData.name || !formData.email || !formData.phone) {
|
||||||
|
showToast('Please fill in all required fields', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Email validation
|
||||||
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||||
|
if (!emailRegex.test(formData.email)) {
|
||||||
|
showToast('Please enter a valid email address', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate new lead
|
||||||
|
const newLead = {
|
||||||
|
id: `L-${String(leads.length + 1).padStart(3, '0')}`,
|
||||||
|
name: formData.name,
|
||||||
|
email: formData.email,
|
||||||
|
phone: formData.phone,
|
||||||
|
source: formData.source,
|
||||||
|
score: Math.floor(Math.random() * 40) + 60, // Random score 60-100
|
||||||
|
owner: formData.owner,
|
||||||
|
status: formData.status,
|
||||||
|
createdAt: 'Just now'
|
||||||
|
};
|
||||||
|
|
||||||
|
setLeads([newLead, ...leads]);
|
||||||
|
setIsCreatingLead(false);
|
||||||
|
showToast(`✅ Lead created: ${formData.name}`, 'success');
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSourceBadgeClass = (source: string) => {
|
||||||
|
switch (source) {
|
||||||
|
case 'Paid': return styles.badgePaid;
|
||||||
|
case 'Organic': return styles.badgeOrganic;
|
||||||
|
case 'Referral': return styles.badgeReferral;
|
||||||
|
default: return styles.badgeNew;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.crmContainer}>
|
||||||
|
{/* Create Lead Modal */}
|
||||||
|
{isCreatingLead && (
|
||||||
|
<div style={{
|
||||||
|
position: 'fixed',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
background: 'rgba(0,0,0,0.7)',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
zIndex: 9999,
|
||||||
|
backdropFilter: 'blur(4px)'
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
|
background: 'linear-gradient(135deg, rgba(20,25,35,0.95) 0%, rgba(30,35,45,0.95) 100%)',
|
||||||
|
border: '1px solid rgba(255,215,0,0.3)',
|
||||||
|
borderRadius: '12px',
|
||||||
|
padding: '2rem',
|
||||||
|
maxWidth: '500px',
|
||||||
|
width: '90%',
|
||||||
|
boxShadow: '0 20px 60px rgba(0,0,0,0.5)',
|
||||||
|
animation: 'fadeIn 0.3s ease-out'
|
||||||
|
}}>
|
||||||
|
<h3 style={{ margin: '0 0 1.5rem 0', color: '#ffd700', fontSize: '1.5rem' }}>
|
||||||
|
Create New Lead
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
|
||||||
|
<div>
|
||||||
|
<label style={{ display: 'block', marginBottom: '0.5rem', color: '#888', fontSize: '0.85rem' }}>
|
||||||
|
Name *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className={styles.inputField}
|
||||||
|
value={formData.name}
|
||||||
|
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||||
|
placeholder="Enter lead name"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label style={{ display: 'block', marginBottom: '0.5rem', color: '#888', fontSize: '0.85rem' }}>
|
||||||
|
Email *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
className={styles.inputField}
|
||||||
|
value={formData.email}
|
||||||
|
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
||||||
|
placeholder="email@example.com"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label style={{ display: 'block', marginBottom: '0.5rem', color: '#888', fontSize: '0.85rem' }}>
|
||||||
|
Phone *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="tel"
|
||||||
|
className={styles.inputField}
|
||||||
|
value={formData.phone}
|
||||||
|
onChange={(e) => setFormData({ ...formData, phone: e.target.value })}
|
||||||
|
placeholder="+91 98765 43210"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem' }}>
|
||||||
|
<div>
|
||||||
|
<label style={{ display: 'block', marginBottom: '0.5rem', color: '#888', fontSize: '0.85rem' }}>
|
||||||
|
Source
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
className={styles.filterSelect}
|
||||||
|
value={formData.source}
|
||||||
|
onChange={(e) => setFormData({ ...formData, source: e.target.value })}
|
||||||
|
>
|
||||||
|
<option value="Paid">Paid</option>
|
||||||
|
<option value="Organic">Organic</option>
|
||||||
|
<option value="Referral">Referral</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label style={{ display: 'block', marginBottom: '0.5rem', color: '#888', fontSize: '0.85rem' }}>
|
||||||
|
Owner
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
className={styles.filterSelect}
|
||||||
|
value={formData.owner}
|
||||||
|
onChange={(e) => setFormData({ ...formData, owner: e.target.value })}
|
||||||
|
>
|
||||||
|
{MOCK_OWNERS.map(owner => (
|
||||||
|
<option key={owner} value={owner}>{owner}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', gap: '1rem', justifyContent: 'flex-end', marginTop: '2rem' }}>
|
||||||
|
<button
|
||||||
|
className={styles.btnSecondary}
|
||||||
|
onClick={() => setIsCreatingLead(false)}
|
||||||
|
style={{ padding: '0.6rem 1.5rem' }}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={styles.btnPrimary}
|
||||||
|
onClick={handleSubmitLead}
|
||||||
|
style={{ padding: '0.6rem 1.5rem' }}
|
||||||
|
>
|
||||||
|
Create Lead
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className={styles.sectionHeader}>
|
||||||
|
<div>
|
||||||
|
<h1 className={styles.pageTitle}>Leads</h1>
|
||||||
|
<p className={styles.pageSubtitle}>Manage and track all your leads</p>
|
||||||
|
</div>
|
||||||
|
<button className={styles.btnPrimary} onClick={handleCreateLead}>
|
||||||
|
+ New Lead
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Filter Bar */}
|
||||||
|
<div className={styles.filterBar}>
|
||||||
|
<div className={styles.filterGroup}>
|
||||||
|
<label className={styles.filterLabel}>Source</label>
|
||||||
|
<select
|
||||||
|
className={styles.filterSelect}
|
||||||
|
value={filterSource}
|
||||||
|
onChange={(e) => setFilterSource(e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="all">All Sources</option>
|
||||||
|
<option value="Paid">Paid</option>
|
||||||
|
<option value="Organic">Organic</option>
|
||||||
|
<option value="Referral">Referral</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.filterGroup}>
|
||||||
|
<label className={styles.filterLabel}>Status</label>
|
||||||
|
<select
|
||||||
|
className={styles.filterSelect}
|
||||||
|
value={filterStatus}
|
||||||
|
onChange={(e) => setFilterStatus(e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="all">All Status</option>
|
||||||
|
<option value="New">New</option>
|
||||||
|
<option value="Contacted">Contacted</option>
|
||||||
|
<option value="Qualified">Qualified</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.filterGroup}>
|
||||||
|
<label className={styles.filterLabel}>Owner</label>
|
||||||
|
<select className={styles.filterSelect}>
|
||||||
|
<option value="all">All Owners</option>
|
||||||
|
{MOCK_OWNERS.map(owner => (
|
||||||
|
<option key={owner} value={owner.toLowerCase()}>{owner}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Leads Table */}
|
||||||
|
<div className={styles.section}>
|
||||||
|
<table className={styles.table}>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Contact</th>
|
||||||
|
<th>Source</th>
|
||||||
|
<th>Score</th>
|
||||||
|
<th>Owner</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Created</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{filteredLeads.map(lead => (
|
||||||
|
<tr key={lead.id}>
|
||||||
|
<td style={{ color: '#888', fontSize: '0.9rem' }}>{lead.id}</td>
|
||||||
|
<td style={{ fontWeight: 600 }}>{lead.name}</td>
|
||||||
|
<td>
|
||||||
|
<div style={{ fontSize: '0.85rem' }}>
|
||||||
|
<div>{lead.email}</div>
|
||||||
|
<div style={{ color: '#888' }}>{lead.phone}</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span className={`${styles.badge} ${getSourceBadgeClass(lead.source)}`}>
|
||||||
|
{lead.source}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
|
||||||
|
<div style={{
|
||||||
|
width: '60px',
|
||||||
|
height: '6px',
|
||||||
|
background: 'rgba(255,255,255,0.1)',
|
||||||
|
borderRadius: '3px',
|
||||||
|
overflow: 'hidden'
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
|
width: `${lead.score}%`,
|
||||||
|
height: '100%',
|
||||||
|
background: lead.score >= 80 ? '#00ff9d' : lead.score >= 60 ? '#ffd700' : '#ff4444'
|
||||||
|
}} />
|
||||||
|
</div>
|
||||||
|
<span style={{ fontSize: '0.85rem', color: '#888' }}>{lead.score}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>{lead.owner}</td>
|
||||||
|
<td>
|
||||||
|
<span className={styles.badge} style={{
|
||||||
|
background: lead.status === 'Qualified' ? 'rgba(0,255,157,0.2)' :
|
||||||
|
lead.status === 'Contacted' ? 'rgba(255,215,0,0.2)' :
|
||||||
|
'rgba(255,255,255,0.1)',
|
||||||
|
color: lead.status === 'Qualified' ? '#00ff9d' :
|
||||||
|
lead.status === 'Contacted' ? '#ffd700' : '#ccc'
|
||||||
|
}}>
|
||||||
|
{lead.status}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td style={{ color: '#888', fontSize: '0.85rem' }}>{lead.createdAt}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
{filteredLeads.length === 0 && (
|
||||||
|
<div className={styles.emptyState}>
|
||||||
|
<div className={styles.emptyStateIcon}>🔍</div>
|
||||||
|
<div className={styles.emptyStateTitle}>No leads found</div>
|
||||||
|
<div className={styles.emptyStateText}>Try adjusting your filters or create a new lead</div>
|
||||||
|
<button className={styles.btnPrimary} onClick={handleCreateLead}>
|
||||||
|
+ Create Lead
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
427
src/app/dashboard/crm/opportunities/page.tsx
Normal file
@@ -0,0 +1,427 @@
|
|||||||
|
'use client';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import styles from '../crm.module.css';
|
||||||
|
import shared from '../../shared/components.module.css';
|
||||||
|
import { useToast } from '@/context/ToastContext';
|
||||||
|
|
||||||
|
// Mock data
|
||||||
|
const MOCK_OPPORTUNITIES = [
|
||||||
|
{ id: 'OPP-001', name: 'Website Redesign - Acme Corp', value: 250000, priority: 3, stage: 'new', contact: 'John Doe', nextActivity: 'Demo call', dueDate: 'Tomorrow' },
|
||||||
|
{ id: 'OPP-002', name: 'CRM Implementation - TechFlow', value: 450000, priority: 5, stage: 'qualified', contact: 'Jane Smith', nextActivity: 'Proposal review', dueDate: '2 days' },
|
||||||
|
{ id: 'OPP-003', name: 'Mobile App Development', value: 800000, priority: 4, stage: 'proposal', contact: 'Mike Johnson', nextActivity: 'Contract signing', dueDate: 'Today' },
|
||||||
|
{ id: 'OPP-004', name: 'Cloud Migration Project', value: 600000, priority: 5, stage: 'negotiation', contact: 'Sarah Williams', nextActivity: 'Final pricing', dueDate: 'Today' },
|
||||||
|
{ id: 'OPP-005', name: 'E-commerce Platform', value: 350000, priority: 2, stage: 'won', contact: 'David Brown', nextActivity: 'Kickoff meeting', dueDate: 'Next week' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const STAGES = [
|
||||||
|
{ id: 'new', title: 'New', color: '#ccc' },
|
||||||
|
{ id: 'qualified', title: 'Qualified', color: '#ffd700' },
|
||||||
|
{ id: 'proposal', title: 'Proposal', color: '#58a6ff' },
|
||||||
|
{ id: 'negotiation', title: 'Negotiation', color: '#ff9500' },
|
||||||
|
{ id: 'won', title: 'Won', color: '#00ff9d' },
|
||||||
|
{ id: 'lost', title: 'Lost', color: '#ff4444' }
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function OpportunitiesPage() {
|
||||||
|
const { showToast } = useToast();
|
||||||
|
const [opportunities, setOpportunities] = useState(MOCK_OPPORTUNITIES);
|
||||||
|
const [draggedOpp, setDraggedOpp] = useState<string | null>(null);
|
||||||
|
const [isCreatingOpp, setIsCreatingOpp] = useState(false);
|
||||||
|
const [collapsedColumns, setCollapsedColumns] = useState<Set<string>>(new Set());
|
||||||
|
|
||||||
|
// Form state
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
name: '',
|
||||||
|
value: '',
|
||||||
|
contact: '',
|
||||||
|
priority: 3,
|
||||||
|
nextActivity: '',
|
||||||
|
dueDate: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const toggleColumn = (stageId: string) => {
|
||||||
|
setCollapsedColumns(prev => {
|
||||||
|
const newSet = new Set(prev);
|
||||||
|
if (newSet.has(stageId)) {
|
||||||
|
newSet.delete(stageId);
|
||||||
|
} else {
|
||||||
|
newSet.add(stageId);
|
||||||
|
}
|
||||||
|
return newSet;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDragStart = (e: React.DragEvent, oppId: string) => {
|
||||||
|
setDraggedOpp(oppId);
|
||||||
|
e.dataTransfer.effectAllowed = 'move';
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDragOver = (e: React.DragEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDrop = (e: React.DragEvent, stage: string) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!draggedOpp) return;
|
||||||
|
|
||||||
|
setOpportunities(prev => prev.map(opp =>
|
||||||
|
opp.id === draggedOpp ? { ...opp, stage } : opp
|
||||||
|
));
|
||||||
|
|
||||||
|
const opp = opportunities.find(o => o.id === draggedOpp);
|
||||||
|
if (stage === 'won') {
|
||||||
|
showToast(`🎉 Deal won: ${opp?.name}!`, 'success');
|
||||||
|
} else if (stage === 'lost') {
|
||||||
|
showToast(`Deal moved to Lost: ${opp?.name}`, 'warning');
|
||||||
|
}
|
||||||
|
|
||||||
|
setDraggedOpp(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreateOpportunity = () => {
|
||||||
|
setIsCreatingOpp(true);
|
||||||
|
setFormData({
|
||||||
|
name: '',
|
||||||
|
value: '',
|
||||||
|
contact: '',
|
||||||
|
priority: 3,
|
||||||
|
nextActivity: '',
|
||||||
|
dueDate: ''
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmitOpportunity = () => {
|
||||||
|
// Validation
|
||||||
|
if (!formData.name || !formData.value || !formData.contact) {
|
||||||
|
showToast('Please fill in all required fields', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const valueNum = parseFloat(formData.value);
|
||||||
|
if (isNaN(valueNum) || valueNum <= 0) {
|
||||||
|
showToast('Please enter a valid deal value', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate new opportunity
|
||||||
|
const newOpp = {
|
||||||
|
id: `OPP-${String(opportunities.length + 1).padStart(3, '0')}`,
|
||||||
|
name: formData.name,
|
||||||
|
value: valueNum,
|
||||||
|
priority: formData.priority,
|
||||||
|
stage: 'new',
|
||||||
|
contact: formData.contact,
|
||||||
|
nextActivity: formData.nextActivity || 'Initial contact',
|
||||||
|
dueDate: formData.dueDate || 'TBD'
|
||||||
|
};
|
||||||
|
|
||||||
|
setOpportunities([newOpp, ...opportunities]);
|
||||||
|
setIsCreatingOpp(false);
|
||||||
|
showToast(`✅ Opportunity created: ${formData.name}`, 'success');
|
||||||
|
};
|
||||||
|
|
||||||
|
const getOpportunitiesByStage = (stage: string) => {
|
||||||
|
return opportunities.filter(opp => opp.stage === stage);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTotalValueByStage = (stage: string) => {
|
||||||
|
return getOpportunitiesByStage(stage)
|
||||||
|
.reduce((sum, opp) => sum + opp.value, 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatCurrency = (value: number) => {
|
||||||
|
return `₹${(value / 100000).toFixed(1)}L`;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.crmContainer}>
|
||||||
|
{/* Create Opportunity Modal */}
|
||||||
|
{isCreatingOpp && (
|
||||||
|
<div style={{
|
||||||
|
position: 'fixed',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
background: 'rgba(0,0,0,0.7)',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
zIndex: 9999,
|
||||||
|
backdropFilter: 'blur(4px)'
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
|
background: 'linear-gradient(135deg, rgba(20,25,35,0.95) 0%, rgba(30,35,45,0.95) 100%)',
|
||||||
|
border: '1px solid rgba(255,215,0,0.3)',
|
||||||
|
borderRadius: '12px',
|
||||||
|
padding: '2rem',
|
||||||
|
maxWidth: '500px',
|
||||||
|
width: '90%',
|
||||||
|
boxShadow: '0 20px 60px rgba(0,0,0,0.5)',
|
||||||
|
animation: 'fadeIn 0.3s ease-out'
|
||||||
|
}}>
|
||||||
|
<h3 style={{ margin: '0 0 1.5rem 0', color: '#ffd700', fontSize: '1.5rem' }}>
|
||||||
|
Create New Opportunity
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
|
||||||
|
<div>
|
||||||
|
<label style={{ display: 'block', marginBottom: '0.5rem', color: '#888', fontSize: '0.85rem' }}>
|
||||||
|
Deal Name *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className={styles.inputField}
|
||||||
|
value={formData.name}
|
||||||
|
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||||
|
placeholder="e.g., Website Redesign - Acme Corp"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem' }}>
|
||||||
|
<div>
|
||||||
|
<label style={{ display: 'block', marginBottom: '0.5rem', color: '#888', fontSize: '0.85rem' }}>
|
||||||
|
Deal Value (₹) *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
className={styles.inputField}
|
||||||
|
value={formData.value}
|
||||||
|
onChange={(e) => setFormData({ ...formData, value: e.target.value })}
|
||||||
|
placeholder="250000"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label style={{ display: 'block', marginBottom: '0.5rem', color: '#888', fontSize: '0.85rem' }}>
|
||||||
|
Priority
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
className={styles.filterSelect}
|
||||||
|
value={formData.priority}
|
||||||
|
onChange={(e) => setFormData({ ...formData, priority: parseInt(e.target.value) })}
|
||||||
|
>
|
||||||
|
<option value="1">★ Low</option>
|
||||||
|
<option value="2">★★ Medium-Low</option>
|
||||||
|
<option value="3">★★★ Medium</option>
|
||||||
|
<option value="4">★★★★ High</option>
|
||||||
|
<option value="5">★★★★★ Critical</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label style={{ display: 'block', marginBottom: '0.5rem', color: '#888', fontSize: '0.85rem' }}>
|
||||||
|
Contact Person *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className={styles.inputField}
|
||||||
|
value={formData.contact}
|
||||||
|
onChange={(e) => setFormData({ ...formData, contact: e.target.value })}
|
||||||
|
placeholder="John Doe"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label style={{ display: 'block', marginBottom: '0.5rem', color: '#888', fontSize: '0.85rem' }}>
|
||||||
|
Next Activity
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className={styles.inputField}
|
||||||
|
value={formData.nextActivity}
|
||||||
|
onChange={(e) => setFormData({ ...formData, nextActivity: e.target.value })}
|
||||||
|
placeholder="Demo call, Proposal review, etc."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label style={{ display: 'block', marginBottom: '0.5rem', color: '#888', fontSize: '0.85rem' }}>
|
||||||
|
Due Date
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className={styles.inputField}
|
||||||
|
value={formData.dueDate}
|
||||||
|
onChange={(e) => setFormData({ ...formData, dueDate: e.target.value })}
|
||||||
|
placeholder="Tomorrow, 2 days, Next week, etc."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', gap: '1rem', justifyContent: 'flex-end', marginTop: '2rem' }}>
|
||||||
|
<button
|
||||||
|
className={styles.btnSecondary}
|
||||||
|
onClick={() => setIsCreatingOpp(false)}
|
||||||
|
style={{ padding: '0.6rem 1.5rem' }}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={styles.btnPrimary}
|
||||||
|
onClick={handleSubmitOpportunity}
|
||||||
|
style={{ padding: '0.6rem 1.5rem' }}
|
||||||
|
>
|
||||||
|
Create Opportunity
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className={styles.sectionHeader}>
|
||||||
|
<div>
|
||||||
|
<h1 className={styles.pageTitle}>Opportunities Pipeline</h1>
|
||||||
|
<p className={styles.pageSubtitle}>Track and manage your sales opportunities</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className={styles.btnPrimary}
|
||||||
|
onClick={handleCreateOpportunity}
|
||||||
|
>
|
||||||
|
+ New Opportunity
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Kanban Board */}
|
||||||
|
<div style={{ display: 'flex', gap: '1rem', flexWrap: 'wrap' }}>
|
||||||
|
{STAGES.map(stage => {
|
||||||
|
const isCollapsed = collapsedColumns.has(stage.id);
|
||||||
|
const stageOpps = getOpportunitiesByStage(stage.id);
|
||||||
|
const stageValue = getTotalValueByStage(stage.id);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={stage.id}
|
||||||
|
style={{
|
||||||
|
minWidth: isCollapsed ? '80px' : '320px',
|
||||||
|
flex: isCollapsed ? '0 0 80px' : '0 0 320px',
|
||||||
|
background: 'rgba(255,255,255,0.02)',
|
||||||
|
borderRadius: '12px',
|
||||||
|
padding: '1rem',
|
||||||
|
border: '1px solid rgba(255,255,255,0.05)',
|
||||||
|
transition: 'all 0.3s ease'
|
||||||
|
}}
|
||||||
|
onDragOver={handleDragOver}
|
||||||
|
onDrop={(e) => handleDrop(e, stage.id)}
|
||||||
|
>
|
||||||
|
{/* Column Header */}
|
||||||
|
<div style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: isCollapsed ? 0 : '1rem',
|
||||||
|
paddingBottom: isCollapsed ? 0 : '0.75rem',
|
||||||
|
borderBottom: isCollapsed ? 'none' : '1px solid rgba(255,255,255,0.1)',
|
||||||
|
cursor: 'pointer'
|
||||||
|
}}
|
||||||
|
onClick={() => toggleColumn(stage.id)}
|
||||||
|
>
|
||||||
|
{isCollapsed ? (
|
||||||
|
<div style={{
|
||||||
|
writingMode: 'vertical-rl',
|
||||||
|
textOrientation: 'mixed',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '0.5rem',
|
||||||
|
width: '100%',
|
||||||
|
justifyContent: 'center'
|
||||||
|
}}>
|
||||||
|
<span style={{
|
||||||
|
fontWeight: 600,
|
||||||
|
color: stage.color,
|
||||||
|
fontSize: '0.9rem'
|
||||||
|
}}>
|
||||||
|
{stage.title}
|
||||||
|
</span>
|
||||||
|
<span style={{
|
||||||
|
background: 'rgba(255,215,0,0.2)',
|
||||||
|
color: '#ffd700',
|
||||||
|
padding: '4px 6px',
|
||||||
|
borderRadius: '12px',
|
||||||
|
fontSize: '0.75rem',
|
||||||
|
fontWeight: 600
|
||||||
|
}}>
|
||||||
|
{stageOpps.length}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
|
||||||
|
<span style={{ fontWeight: 600, color: stage.color }}>
|
||||||
|
{stage.title}
|
||||||
|
</span>
|
||||||
|
<span style={{
|
||||||
|
background: 'rgba(255,215,0,0.2)',
|
||||||
|
color: '#ffd700',
|
||||||
|
padding: '2px 8px',
|
||||||
|
borderRadius: '12px',
|
||||||
|
fontSize: '0.8rem',
|
||||||
|
fontWeight: 600
|
||||||
|
}}>
|
||||||
|
{stageOpps.length}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: '0.85rem', color: '#888' }}>
|
||||||
|
{formatCurrency(stageValue)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Cards - Only show when expanded */}
|
||||||
|
{!isCollapsed && (
|
||||||
|
<>
|
||||||
|
{stageOpps.map(opp => (
|
||||||
|
<div
|
||||||
|
key={opp.id}
|
||||||
|
className={styles.kanbanCard}
|
||||||
|
draggable
|
||||||
|
onDragStart={(e) => handleDragStart(e, opp.id)}
|
||||||
|
>
|
||||||
|
<div className={styles.cardTitle}>{opp.name}</div>
|
||||||
|
<div className={styles.cardValue}>{formatCurrency(opp.value)}</div>
|
||||||
|
<div className={styles.cardMeta}>
|
||||||
|
<div>
|
||||||
|
<div style={{ fontSize: '0.75rem', color: '#666', marginBottom: '2px' }}>
|
||||||
|
Next: {opp.nextActivity}
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: '0.75rem', color: '#888' }}>
|
||||||
|
Due: {opp.dueDate}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.priorityStars}>
|
||||||
|
{'★'.repeat(opp.priority)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style={{
|
||||||
|
marginTop: '0.75rem',
|
||||||
|
paddingTop: '0.75rem',
|
||||||
|
borderTop: '1px solid rgba(255,255,255,0.05)',
|
||||||
|
fontSize: '0.8rem',
|
||||||
|
color: '#888'
|
||||||
|
}}>
|
||||||
|
👤 {opp.contact}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{stageOpps.length === 0 && (
|
||||||
|
<div style={{
|
||||||
|
padding: '2rem 1rem',
|
||||||
|
textAlign: 'center',
|
||||||
|
color: '#666',
|
||||||
|
fontSize: '0.85rem'
|
||||||
|
}}>
|
||||||
|
No opportunities
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
162
src/app/dashboard/crm/page.tsx
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
'use client';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import styles from './crm.module.css';
|
||||||
|
import { useToast } from '@/context/ToastContext';
|
||||||
|
|
||||||
|
export default function CRMDashboard() {
|
||||||
|
const { showToast } = useToast();
|
||||||
|
|
||||||
|
const stats = [
|
||||||
|
{ label: 'Total Leads', value: '247', change: '+12%', positive: true, icon: '👥' },
|
||||||
|
{ label: 'Open Opportunities', value: '34', change: '+5%', positive: true, icon: '🎯' },
|
||||||
|
{ label: 'Quotes Pending', value: '12', change: '-3%', positive: false, icon: '📄' },
|
||||||
|
{ label: 'Revenue (MTD)', value: '₹8.4L', change: '+18%', positive: true, icon: '💰' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const recentActivities = [
|
||||||
|
{ type: 'lead', title: 'New lead from website', time: '5 mins ago', icon: '🆕' },
|
||||||
|
{ type: 'opportunity', title: 'Deal moved to Proposal stage', time: '1 hour ago', icon: '📈' },
|
||||||
|
{ type: 'quote', title: 'Quote sent to Acme Corp', time: '2 hours ago', icon: '📧' },
|
||||||
|
{ type: 'won', title: 'Deal won - TechFlow Solutions', time: '3 hours ago', icon: '🎉' }
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.crmContainer}>
|
||||||
|
<div className={styles.sectionHeader}>
|
||||||
|
<div>
|
||||||
|
<h1 className={styles.pageTitle}>CRM Dashboard</h1>
|
||||||
|
<p className={styles.pageSubtitle}>Manage leads, opportunities, and customer relationships</p>
|
||||||
|
</div>
|
||||||
|
<Link href="/dashboard/crm/leads">
|
||||||
|
<button className={styles.btnPrimary}>View Leads</button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Stats Grid */}
|
||||||
|
<div className={styles.statsGrid}>
|
||||||
|
{stats.map((stat, index) => (
|
||||||
|
<div key={index} className={styles.statCard}>
|
||||||
|
<div className={styles.statLabel}>
|
||||||
|
<span style={{ marginRight: '0.5rem' }}>{stat.icon}</span>
|
||||||
|
{stat.label}
|
||||||
|
</div>
|
||||||
|
<div className={styles.statValue}>{stat.value}</div>
|
||||||
|
<div className={`${styles.statChange} ${stat.positive ? styles.positive : styles.negative}`}>
|
||||||
|
<span>{stat.positive ? '↑' : '↓'}</span>
|
||||||
|
<span>{stat.change} from last month</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Quick Actions & Recent Activity */}
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: '2fr 1fr', gap: '2rem' }}>
|
||||||
|
{/* Quick Actions */}
|
||||||
|
<div className={styles.section}>
|
||||||
|
<h2 className={styles.sectionTitle}>⚡ Quick Actions</h2>
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: '1rem' }}>
|
||||||
|
<Link href="/dashboard/crm/leads">
|
||||||
|
<div style={{
|
||||||
|
background: 'rgba(255,215,0,0.1)',
|
||||||
|
border: '1px solid rgba(255,215,0,0.2)',
|
||||||
|
borderRadius: '8px',
|
||||||
|
padding: '1.5rem',
|
||||||
|
cursor: 'pointer',
|
||||||
|
transition: 'all 0.3s ease'
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => e.currentTarget.style.transform = 'translateY(-4px)'}
|
||||||
|
onMouseLeave={(e) => e.currentTarget.style.transform = 'translateY(0)'}
|
||||||
|
>
|
||||||
|
<div style={{ fontSize: '2rem', marginBottom: '0.5rem' }}>👥</div>
|
||||||
|
<div style={{ fontWeight: 600, color: '#fff', marginBottom: '0.3rem' }}>Manage Leads</div>
|
||||||
|
<div style={{ fontSize: '0.85rem', color: '#888' }}>View and manage all leads</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Link href="/dashboard/crm/opportunities">
|
||||||
|
<div style={{
|
||||||
|
background: 'rgba(0,255,157,0.1)',
|
||||||
|
border: '1px solid rgba(0,255,157,0.2)',
|
||||||
|
borderRadius: '8px',
|
||||||
|
padding: '1.5rem',
|
||||||
|
cursor: 'pointer',
|
||||||
|
transition: 'all 0.3s ease'
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => e.currentTarget.style.transform = 'translateY(-4px)'}
|
||||||
|
onMouseLeave={(e) => e.currentTarget.style.transform = 'translateY(0)'}
|
||||||
|
>
|
||||||
|
<div style={{ fontSize: '2rem', marginBottom: '0.5rem' }}>🎯</div>
|
||||||
|
<div style={{ fontWeight: 600, color: '#fff', marginBottom: '0.3rem' }}>Pipeline</div>
|
||||||
|
<div style={{ fontSize: '0.85rem', color: '#888' }}>Track opportunities</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Link href="/dashboard/crm/quotes">
|
||||||
|
<div style={{
|
||||||
|
background: 'rgba(88,166,255,0.1)',
|
||||||
|
border: '1px solid rgba(88,166,255,0.2)',
|
||||||
|
borderRadius: '8px',
|
||||||
|
padding: '1.5rem',
|
||||||
|
cursor: 'pointer',
|
||||||
|
transition: 'all 0.3s ease'
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => e.currentTarget.style.transform = 'translateY(-4px)'}
|
||||||
|
onMouseLeave={(e) => e.currentTarget.style.transform = 'translateY(0)'}
|
||||||
|
>
|
||||||
|
<div style={{ fontSize: '2rem', marginBottom: '0.5rem' }}>📄</div>
|
||||||
|
<div style={{ fontWeight: 600, color: '#fff', marginBottom: '0.3rem' }}>Quotes</div>
|
||||||
|
<div style={{ fontSize: '0.85rem', color: '#888' }}>Create and send quotes</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Link href="/dashboard/crm/analytics">
|
||||||
|
<div style={{
|
||||||
|
background: 'rgba(255,68,68,0.1)',
|
||||||
|
border: '1px solid rgba(255,68,68,0.2)',
|
||||||
|
borderRadius: '8px',
|
||||||
|
padding: '1.5rem',
|
||||||
|
cursor: 'pointer',
|
||||||
|
transition: 'all 0.3s ease'
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => e.currentTarget.style.transform = 'translateY(-4px)'}
|
||||||
|
onMouseLeave={(e) => e.currentTarget.style.transform = 'translateY(0)'}
|
||||||
|
>
|
||||||
|
<div style={{ fontSize: '2rem', marginBottom: '0.5rem' }}>📊</div>
|
||||||
|
<div style={{ fontWeight: 600, color: '#fff', marginBottom: '0.3rem' }}>Analytics</div>
|
||||||
|
<div style={{ fontSize: '0.85rem', color: '#888' }}>View reports and insights</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Recent Activity */}
|
||||||
|
<div className={styles.section}>
|
||||||
|
<h2 className={styles.sectionTitle}>🔔 Recent Activity</h2>
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
|
||||||
|
{recentActivities.map((activity, index) => (
|
||||||
|
<div key={index} style={{
|
||||||
|
padding: '1rem',
|
||||||
|
background: 'rgba(255,255,255,0.03)',
|
||||||
|
borderRadius: '8px',
|
||||||
|
borderLeft: '3px solid #ffd700'
|
||||||
|
}}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'start', gap: '0.75rem' }}>
|
||||||
|
<span style={{ fontSize: '1.5rem' }}>{activity.icon}</span>
|
||||||
|
<div style={{ flex: 1 }}>
|
||||||
|
<div style={{ color: '#fff', fontWeight: 500, marginBottom: '0.25rem' }}>
|
||||||
|
{activity.title}
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: '0.8rem', color: '#888' }}>
|
||||||
|
{activity.time}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
645
src/app/dashboard/crm/quotes/page.tsx
Normal file
@@ -0,0 +1,645 @@
|
|||||||
|
'use client';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import styles from '../crm.module.css';
|
||||||
|
import shared from '../../shared/components.module.css';
|
||||||
|
import jsPDF from 'jspdf';
|
||||||
|
import { useToast } from '@/context/ToastContext';
|
||||||
|
|
||||||
|
// Mock data
|
||||||
|
const MOCK_QUOTES = [
|
||||||
|
{ id: 'Q-001', customer: 'Acme Corp', title: 'Website Redesign Package', value: 250000, status: 'Draft', createdAt: '2024-01-15', validUntil: '2024-02-15' },
|
||||||
|
{ id: 'Q-002', customer: 'TechFlow Solutions', title: 'CRM Implementation', value: 450000, status: 'Sent', createdAt: '2024-01-14', validUntil: '2024-02-14' },
|
||||||
|
{ id: 'Q-003', customer: 'Global Enterprises', title: 'Mobile App Development', value: 800000, status: 'Approved', createdAt: '2024-01-12', validUntil: '2024-02-12' },
|
||||||
|
{ id: 'Q-004', customer: 'StartupXYZ', title: 'Cloud Migration Services', value: 600000, status: 'Sent', createdAt: '2024-01-10', validUntil: '2024-02-10' },
|
||||||
|
{ id: 'Q-005', customer: 'RetailCo', title: 'E-commerce Platform', value: 350000, status: 'Draft', createdAt: '2024-01-08', validUntil: '2024-02-08' }
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function QuotesPage() {
|
||||||
|
const { showToast } = useToast();
|
||||||
|
const [quotes, setQuotes] = useState(MOCK_QUOTES);
|
||||||
|
const [filterStatus, setFilterStatus] = useState('all');
|
||||||
|
const [previewQuote, setPreviewQuote] = useState<typeof MOCK_QUOTES[0] | null>(null);
|
||||||
|
const [isCreatingQuote, setIsCreatingQuote] = useState(false);
|
||||||
|
|
||||||
|
// Form state
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
customer: '',
|
||||||
|
title: '',
|
||||||
|
value: '',
|
||||||
|
validUntil: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const filteredQuotes = quotes.filter(quote => {
|
||||||
|
if (filterStatus !== 'all' && quote.status !== filterStatus) return false;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleCreateQuote = () => {
|
||||||
|
setIsCreatingQuote(true);
|
||||||
|
setFormData({
|
||||||
|
customer: '',
|
||||||
|
title: '',
|
||||||
|
value: '',
|
||||||
|
validUntil: ''
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmitQuote = () => {
|
||||||
|
// Validation
|
||||||
|
if (!formData.customer || !formData.title || !formData.value || !formData.validUntil) {
|
||||||
|
showToast('Please fill in all required fields', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const valueNum = parseFloat(formData.value);
|
||||||
|
if (isNaN(valueNum) || valueNum <= 0) {
|
||||||
|
showToast('Please enter a valid quote value', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate new quote
|
||||||
|
const newQuote = {
|
||||||
|
id: `Q-${String(quotes.length + 1).padStart(3, '0')}`,
|
||||||
|
customer: formData.customer,
|
||||||
|
title: formData.title,
|
||||||
|
value: valueNum,
|
||||||
|
status: 'Draft',
|
||||||
|
createdAt: new Date().toISOString().split('T')[0],
|
||||||
|
validUntil: formData.validUntil
|
||||||
|
};
|
||||||
|
|
||||||
|
setQuotes([newQuote, ...quotes]);
|
||||||
|
setIsCreatingQuote(false);
|
||||||
|
showToast(`✅ Quote ${newQuote.id} created successfully!`, 'success');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePreview = (quoteId: string) => {
|
||||||
|
const quote = quotes.find(q => q.id === quoteId);
|
||||||
|
if (quote) {
|
||||||
|
setPreviewQuote(quote);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDownload = (quoteId: string) => {
|
||||||
|
const quote = quotes.find(q => q.id === quoteId);
|
||||||
|
if (!quote) return;
|
||||||
|
|
||||||
|
// Dynamically import jsPDF
|
||||||
|
import('jspdf').then(({ default: jsPDF }) => {
|
||||||
|
const doc = new jsPDF();
|
||||||
|
|
||||||
|
// Colors
|
||||||
|
const goldColor: [number, number, number] = [255, 215, 0];
|
||||||
|
const darkGray: [number, number, number] = [51, 51, 51];
|
||||||
|
const lightGray: [number, number, number] = [136, 136, 136];
|
||||||
|
|
||||||
|
// Header - Company Name
|
||||||
|
doc.setFontSize(20);
|
||||||
|
doc.setTextColor(...goldColor);
|
||||||
|
doc.setFont('helvetica', 'bold');
|
||||||
|
doc.text('PENNYZO', 105, 20, { align: 'center' });
|
||||||
|
|
||||||
|
// Company Details
|
||||||
|
doc.setFontSize(8);
|
||||||
|
doc.setTextColor(...lightGray);
|
||||||
|
doc.setFont('helvetica', 'normal');
|
||||||
|
doc.text('Business Management Suite', 105, 27, { align: 'center' });
|
||||||
|
doc.text('Tel: +91 1234567890 | info@pennyzo.com', 105, 32, { align: 'center' });
|
||||||
|
doc.text('GSTIN: 27AALCC2781L1Z2', 105, 37, { align: 'center' });
|
||||||
|
|
||||||
|
// Title
|
||||||
|
doc.setFontSize(16);
|
||||||
|
doc.setTextColor(...darkGray);
|
||||||
|
doc.setFont('helvetica', 'bold');
|
||||||
|
doc.text('QUOTATION', 105, 50, { align: 'center' });
|
||||||
|
|
||||||
|
// Quote Details - Right Side
|
||||||
|
doc.setFontSize(9);
|
||||||
|
doc.setFont('helvetica', 'normal');
|
||||||
|
doc.text(`Quotation No.: ${quote.id}`, 150, 65);
|
||||||
|
doc.text(`Date: ${quote.createdAt}`, 150, 70);
|
||||||
|
doc.text(`Valid till: ${quote.validUntil}`, 150, 75);
|
||||||
|
|
||||||
|
// CIN and GSTIN - Left Side
|
||||||
|
doc.setFontSize(8);
|
||||||
|
doc.text(`CIN: U62099PN2024PTC213379`, 15, 65);
|
||||||
|
doc.text(`GSTIN: 27AALCC2781L1Z2`, 15, 70);
|
||||||
|
|
||||||
|
// Billing and Shipping Address Section
|
||||||
|
doc.setFontSize(9);
|
||||||
|
doc.setFont('helvetica', 'bold');
|
||||||
|
|
||||||
|
// Table header background
|
||||||
|
doc.setFillColor(240, 240, 240);
|
||||||
|
doc.rect(15, 85, 180, 8, 'F');
|
||||||
|
|
||||||
|
doc.text('Billing Address', 20, 90);
|
||||||
|
doc.text('Shipping Address', 110, 90);
|
||||||
|
|
||||||
|
// Address content
|
||||||
|
doc.setFont('helvetica', 'normal');
|
||||||
|
doc.setFontSize(8);
|
||||||
|
|
||||||
|
const billingY = 98;
|
||||||
|
doc.text(`${quote.customer}`, 20, billingY);
|
||||||
|
doc.text('KAPIL MALHAR, BANGLOW NO.2,, SR. NO.184,188', 20, billingY + 5);
|
||||||
|
doc.text('Pune, Maharashtra - 411045', 20, billingY + 10);
|
||||||
|
doc.text('GSTIN: 27AACFP9167Q1ZQ', 20, billingY + 15);
|
||||||
|
doc.text(`Email: info@${quote.customer.toLowerCase().replace(/\s+/g, '')}.com`, 20, billingY + 20);
|
||||||
|
|
||||||
|
doc.text(`${quote.customer}`, 110, billingY);
|
||||||
|
doc.text('KAPIL MALHAR, BANGLOW NO.2,, SR. NO.184,188', 110, billingY + 5);
|
||||||
|
doc.text('Pune, Maharashtra - 411045', 110, billingY + 10);
|
||||||
|
doc.text('GSTIN: 27AACFP9167Q1ZQ', 110, billingY + 15);
|
||||||
|
doc.text(`Email: info@${quote.customer.toLowerCase().replace(/\s+/g, '')}.com`, 110, billingY + 20);
|
||||||
|
|
||||||
|
// Items Table
|
||||||
|
const tableStartY = 130;
|
||||||
|
|
||||||
|
// Table headers
|
||||||
|
doc.setFillColor(240, 240, 240);
|
||||||
|
doc.rect(15, tableStartY, 180, 8, 'F');
|
||||||
|
|
||||||
|
doc.setFont('helvetica', 'bold');
|
||||||
|
doc.setFontSize(8);
|
||||||
|
doc.text('No.', 17, tableStartY + 5);
|
||||||
|
doc.text('Item & Description', 25, tableStartY + 5);
|
||||||
|
doc.text('HSN/SAC', 95, tableStartY + 5);
|
||||||
|
doc.text('Qty', 115, tableStartY + 5);
|
||||||
|
doc.text('Rate (₹)', 130, tableStartY + 5);
|
||||||
|
doc.text('Amount (₹)', 165, tableStartY + 5);
|
||||||
|
|
||||||
|
// Table row
|
||||||
|
doc.setFont('helvetica', 'normal');
|
||||||
|
const rowY = tableStartY + 12;
|
||||||
|
doc.text('1', 17, rowY);
|
||||||
|
doc.text(quote.title, 25, rowY);
|
||||||
|
doc.text('998313', 95, rowY);
|
||||||
|
doc.text('1', 115, rowY);
|
||||||
|
doc.text(quote.value.toLocaleString('en-IN'), 130, rowY);
|
||||||
|
doc.text(quote.value.toLocaleString('en-IN'), 165, rowY);
|
||||||
|
|
||||||
|
// Totals section
|
||||||
|
const totalsY = rowY + 20;
|
||||||
|
doc.setFont('helvetica', 'bold');
|
||||||
|
doc.text('Total Amount Before Tax (₹):', 120, totalsY);
|
||||||
|
doc.text(quote.value.toLocaleString('en-IN'), 175, totalsY, { align: 'right' });
|
||||||
|
|
||||||
|
const cgst = quote.value * 0.09;
|
||||||
|
const sgst = quote.value * 0.09;
|
||||||
|
|
||||||
|
doc.setFont('helvetica', 'normal');
|
||||||
|
doc.text('Add CGST (₹):', 120, totalsY + 5);
|
||||||
|
doc.text(cgst.toLocaleString('en-IN'), 175, totalsY + 5, { align: 'right' });
|
||||||
|
|
||||||
|
doc.text('Add SGST (₹):', 120, totalsY + 10);
|
||||||
|
doc.text(sgst.toLocaleString('en-IN'), 175, totalsY + 10, { align: 'right' });
|
||||||
|
|
||||||
|
// Grand Total
|
||||||
|
const grandTotal = quote.value + cgst + sgst;
|
||||||
|
doc.setFont('helvetica', 'bold');
|
||||||
|
doc.setFontSize(10);
|
||||||
|
doc.text('Grand Total (₹):', 120, totalsY + 18);
|
||||||
|
doc.text(grandTotal.toLocaleString('en-IN'), 175, totalsY + 18, { align: 'right' });
|
||||||
|
|
||||||
|
// Amount in words
|
||||||
|
doc.setFontSize(8);
|
||||||
|
doc.setFont('helvetica', 'bold');
|
||||||
|
doc.text('Total Quotation Amount in Words:', 15, totalsY + 30);
|
||||||
|
doc.setFont('helvetica', 'normal');
|
||||||
|
doc.text('Rupees ' + convertToWords(grandTotal) + ' only', 15, totalsY + 35);
|
||||||
|
|
||||||
|
// Bank Details
|
||||||
|
doc.setFont('helvetica', 'bold');
|
||||||
|
doc.text('Bank Details:', 15, totalsY + 45);
|
||||||
|
doc.setFont('helvetica', 'normal');
|
||||||
|
doc.setFontSize(7);
|
||||||
|
doc.text('Bank Name: HDFC BANK', 15, totalsY + 50);
|
||||||
|
doc.text('Branch: AUNDH', 15, totalsY + 54);
|
||||||
|
doc.text('Account No.: 50200086348713', 15, totalsY + 58);
|
||||||
|
doc.text('IFSC: HDFC0000052', 15, totalsY + 62);
|
||||||
|
|
||||||
|
// Terms and Conditions
|
||||||
|
doc.setFont('helvetica', 'bold');
|
||||||
|
doc.setFontSize(8);
|
||||||
|
doc.text('Terms and Conditions:', 15, totalsY + 72);
|
||||||
|
doc.setFont('helvetica', 'normal');
|
||||||
|
doc.setFontSize(7);
|
||||||
|
const terms = [
|
||||||
|
'1. Payment terms: 50% advance, 50% on completion',
|
||||||
|
'2. Quotation validity: As mentioned above',
|
||||||
|
'3. Delivery timeline will be confirmed upon order confirmation',
|
||||||
|
'4. Any changes in scope may result in revised quotation',
|
||||||
|
'5. All disputes subject to Pune jurisdiction',
|
||||||
|
'6. GST as applicable will be charged extra'
|
||||||
|
];
|
||||||
|
let termsY = totalsY + 77;
|
||||||
|
terms.forEach(term => {
|
||||||
|
doc.text(term, 15, termsY);
|
||||||
|
termsY += 4;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Footer
|
||||||
|
doc.setFontSize(7);
|
||||||
|
doc.setTextColor(...lightGray);
|
||||||
|
doc.text('This is a computer-generated quotation. E. & O. E.', 15, 280);
|
||||||
|
doc.text('Authorized Signatory', 165, 280);
|
||||||
|
|
||||||
|
// Save PDF
|
||||||
|
doc.save(`Quote_${quote.id}_${quote.customer.replace(/\s+/g, '_')}.pdf`);
|
||||||
|
showToast(`✅ Quote ${quote.id} downloaded successfully!`, 'success');
|
||||||
|
}).catch(error => {
|
||||||
|
console.error('Error generating PDF:', error);
|
||||||
|
showToast('Error generating PDF. Please try again.', 'error');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper function to convert number to words (simplified)
|
||||||
|
const convertToWords = (num: number): string => {
|
||||||
|
const ones = ['', 'One', 'Two', 'Three', 'Four', 'Five', 'Six', 'Seven', 'Eight', 'Nine'];
|
||||||
|
const tens = ['', '', 'Twenty', 'Thirty', 'Forty', 'Fifty', 'Sixty', 'Seventy', 'Eighty', 'Ninety'];
|
||||||
|
const teens = ['Ten', 'Eleven', 'Twelve', 'Thirteen', 'Fourteen', 'Fifteen', 'Sixteen', 'Seventeen', 'Eighteen', 'Nineteen'];
|
||||||
|
|
||||||
|
if (num === 0) return 'Zero';
|
||||||
|
|
||||||
|
const numStr = Math.floor(num).toString();
|
||||||
|
const lakhs = Math.floor(num / 100000);
|
||||||
|
const thousands = Math.floor((num % 100000) / 1000);
|
||||||
|
const hundreds = Math.floor((num % 1000) / 100);
|
||||||
|
const remainder = num % 100;
|
||||||
|
|
||||||
|
let words = '';
|
||||||
|
|
||||||
|
if (lakhs > 0) {
|
||||||
|
words += convertToWords(lakhs) + ' Lakh ';
|
||||||
|
}
|
||||||
|
if (thousands > 0) {
|
||||||
|
words += convertToWords(thousands) + ' Thousand ';
|
||||||
|
}
|
||||||
|
if (hundreds > 0) {
|
||||||
|
words += ones[hundreds] + ' Hundred ';
|
||||||
|
}
|
||||||
|
if (remainder >= 20) {
|
||||||
|
words += tens[Math.floor(remainder / 10)] + ' ';
|
||||||
|
if (remainder % 10 > 0) {
|
||||||
|
words += ones[remainder % 10];
|
||||||
|
}
|
||||||
|
} else if (remainder >= 10) {
|
||||||
|
words += teens[remainder - 10];
|
||||||
|
} else if (remainder > 0) {
|
||||||
|
words += ones[remainder];
|
||||||
|
}
|
||||||
|
|
||||||
|
return words.trim();
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusBadgeStyle = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'Draft':
|
||||||
|
return { background: 'rgba(255,255,255,0.1)', color: '#ccc' };
|
||||||
|
case 'Sent':
|
||||||
|
return { background: 'rgba(255,215,0,0.2)', color: '#ffd700' };
|
||||||
|
case 'Approved':
|
||||||
|
return { background: 'rgba(0,255,157,0.2)', color: '#00ff9d' };
|
||||||
|
case 'Rejected':
|
||||||
|
return { background: 'rgba(255,68,68,0.2)', color: '#ff4444' };
|
||||||
|
default:
|
||||||
|
return { background: 'rgba(255,255,255,0.1)', color: '#ccc' };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatCurrency = (value: number) => {
|
||||||
|
return `₹${(value / 100000).toFixed(1)}L`;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.crmContainer}>
|
||||||
|
{/* Create Quote Modal */}
|
||||||
|
{isCreatingQuote && (
|
||||||
|
<div style={{
|
||||||
|
position: 'fixed',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
background: 'rgba(0,0,0,0.7)',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
zIndex: 9999,
|
||||||
|
backdropFilter: 'blur(4px)'
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
|
background: 'linear-gradient(135deg, rgba(20,25,35,0.95) 0%, rgba(30,35,45,0.95) 100%)',
|
||||||
|
border: '1px solid rgba(255,215,0,0.3)',
|
||||||
|
borderRadius: '12px',
|
||||||
|
padding: '2rem',
|
||||||
|
maxWidth: '500px',
|
||||||
|
width: '90%',
|
||||||
|
boxShadow: '0 20px 60px rgba(0,0,0,0.5)',
|
||||||
|
animation: 'fadeIn 0.3s ease-out'
|
||||||
|
}}>
|
||||||
|
<h3 style={{ margin: '0 0 1.5rem 0', color: '#ffd700', fontSize: '1.5rem' }}>
|
||||||
|
Create New Quote
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
|
||||||
|
<div>
|
||||||
|
<label style={{ display: 'block', marginBottom: '0.5rem', color: '#888', fontSize: '0.85rem' }}>
|
||||||
|
Customer Name *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className={styles.inputField}
|
||||||
|
value={formData.customer}
|
||||||
|
onChange={(e) => setFormData({ ...formData, customer: e.target.value })}
|
||||||
|
placeholder="e.g., Acme Corp"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label style={{ display: 'block', marginBottom: '0.5rem', color: '#888', fontSize: '0.85rem' }}>
|
||||||
|
Quote Title *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className={styles.inputField}
|
||||||
|
value={formData.title}
|
||||||
|
onChange={(e) => setFormData({ ...formData, title: e.target.value })}
|
||||||
|
placeholder="e.g., Website Redesign Package"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label style={{ display: 'block', marginBottom: '0.5rem', color: '#888', fontSize: '0.85rem' }}>
|
||||||
|
Quote Value (₹) *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
className={styles.inputField}
|
||||||
|
value={formData.value}
|
||||||
|
onChange={(e) => setFormData({ ...formData, value: e.target.value })}
|
||||||
|
placeholder="250000"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label style={{ display: 'block', marginBottom: '0.5rem', color: '#888', fontSize: '0.85rem' }}>
|
||||||
|
Valid Until *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
className={styles.inputField}
|
||||||
|
value={formData.validUntil}
|
||||||
|
onChange={(e) => setFormData({ ...formData, validUntil: e.target.value })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', gap: '1rem', justifyContent: 'flex-end', marginTop: '2rem' }}>
|
||||||
|
<button
|
||||||
|
className={styles.btnSecondary}
|
||||||
|
onClick={() => setIsCreatingQuote(false)}
|
||||||
|
style={{ padding: '0.6rem 1.5rem' }}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={styles.btnPrimary}
|
||||||
|
onClick={handleSubmitQuote}
|
||||||
|
style={{ padding: '0.6rem 1.5rem' }}
|
||||||
|
>
|
||||||
|
Create Quote
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Preview Modal */}
|
||||||
|
{previewQuote && (
|
||||||
|
<div style={{
|
||||||
|
position: 'fixed',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
background: 'rgba(0,0,0,0.7)',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
zIndex: 9999,
|
||||||
|
backdropFilter: 'blur(4px)',
|
||||||
|
padding: '2rem'
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
|
background: 'linear-gradient(135deg, rgba(20,25,35,0.98) 0%, rgba(30,35,45,0.98) 100%)',
|
||||||
|
border: '1px solid rgba(255,215,0,0.3)',
|
||||||
|
borderRadius: '12px',
|
||||||
|
padding: '2rem',
|
||||||
|
maxWidth: '800px',
|
||||||
|
width: '100%',
|
||||||
|
maxHeight: '90vh',
|
||||||
|
overflow: 'auto',
|
||||||
|
boxShadow: '0 20px 60px rgba(0,0,0,0.5)'
|
||||||
|
}}>
|
||||||
|
{/* Header */}
|
||||||
|
<div style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: '2rem',
|
||||||
|
paddingBottom: '1rem',
|
||||||
|
borderBottom: '2px solid rgba(255,215,0,0.3)'
|
||||||
|
}}>
|
||||||
|
<div>
|
||||||
|
<h2 style={{ margin: 0, color: '#ffd700', fontSize: '1.8rem' }}>
|
||||||
|
Quote Preview
|
||||||
|
</h2>
|
||||||
|
<p style={{ margin: '0.5rem 0 0 0', color: '#888', fontSize: '0.9rem' }}>
|
||||||
|
{previewQuote.id}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setPreviewQuote(null)}
|
||||||
|
style={{
|
||||||
|
background: 'rgba(255,255,255,0.1)',
|
||||||
|
border: 'none',
|
||||||
|
color: '#fff',
|
||||||
|
fontSize: '1.5rem',
|
||||||
|
width: '40px',
|
||||||
|
height: '40px',
|
||||||
|
borderRadius: '8px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
transition: 'all 0.3s ease'
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => e.currentTarget.style.background = 'rgba(255,68,68,0.3)'}
|
||||||
|
onMouseLeave={(e) => e.currentTarget.style.background = 'rgba(255,255,255,0.1)'}
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Quote Details */}
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1.5rem' }}>
|
||||||
|
<div>
|
||||||
|
<div style={{ color: '#888', fontSize: '0.85rem', marginBottom: '0.5rem' }}>Customer</div>
|
||||||
|
<div style={{ color: '#fff', fontSize: '1.1rem', fontWeight: 600 }}>{previewQuote.customer}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div style={{ color: '#888', fontSize: '0.85rem', marginBottom: '0.5rem' }}>Status</div>
|
||||||
|
<span className={styles.badge} style={getStatusBadgeStyle(previewQuote.status)}>
|
||||||
|
{previewQuote.status}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div style={{ color: '#888', fontSize: '0.85rem', marginBottom: '0.5rem' }}>Created Date</div>
|
||||||
|
<div style={{ color: '#fff', fontSize: '1rem' }}>{previewQuote.createdAt}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div style={{ color: '#888', fontSize: '0.85rem', marginBottom: '0.5rem' }}>Valid Until</div>
|
||||||
|
<div style={{ color: '#fff', fontSize: '1rem' }}>{previewQuote.validUntil}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Quote Title */}
|
||||||
|
<div style={{ margin: '2rem 0' }}>
|
||||||
|
<div style={{ color: '#888', fontSize: '0.85rem', marginBottom: '0.5rem' }}>Quote Title</div>
|
||||||
|
<div style={{ color: '#fff', fontSize: '1.2rem', fontWeight: 600 }}>{previewQuote.title}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Value */}
|
||||||
|
<div style={{
|
||||||
|
background: 'rgba(255,215,0,0.1)',
|
||||||
|
border: '1px solid rgba(255,215,0,0.3)',
|
||||||
|
borderRadius: '8px',
|
||||||
|
padding: '1.5rem',
|
||||||
|
marginTop: '2rem'
|
||||||
|
}}>
|
||||||
|
<div style={{ color: '#888', fontSize: '0.85rem', marginBottom: '0.5rem' }}>Total Value</div>
|
||||||
|
<div style={{ color: '#ffd700', fontSize: '2rem', fontWeight: 700 }}>
|
||||||
|
₹{previewQuote.value.toLocaleString('en-IN')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
|
<div style={{ display: 'flex', gap: '1rem', justifyContent: 'flex-end', marginTop: '2rem' }}>
|
||||||
|
<button
|
||||||
|
className={styles.btnSecondary}
|
||||||
|
onClick={() => setPreviewQuote(null)}
|
||||||
|
style={{ padding: '0.75rem 1.5rem' }}
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={styles.btnPrimary}
|
||||||
|
onClick={() => {
|
||||||
|
handleDownload(previewQuote.id);
|
||||||
|
setPreviewQuote(null);
|
||||||
|
}}
|
||||||
|
style={{ padding: '0.75rem 1.5rem' }}
|
||||||
|
>
|
||||||
|
⬇️ Download PDF
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className={styles.sectionHeader}>
|
||||||
|
<div>
|
||||||
|
<h1 className={styles.pageTitle}>Quotes</h1>
|
||||||
|
<p className={styles.pageSubtitle}>Create and manage quotations</p>
|
||||||
|
</div>
|
||||||
|
<button className={styles.btnPrimary} onClick={handleCreateQuote}>
|
||||||
|
+ New Quote
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Filter Bar */}
|
||||||
|
<div className={styles.filterBar}>
|
||||||
|
<div className={styles.filterGroup}>
|
||||||
|
<label className={styles.filterLabel}>Status</label>
|
||||||
|
<select
|
||||||
|
className={styles.filterSelect}
|
||||||
|
value={filterStatus}
|
||||||
|
onChange={(e) => setFilterStatus(e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="all">All Status</option>
|
||||||
|
<option value="Draft">Draft</option>
|
||||||
|
<option value="Sent">Sent</option>
|
||||||
|
<option value="Approved">Approved</option>
|
||||||
|
<option value="Rejected">Rejected</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Quotes Table */}
|
||||||
|
<div className={styles.section}>
|
||||||
|
<table className={styles.table}>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Quote ID</th>
|
||||||
|
<th>Customer</th>
|
||||||
|
<th>Title</th>
|
||||||
|
<th>Value</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Created</th>
|
||||||
|
<th>Valid Until</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{filteredQuotes.map(quote => (
|
||||||
|
<tr key={quote.id}>
|
||||||
|
<td style={{ color: '#888', fontSize: '0.9rem', fontWeight: 600 }}>{quote.id}</td>
|
||||||
|
<td style={{ fontWeight: 600 }}>{quote.customer}</td>
|
||||||
|
<td>{quote.title}</td>
|
||||||
|
<td style={{ color: '#ffd700', fontWeight: 700 }}>{formatCurrency(quote.value)}</td>
|
||||||
|
<td>
|
||||||
|
<span
|
||||||
|
className={styles.badge}
|
||||||
|
style={getStatusBadgeStyle(quote.status)}
|
||||||
|
>
|
||||||
|
{quote.status}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td style={{ color: '#888', fontSize: '0.85rem' }}>{quote.createdAt}</td>
|
||||||
|
<td style={{ color: '#888', fontSize: '0.85rem' }}>{quote.validUntil}</td>
|
||||||
|
<td>
|
||||||
|
<div style={{ display: 'flex', gap: '0.5rem' }}>
|
||||||
|
<button
|
||||||
|
className={styles.btnSecondary}
|
||||||
|
style={{ padding: '0.4rem 0.8rem', fontSize: '0.8rem' }}
|
||||||
|
onClick={() => handlePreview(quote.id)}
|
||||||
|
>
|
||||||
|
👁️ Preview
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={styles.btnPrimary}
|
||||||
|
style={{ padding: '0.4rem 0.8rem', fontSize: '0.8rem' }}
|
||||||
|
onClick={() => handleDownload(quote.id)}
|
||||||
|
>
|
||||||
|
⬇️ PDF
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
{filteredQuotes.length === 0 && (
|
||||||
|
<div className={styles.emptyState}>
|
||||||
|
<div className={styles.emptyStateIcon}>📄</div>
|
||||||
|
<div className={styles.emptyStateTitle}>No quotes found</div>
|
||||||
|
<div className={styles.emptyStateText}>Try adjusting your filters or create a new quote</div>
|
||||||
|
<button className={styles.btnPrimary} onClick={handleCreateQuote}>
|
||||||
|
+ Create Quote
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
446
src/app/dashboard/dashboard.module.css
Normal file
@@ -0,0 +1,446 @@
|
|||||||
|
.dashboardContainer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: var(--bg-deep);
|
||||||
|
background-image:
|
||||||
|
linear-gradient(135deg, rgba(5, 50, 25, 0.65), rgba(2, 20, 10, 0.9)),
|
||||||
|
url('/bg_pennyzo.png');
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
color: #fff;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Top Navbar */
|
||||||
|
.topNavbar {
|
||||||
|
height: 70px;
|
||||||
|
background: rgba(10, 20, 30, 0.6);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
-webkit-backdrop-filter: blur(12px);
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 2rem;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 100;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.navLeft {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userProfile {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.8rem;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 50px;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userProfile:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border-color: var(--color-gold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.userAvatar {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: linear-gradient(135deg, var(--color-gold), #b8860b);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #000;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userName {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userRole {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.navCenter {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.companySwitcher {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.8rem;
|
||||||
|
background: rgba(0, 0, 0, 0.4);
|
||||||
|
padding: 0.5rem 1.2rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid rgba(255, 215, 0, 0.3);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.companySwitcher:hover {
|
||||||
|
box-shadow: 0 0 15px rgba(255, 215, 0, 0.1);
|
||||||
|
border-color: var(--color-gold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.companyLogo {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.companyName {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-gold);
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdownArrow {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.navRight {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.onboardingLink {
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--color-cyan);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.onboardingLink:hover {
|
||||||
|
background: rgba(0, 255, 255, 0.1);
|
||||||
|
border-color: rgba(0, 255, 255, 0.3);
|
||||||
|
box-shadow: 0 0 10px rgba(0, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Language Selector */
|
||||||
|
.languageSelector {
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
border: 1px solid rgba(255, 215, 0, 0.3);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
margin-right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.languageSelector:hover {
|
||||||
|
background: rgba(255, 215, 0, 0.1);
|
||||||
|
border-color: rgba(255, 215, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.languageSelector:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: rgba(255, 215, 0, 0.6);
|
||||||
|
box-shadow: 0 0 0 3px rgba(255, 215, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.languageSelector option {
|
||||||
|
background: rgba(10, 20, 30, 0.95);
|
||||||
|
color: #fff;
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Layout Grid */
|
||||||
|
.mainLayout {
|
||||||
|
display: flex;
|
||||||
|
margin-top: 70px;
|
||||||
|
/* Navbar height */
|
||||||
|
height: calc(100vh - 70px);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contentArea {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 2rem;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Left Sidebar */
|
||||||
|
.leftSidebar {
|
||||||
|
width: 260px;
|
||||||
|
background: rgba(10, 20, 30, 0.4);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border-right: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 1.5rem 1rem;
|
||||||
|
position: relative;
|
||||||
|
order: -1;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
/* Hide scrollbar for Chrome, Safari and Opera */
|
||||||
|
scrollbar-width: none;
|
||||||
|
/* Firefox */
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
/* IE and Edge */
|
||||||
|
/* Ensure it appears first if using flex order, but DOM order is better */
|
||||||
|
}
|
||||||
|
|
||||||
|
.leftSidebar::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
/* Chrome, Safari, Opera */
|
||||||
|
}
|
||||||
|
|
||||||
|
.leftSidebar.collapsed {
|
||||||
|
width: 70px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggleBtn {
|
||||||
|
position: absolute;
|
||||||
|
top: 1rem;
|
||||||
|
right: 0.5rem;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: linear-gradient(135deg, rgba(15, 20, 30, 0.95), rgba(25, 30, 40, 0.95));
|
||||||
|
border: 1px solid rgba(255, 215, 0, 0.3);
|
||||||
|
color: #ffd700;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
z-index: 1000;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggleBtn:hover {
|
||||||
|
background: linear-gradient(135deg, rgba(25, 30, 40, 0.98), rgba(35, 40, 50, 0.98));
|
||||||
|
border-color: rgba(255, 215, 0, 0.6);
|
||||||
|
transform: scale(1.1);
|
||||||
|
box-shadow: 0 4px 12px rgba(255, 215, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.leftSidebar.collapsed .toggleBtn {
|
||||||
|
right: 50%;
|
||||||
|
transform: translateX(50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.leftSidebar.collapsed .toggleBtn:hover {
|
||||||
|
transform: translateX(50%) scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.leftSidebar.collapsed .menuItem {
|
||||||
|
justify-content: center;
|
||||||
|
padding: 1rem 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leftSidebar.collapsed .menuItem .menuIcon {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menuItem {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 1rem;
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 12px;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
transition: all 0.2s;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menuItem:hover,
|
||||||
|
.menuItem.active {
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
color: var(--color-gold);
|
||||||
|
border-color: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menuItem.active {
|
||||||
|
background: linear-gradient(90deg, rgba(255, 215, 0, 0.1), transparent);
|
||||||
|
border-left: 3px solid var(--color-gold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menuIcon {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
min-width: 24px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expandArrow {
|
||||||
|
margin-left: auto;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
transition: transform 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expandArrow.expanded {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.subMenu {
|
||||||
|
margin-left: 1rem;
|
||||||
|
border-left: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
padding-left: 0.5rem;
|
||||||
|
overflow: hidden;
|
||||||
|
max-height: 0;
|
||||||
|
opacity: 0;
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.subMenu.expanded {
|
||||||
|
max-height: 500px;
|
||||||
|
/* Arbitrary large height */
|
||||||
|
opacity: 1;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subMenuItem {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
padding: 0.7rem 1rem;
|
||||||
|
padding-left: 2rem;
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subMenuItem>span:first-child {
|
||||||
|
color: rgba(255, 215, 0, 0.6);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
min-width: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subMenuItem:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subMenuItem:hover>span:first-child {
|
||||||
|
color: rgba(255, 215, 0, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.subMenuItem.active {
|
||||||
|
background: rgba(255, 215, 0, 0.15);
|
||||||
|
color: var(--color-gold);
|
||||||
|
border-left: 3px solid var(--color-gold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.subMenuItem.active>span:first-child {
|
||||||
|
color: var(--color-gold);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dashboard Content */
|
||||||
|
.dashboardHeader {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcomeText {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.roleBadge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.2rem 0.8rem;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.roleBadge.founder {
|
||||||
|
background: rgba(255, 215, 0, 0.1);
|
||||||
|
border: 1px solid var(--color-gold);
|
||||||
|
color: var(--color-gold);
|
||||||
|
box-shadow: 0 0 10px rgba(255, 215, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.roleBadge.employee {
|
||||||
|
background: rgba(0, 255, 255, 0.1);
|
||||||
|
border: 1px solid var(--color-cyan);
|
||||||
|
color: var(--color-cyan);
|
||||||
|
box-shadow: 0 0 10px rgba(0, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glassCard {
|
||||||
|
background: rgba(255, 255, 255, 0.03);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
transition: transform 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glassCard:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
border-color: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.statsGrid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||||||
|
gap: 1.5rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statValue {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #fff;
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statLabel {
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
169
src/app/dashboard/documents/[id]/page.tsx
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useParams } from 'next/navigation';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import styles from '../dms.module.css';
|
||||||
|
|
||||||
|
export default function DocumentDetailPage() {
|
||||||
|
const params = useParams();
|
||||||
|
const id = params?.id;
|
||||||
|
|
||||||
|
// Detailed Mock Data (in a real app, fetch by ID)
|
||||||
|
const [doc, setDoc] = useState({
|
||||||
|
id: id,
|
||||||
|
name: 'Project_Alpha_proposal.pdf',
|
||||||
|
type: 'PDF',
|
||||||
|
size: '2.4 MB',
|
||||||
|
date: '2024-11-20',
|
||||||
|
author: 'Vikram Singh',
|
||||||
|
tags: ['Project', 'Alpha'],
|
||||||
|
description: 'Initial proposal for the Alpha project phases and budget.',
|
||||||
|
versions: [
|
||||||
|
{ v: '1.0', date: '2024-11-20', user: 'Vikram Singh' }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
|
const [newTag, setNewTag] = useState('');
|
||||||
|
|
||||||
|
const handleAddTag = (e: React.KeyboardEvent) => {
|
||||||
|
if (e.key === 'Enter' && newTag) {
|
||||||
|
setDoc({ ...doc, tags: [...doc.tags, newTag] });
|
||||||
|
setNewTag('');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeTag = (tagToRemove: string) => {
|
||||||
|
setDoc({ ...doc, tags: doc.tags.filter(t => t !== tagToRemove) });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
{/* Header */}
|
||||||
|
<div className={styles.header}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
|
||||||
|
<Link href="/dashboard/documents/all" style={{ fontSize: '1.5rem', textDecoration: 'none', color: '#fff' }}>←</Link>
|
||||||
|
<div>
|
||||||
|
<h1>{doc.name}</h1>
|
||||||
|
<p className={styles.subtitle}>Document ID: {doc.id}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.actions}>
|
||||||
|
<button
|
||||||
|
className={styles.btnPrimary}
|
||||||
|
style={{ background: 'transparent', border: '1px solid rgba(255,215,0,0.5)', color: '#FFD700' }}
|
||||||
|
onClick={() => setIsEditing(!isEditing)}
|
||||||
|
>
|
||||||
|
{isEditing ? 'Done Editing' : 'Edit Metadata'}
|
||||||
|
</button>
|
||||||
|
<button className={styles.btnPrimary} style={{ marginLeft: '1rem' }}>
|
||||||
|
Download
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 350px', gap: '2rem' }}>
|
||||||
|
|
||||||
|
{/* Left: Preview Area */}
|
||||||
|
<div className={styles.section} style={{ height: '600px', display: 'flex', alignItems: 'center', justifyContent: 'center', background: 'rgba(0,0,0,0.2)' }}>
|
||||||
|
<div style={{ textAlign: 'center', opacity: 0.5 }}>
|
||||||
|
<span style={{ fontSize: '4rem' }}>📄</span>
|
||||||
|
<h3>No Preview Available</h3>
|
||||||
|
<p>{doc.type} File Viewer Placeholder</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right: Metadata Panel */}
|
||||||
|
<div className={styles.section}>
|
||||||
|
<h3 style={{ marginBottom: '1.5rem', borderBottom: '1px solid rgba(255,255,255,0.1)', paddingBottom: '0.5rem' }}>Metadata</h3>
|
||||||
|
|
||||||
|
<div style={{ marginBottom: '1.5rem' }}>
|
||||||
|
<label style={{ display: 'block', color: '#888', marginBottom: '0.5rem', fontSize: '0.85rem' }}>File Name</label>
|
||||||
|
{isEditing ? (
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={doc.name}
|
||||||
|
onChange={(e) => setDoc({ ...doc, name: e.target.value })}
|
||||||
|
style={{ width: '100%', padding: '8px', background: 'rgba(0,0,0,0.2)', border: '1px solid #444', color: '#fff', borderRadius: '4px' }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div style={{ fontSize: '1rem' }}>{doc.name}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ marginBottom: '1.5rem' }}>
|
||||||
|
<label style={{ display: 'block', color: '#888', marginBottom: '0.5rem', fontSize: '0.85rem' }}>Description</label>
|
||||||
|
{isEditing ? (
|
||||||
|
<textarea
|
||||||
|
value={doc.description}
|
||||||
|
onChange={(e) => setDoc({ ...doc, description: e.target.value })}
|
||||||
|
style={{ width: '100%', padding: '8px', background: 'rgba(0,0,0,0.2)', border: '1px solid #444', color: '#fff', borderRadius: '4px', height: '80px' }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div style={{ fontSize: '0.9rem', color: '#ddd', lineHeight: '1.4' }}>{doc.description}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ marginBottom: '1.5rem' }}>
|
||||||
|
<label style={{ display: 'block', color: '#888', marginBottom: '0.5rem', fontSize: '0.85rem' }}>Tags</label>
|
||||||
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.5rem', marginBottom: isEditing ? '0.5rem' : '0' }}>
|
||||||
|
{doc.tags.map(tag => (
|
||||||
|
<span key={tag} style={{
|
||||||
|
background: 'rgba(255,215,0,0.1)',
|
||||||
|
color: '#FFD700',
|
||||||
|
padding: '2px 8px',
|
||||||
|
borderRadius: '12px',
|
||||||
|
fontSize: '0.75rem',
|
||||||
|
border: '1px solid rgba(255,215,0,0.2)',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '5px'
|
||||||
|
}}>
|
||||||
|
{tag}
|
||||||
|
{isEditing && (
|
||||||
|
<span
|
||||||
|
onClick={() => removeTag(tag)}
|
||||||
|
style={{ cursor: 'pointer', fontWeight: 'bold' }}
|
||||||
|
>×</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{isEditing && (
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Type tag & Enter..."
|
||||||
|
value={newTag}
|
||||||
|
onChange={(e) => setNewTag(e.target.value)}
|
||||||
|
onKeyDown={handleAddTag}
|
||||||
|
style={{ width: '100%', padding: '6px', background: 'rgba(0,0,0,0.2)', border: '1px solid #444', color: '#fff', borderRadius: '4px', fontSize: '0.85rem' }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem', marginTop: '2rem', paddingTop: '1rem', borderTop: '1px solid rgba(255,255,255,0.1)' }}>
|
||||||
|
<div>
|
||||||
|
<label style={{ display: 'block', color: '#888', fontSize: '0.8rem' }}>Uploaded By</label>
|
||||||
|
<div>{doc.author}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label style={{ display: 'block', color: '#888', fontSize: '0.8rem' }}>Date</label>
|
||||||
|
<div>{doc.date}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label style={{ display: 'block', color: '#888', fontSize: '0.8rem' }}>Size</label>
|
||||||
|
<div>{doc.size}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label style={{ display: 'block', color: '#888', fontSize: '0.8rem' }}>Format</label>
|
||||||
|
<div>{doc.type}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
160
src/app/dashboard/documents/activity/page.tsx
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
'use client';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import styles from '../legal.module.css';
|
||||||
|
import hamStyles from '../../ham/ham.module.css';
|
||||||
|
|
||||||
|
const MOCK_ACTIVITIES = [
|
||||||
|
{ id: 1, type: 'upload', document: 'Employment Contract.pdf', user: 'HR Team', timestamp: '2024-12-20 14:30', ip: '192.168.1.100', location: 'Mumbai, India' },
|
||||||
|
{ id: 2, type: 'request', document: 'NDA Agreement.pdf', user: 'Legal Team', timestamp: '2024-12-20 13:15', ip: '192.168.1.101', location: 'Delhi, India' },
|
||||||
|
{ id: 3, type: 'opened', document: 'Service Agreement.docx', user: 'John Doe', timestamp: '2024-12-20 12:45', ip: '203.0.113.45', location: 'Bangalore, India' },
|
||||||
|
{ id: 4, type: 'signed', document: 'Partnership Agreement.pdf', user: 'Sarah Williams', timestamp: '2024-12-20 11:20', ip: '198.51.100.23', location: 'Pune, India' },
|
||||||
|
{ id: 5, type: 'completed', document: 'Consulting Agreement.pdf', user: 'System', timestamp: '2024-12-20 10:00', ip: 'N/A', location: 'N/A' },
|
||||||
|
{ id: 6, type: 'opened', document: 'Employment Contract.pdf', user: 'Mike Johnson', timestamp: '2024-12-19 16:30', ip: '203.0.113.67', location: 'Chennai, India' },
|
||||||
|
{ id: 7, type: 'signed', document: 'NDA Agreement.pdf', user: 'Emily Chen', timestamp: '2024-12-19 15:10', ip: '198.51.100.89', location: 'Hyderabad, India' },
|
||||||
|
{ id: 8, type: 'request', document: 'Lease Agreement.pdf', user: 'Finance Team', timestamp: '2024-12-19 14:00', ip: '192.168.1.102', location: 'Mumbai, India' }
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function ActivityLogPage() {
|
||||||
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
const [typeFilter, setTypeFilter] = useState('All');
|
||||||
|
const [dateFilter, setDateFilter] = useState('');
|
||||||
|
|
||||||
|
const filteredActivities = MOCK_ACTIVITIES.filter(activity => {
|
||||||
|
const matchesSearch = activity.document.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
|
activity.user.toLowerCase().includes(searchQuery.toLowerCase());
|
||||||
|
const matchesType = typeFilter === 'All' || activity.type === typeFilter;
|
||||||
|
const matchesDate = !dateFilter || activity.timestamp.startsWith(dateFilter);
|
||||||
|
return matchesSearch && matchesType && matchesDate;
|
||||||
|
});
|
||||||
|
|
||||||
|
const getActivityIcon = (type: string) => {
|
||||||
|
const icons: { [key: string]: string } = {
|
||||||
|
'upload': '📤',
|
||||||
|
'request': '✉️',
|
||||||
|
'opened': '👁️',
|
||||||
|
'signed': '✍️',
|
||||||
|
'completed': '✅'
|
||||||
|
};
|
||||||
|
return icons[type] || '📄';
|
||||||
|
};
|
||||||
|
|
||||||
|
const getActivityText = (activity: typeof MOCK_ACTIVITIES[0]) => {
|
||||||
|
const actions: { [key: string]: string } = {
|
||||||
|
'upload': 'uploaded',
|
||||||
|
'request': 'requested signature for',
|
||||||
|
'opened': 'opened',
|
||||||
|
'signed': 'signed',
|
||||||
|
'completed': 'completed all signatures for'
|
||||||
|
};
|
||||||
|
return `${activity.user} ${actions[activity.type]} ${activity.document}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={hamStyles.hamContainer}>
|
||||||
|
<div className={hamStyles.sectionHeader}>
|
||||||
|
<div>
|
||||||
|
<h1 className={hamStyles.pageTitle}>Activity Log</h1>
|
||||||
|
<p className={hamStyles.pageSubtitle}>Complete audit trail of all document activities</p>
|
||||||
|
</div>
|
||||||
|
<button className={hamStyles.btnPrimary} onClick={() => alert('Export functionality coming soon')}>
|
||||||
|
📊 Export Log
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Filters */}
|
||||||
|
<div className={hamStyles.filterBar}>
|
||||||
|
<div className={hamStyles.filterGroup}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className={hamStyles.inputField}
|
||||||
|
placeholder="Search activities..."
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
style={{ minWidth: '300px' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={hamStyles.filterGroup}>
|
||||||
|
<label className={hamStyles.filterLabel}>Activity Type</label>
|
||||||
|
<select
|
||||||
|
className={hamStyles.filterSelect}
|
||||||
|
value={typeFilter}
|
||||||
|
onChange={(e) => setTypeFilter(e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="All">All Activities</option>
|
||||||
|
<option value="upload">Uploads</option>
|
||||||
|
<option value="request">Signature Requests</option>
|
||||||
|
<option value="opened">Document Opens</option>
|
||||||
|
<option value="signed">Signatures</option>
|
||||||
|
<option value="completed">Completions</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className={hamStyles.filterGroup}>
|
||||||
|
<label className={hamStyles.filterLabel}>Date</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
className={hamStyles.inputField}
|
||||||
|
value={dateFilter}
|
||||||
|
onChange={(e) => setDateFilter(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Stats */}
|
||||||
|
<div className={hamStyles.widgetsGrid}>
|
||||||
|
<div className={hamStyles.widget}>
|
||||||
|
<div className={hamStyles.widgetIcon}>📊</div>
|
||||||
|
<div className={hamStyles.widgetValue}>{MOCK_ACTIVITIES.length}</div>
|
||||||
|
<div className={hamStyles.widgetLabel}>Total Activities</div>
|
||||||
|
</div>
|
||||||
|
<div className={hamStyles.widget}>
|
||||||
|
<div className={hamStyles.widgetIcon}>📤</div>
|
||||||
|
<div className={hamStyles.widgetValue}>{MOCK_ACTIVITIES.filter(a => a.type === 'upload').length}</div>
|
||||||
|
<div className={hamStyles.widgetLabel}>Documents Uploaded</div>
|
||||||
|
</div>
|
||||||
|
<div className={hamStyles.widget}>
|
||||||
|
<div className={hamStyles.widgetIcon}>✍️</div>
|
||||||
|
<div className={hamStyles.widgetValue}>{MOCK_ACTIVITIES.filter(a => a.type === 'signed').length}</div>
|
||||||
|
<div className={hamStyles.widgetLabel}>Signatures</div>
|
||||||
|
</div>
|
||||||
|
<div className={hamStyles.widget}>
|
||||||
|
<div className={hamStyles.widgetIcon}>✅</div>
|
||||||
|
<div className={hamStyles.widgetValue}>{MOCK_ACTIVITIES.filter(a => a.type === 'completed').length}</div>
|
||||||
|
<div className={hamStyles.widgetLabel}>Completed</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Activity Timeline */}
|
||||||
|
<div className={hamStyles.section}>
|
||||||
|
<h3 style={{ color: '#ffd700', marginBottom: '1.5rem' }}>Activity Timeline</h3>
|
||||||
|
{filteredActivities.length === 0 ? (
|
||||||
|
<div className={hamStyles.emptyState}>
|
||||||
|
<div style={{ fontSize: '3rem', marginBottom: '1rem' }}>📊</div>
|
||||||
|
<p>No activities found</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className={styles.activityTimeline}>
|
||||||
|
{filteredActivities.map(activity => (
|
||||||
|
<div key={activity.id} className={styles.activityItem}>
|
||||||
|
<div className={styles.activityContent}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'start', gap: '1rem' }}>
|
||||||
|
<div style={{ fontSize: '1.5rem' }}>{getActivityIcon(activity.type)}</div>
|
||||||
|
<div style={{ flex: 1 }}>
|
||||||
|
<div style={{ color: '#fff', marginBottom: '0.5rem', fontSize: '1rem' }}>
|
||||||
|
{getActivityText(activity)}
|
||||||
|
</div>
|
||||||
|
<div className={styles.activityMeta}>
|
||||||
|
<span>🕐 {activity.timestamp}</span>
|
||||||
|
<span>📍 {activity.location}</span>
|
||||||
|
<span>🌐 {activity.ip}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
182
src/app/dashboard/documents/all/page.tsx
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import FileRow from '@/components/dms/FileRow';
|
||||||
|
import styles from '../dms.module.css'; // Reusing dashboard styles for consistency
|
||||||
|
import ShareModal from '@/components/dms/ShareModal';
|
||||||
|
import Toast from '@/components/dms/Toast';
|
||||||
|
import ConfirmationModal from '@/components/dms/ConfirmationModal';
|
||||||
|
|
||||||
|
export default function AllDocumentsPage() {
|
||||||
|
const [filter, setFilter] = useState('All');
|
||||||
|
const [search, setSearch] = useState('');
|
||||||
|
|
||||||
|
// Mock Data
|
||||||
|
const [files, setFiles] = useState([
|
||||||
|
{ id: 1, name: 'Project_Alpha_proposal.pdf', type: 'PDF', size: '2.4 MB', date: '2024-11-20', tags: ['Project', 'Alpha'] },
|
||||||
|
{ id: 2, name: 'Invoice_Nov_2024.pdf', type: 'PDF', size: '1.1 MB', date: '2024-11-19', tags: ['Finance', 'Invoice'] },
|
||||||
|
{ id: 3, name: 'Site_Images_Nov.zip', type: 'ZIP', size: '15 MB', date: '2024-11-18', tags: ['Site', 'Images'] },
|
||||||
|
{ id: 4, name: 'Meeting_Notes_Q3.docx', type: 'DOCX', size: '450 KB', date: '2024-11-15', tags: ['Internal'] },
|
||||||
|
{ id: 5, name: 'Company_Policy_v2.pdf', type: 'PDF', size: '4.2 MB', date: '2024-11-10', tags: ['HR', 'Policy'] },
|
||||||
|
{ id: 6, name: 'Logo_Pack.zip', type: 'ZIP', size: '22 MB', date: '2024-11-05', tags: ['Branding'] },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const handleDelete = (id: number | string) => {
|
||||||
|
setConfirmModal({
|
||||||
|
isOpen: true,
|
||||||
|
title: 'Delete Document',
|
||||||
|
message: 'Are you sure you want to delete this file? This action cannot be undone.',
|
||||||
|
onConfirm: () => {
|
||||||
|
setFiles(prev => prev.filter(f => f.id !== id));
|
||||||
|
setConfirmModal(null);
|
||||||
|
setToast({ message: 'File deleted successfully', type: 'success' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const filteredFiles = files.filter(f => {
|
||||||
|
const matchesSearch = f.name.toLowerCase().includes(search.toLowerCase());
|
||||||
|
const matchesType = filter === 'All' ? true : f.type === filter;
|
||||||
|
return matchesSearch && matchesType;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Share Modal Logic
|
||||||
|
const [isShareModalOpen, setIsShareModalOpen] = useState(false);
|
||||||
|
// State for UI Components
|
||||||
|
const [toast, setToast] = useState<{ message: string; type: 'success' | 'error' | 'info' } | null>(null);
|
||||||
|
const [confirmModal, setConfirmModal] = useState<{
|
||||||
|
isOpen: boolean;
|
||||||
|
title: string;
|
||||||
|
message: string;
|
||||||
|
onConfirm: () => void;
|
||||||
|
} | null>(null);
|
||||||
|
const [fileToShare, setFileToShare] = useState<any>(null);
|
||||||
|
|
||||||
|
const mockTeamMembers = [
|
||||||
|
{ id: 101, name: 'Vikram Singh', role: 'Project Manager', avatar: 'VS' },
|
||||||
|
{ id: 102, name: 'Anjali Gupta', role: 'Lead Architect', avatar: 'AG' },
|
||||||
|
{ id: 103, name: 'Rohan Mehta', role: 'Finance Head', avatar: 'RM' },
|
||||||
|
{ id: 104, name: 'Priya Sharma', role: 'HR Manager', avatar: 'PS' },
|
||||||
|
{ id: 105, name: 'Karan Patel', role: 'Developer', avatar: 'KP' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const openShareModal = (file: any) => {
|
||||||
|
setFileToShare(file);
|
||||||
|
setIsShareModalOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleShareConfirm = (userIds: (number | string)[], permissions: string[]) => {
|
||||||
|
console.log('Sharing', fileToShare?.name, 'with', userIds, 'Permissions:', permissions);
|
||||||
|
// Here you would make an API call
|
||||||
|
setToast({
|
||||||
|
message: `Successfully shared "${fileToShare?.name}" with ${userIds.length} team members!`,
|
||||||
|
type: 'success'
|
||||||
|
});
|
||||||
|
setIsShareModalOpen(false);
|
||||||
|
setFileToShare(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
{toast && (
|
||||||
|
<Toast
|
||||||
|
message={toast.message}
|
||||||
|
type={toast.type}
|
||||||
|
onClose={() => setToast(null)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{confirmModal && confirmModal.isOpen && (
|
||||||
|
<ConfirmationModal
|
||||||
|
title={confirmModal.title}
|
||||||
|
message={confirmModal.message}
|
||||||
|
onConfirm={confirmModal.onConfirm}
|
||||||
|
onCancel={() => setConfirmModal(null)}
|
||||||
|
confirmText="Delete"
|
||||||
|
isDestructive={true}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isShareModalOpen && fileToShare && (
|
||||||
|
<ShareModal
|
||||||
|
fileName={fileToShare.name}
|
||||||
|
users={mockTeamMembers}
|
||||||
|
onClose={() => setIsShareModalOpen(false)}
|
||||||
|
onShare={handleShareConfirm}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Header */}
|
||||||
|
<div className={styles.header}>
|
||||||
|
<div>
|
||||||
|
<h1>All Documents</h1>
|
||||||
|
<p className={styles.subtitle}>Browse and manage your entire library</p>
|
||||||
|
</div>
|
||||||
|
<div className={styles.actions}>
|
||||||
|
<Link href="/dashboard/documents/upload" className={styles.btnPrimary}>
|
||||||
|
+ Upload New
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Filters & Search */}
|
||||||
|
<div style={{ display: 'flex', gap: '1rem', marginBottom: '2rem', flexWrap: 'wrap' }}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search files..."
|
||||||
|
value={search}
|
||||||
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
|
className={styles.searchInput}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<select
|
||||||
|
value={filter}
|
||||||
|
onChange={(e) => setFilter(e.target.value)}
|
||||||
|
className={styles.dropdown}
|
||||||
|
>
|
||||||
|
<option value="All">All Types</option>
|
||||||
|
<option value="PDF">PDF Documents</option>
|
||||||
|
<option value="ZIP">Archives (ZIP)</option>
|
||||||
|
<option value="DOCX">Word Documents</option>
|
||||||
|
<option value="IMG">Images</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Files List */}
|
||||||
|
<div className={styles.section}>
|
||||||
|
<div className={styles.tableContainer}>
|
||||||
|
<table className={styles.table}>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Tags</th>
|
||||||
|
<th>Size</th>
|
||||||
|
<th>Date</th>
|
||||||
|
<th>Action</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{filteredFiles.length > 0 ? (
|
||||||
|
filteredFiles.map(file => (
|
||||||
|
<FileRow
|
||||||
|
key={file.id}
|
||||||
|
file={file}
|
||||||
|
onDelete={handleDelete}
|
||||||
|
onShare={openShareModal}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={5} style={{ textAlign: 'center', padding: '3rem', color: '#666' }}>
|
||||||
|
No files found matching your criteria.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
266
src/app/dashboard/documents/dms.module.css
Normal file
@@ -0,0 +1,266 @@
|
|||||||
|
.container {
|
||||||
|
padding: 2rem;
|
||||||
|
color: #fff;
|
||||||
|
animation: fadeIn 0.5s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
color: #aaa;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btnPrimary {
|
||||||
|
background: linear-gradient(135deg, #FFD700 0%, #FFAA00 100%);
|
||||||
|
color: #000;
|
||||||
|
border: none;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: none;
|
||||||
|
display: inline-block;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btnPrimary:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 15px rgba(255, 215, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stats Cards */
|
||||||
|
.statsGrid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
|
gap: 1.5rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statCard {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 1rem;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statIcon {
|
||||||
|
font-size: 2rem;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statInfo h3 {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #aaa;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statInfo p {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--color-gold, #FFD700);
|
||||||
|
}
|
||||||
|
|
||||||
|
.subStat {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #666;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progressBar {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 4px;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progressBar div {
|
||||||
|
height: 100%;
|
||||||
|
background: var(--color-gold, #FFD700);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Table Section */
|
||||||
|
.section {
|
||||||
|
background: rgba(255, 255, 255, 0.03);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionHeader {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link {
|
||||||
|
color: var(--color-gold, #FFD700);
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableContainer {
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table th {
|
||||||
|
text-align: left;
|
||||||
|
padding: 1rem;
|
||||||
|
color: #888;
|
||||||
|
font-weight: 500;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table td {
|
||||||
|
padding: 1rem;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
color: #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fileName {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fileIcon {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iconBtn {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: #aaa;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Quick Actions */
|
||||||
|
.quickActions {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actionCard {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actionCard:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border-color: rgba(255, 215, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.actionIcon {
|
||||||
|
font-size: 2rem;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actionCard h3 {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actionCard p {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom Dropdown Style */
|
||||||
|
.dropdown {
|
||||||
|
padding: 10px 15px;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid rgba(255, 215, 0, 0.3);
|
||||||
|
background: rgba(10, 20, 30, 0.6);
|
||||||
|
color: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: inherit;
|
||||||
|
outline: none;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
min-width: 150px;
|
||||||
|
appearance: none;
|
||||||
|
/* Remove default arrow */
|
||||||
|
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23ffd700' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e");
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: right 10px center;
|
||||||
|
background-size: 16px;
|
||||||
|
padding-right: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown:hover,
|
||||||
|
.dropdown:focus {
|
||||||
|
border-color: var(--color-gold);
|
||||||
|
box-shadow: 0 0 10px rgba(255, 215, 0, 0.1);
|
||||||
|
background-color: rgba(10, 20, 30, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown option {
|
||||||
|
background: #050a10;
|
||||||
|
color: #fff;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Search Input Style */
|
||||||
|
.searchInput {
|
||||||
|
padding: 10px 15px;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
background: rgba(10, 20, 30, 0.4);
|
||||||
|
color: #fff;
|
||||||
|
min-width: 300px;
|
||||||
|
outline: none;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchInput:focus {
|
||||||
|
border-color: var(--color-gold);
|
||||||
|
background: rgba(10, 20, 30, 0.6);
|
||||||
|
box-shadow: 0 0 10px rgba(255, 215, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure global scrollbar applies */
|
||||||
|
.tableContainer {
|
||||||
|
overflow-x: auto;
|
||||||
|
/* Custom scrollbar triggers */
|
||||||
|
}
|
||||||
376
src/app/dashboard/documents/legal.module.css
Normal file
@@ -0,0 +1,376 @@
|
|||||||
|
/* Legal Module Styles - Luxury Tech Theme */
|
||||||
|
|
||||||
|
/* Import base styles */
|
||||||
|
@import '../ham/ham.module.css';
|
||||||
|
|
||||||
|
/* Consultation Booking */
|
||||||
|
.consultationCard {
|
||||||
|
background: linear-gradient(135deg, rgba(255, 255, 255, 0.05) 0%, rgba(255, 255, 255, 0.02) 100%);
|
||||||
|
border: 1px solid rgba(255, 215, 0, 0.2);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.consultationCard:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
border-color: rgba(255, 215, 0, 0.4);
|
||||||
|
box-shadow: 0 8px 24px rgba(255, 215, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lawyerAvatar {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: linear-gradient(135deg, #ffd700 0%, #ffed4e 100%);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #0a0e1a;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Signature Canvas */
|
||||||
|
.signatureCanvas {
|
||||||
|
border: 2px dashed rgba(255, 215, 0, 0.3);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(255, 255, 255, 0.02);
|
||||||
|
cursor: crosshair;
|
||||||
|
touch-action: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signatureCanvas:hover {
|
||||||
|
border-color: rgba(255, 215, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.signatureTools {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
padding: 1rem;
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signatureTool {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 6px;
|
||||||
|
color: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signatureTool:hover {
|
||||||
|
background: rgba(255, 215, 0, 0.1);
|
||||||
|
border-color: rgba(255, 215, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.signatureTool.active {
|
||||||
|
background: rgba(255, 215, 0, 0.2);
|
||||||
|
border-color: rgba(255, 215, 0, 0.5);
|
||||||
|
color: #ffd700;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Document Viewer */
|
||||||
|
.documentViewer {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 2rem;
|
||||||
|
min-height: 600px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.documentPage {
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.viewerControls {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
background: rgba(10, 14, 26, 0.95);
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Signature Field Placement */
|
||||||
|
.signatureField {
|
||||||
|
position: absolute;
|
||||||
|
border: 2px solid #ffd700;
|
||||||
|
background: rgba(255, 215, 0, 0.1);
|
||||||
|
cursor: move;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #ffd700;
|
||||||
|
font-weight: 600;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signatureField:hover {
|
||||||
|
background: rgba(255, 215, 0, 0.2);
|
||||||
|
box-shadow: 0 0 12px rgba(255, 215, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.signatureField.required::after {
|
||||||
|
content: '*';
|
||||||
|
color: #ff4444;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fieldHandle {
|
||||||
|
position: absolute;
|
||||||
|
right: -8px;
|
||||||
|
top: -8px;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
background: #ffd700;
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: nwse-resize;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Activity Timeline */
|
||||||
|
.activityTimeline {
|
||||||
|
position: relative;
|
||||||
|
padding-left: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activityTimeline::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 8px;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 2px;
|
||||||
|
background: linear-gradient(180deg, rgba(255, 215, 0, 0.5) 0%, rgba(255, 215, 0, 0.1) 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.activityItem {
|
||||||
|
position: relative;
|
||||||
|
padding: 1rem 0 1rem 1.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activityItem::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 1.5rem;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
background: #ffd700;
|
||||||
|
border: 3px solid #0a0e1a;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activityContent {
|
||||||
|
background: rgba(255, 255, 255, 0.03);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activityMeta {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #888;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Status Badges */
|
||||||
|
.statusDraft {
|
||||||
|
background: rgba(128, 128, 128, 0.2);
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statusPending {
|
||||||
|
background: rgba(255, 193, 7, 0.2);
|
||||||
|
color: #ffc107;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statusCompleted {
|
||||||
|
background: rgba(76, 175, 80, 0.2);
|
||||||
|
color: #4caf50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statusExpired {
|
||||||
|
background: rgba(244, 67, 54, 0.2);
|
||||||
|
color: #f44336;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* File Uploader */
|
||||||
|
.fileUploadZone {
|
||||||
|
border: 2px dashed rgba(255, 215, 0, 0.3);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 3rem 2rem;
|
||||||
|
text-align: center;
|
||||||
|
background: rgba(255, 255, 255, 0.02);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fileUploadZone:hover {
|
||||||
|
border-color: rgba(255, 215, 0, 0.5);
|
||||||
|
background: rgba(255, 215, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fileUploadZone.dragOver {
|
||||||
|
border-color: #ffd700;
|
||||||
|
background: rgba(255, 215, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploadIcon {
|
||||||
|
font-size: 3rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filePreview {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 1rem;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fileIcon {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fileInfo {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fileName {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #fff;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fileSize {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.removeFile {
|
||||||
|
background: rgba(244, 67, 54, 0.2);
|
||||||
|
border: 1px solid rgba(244, 67, 54, 0.3);
|
||||||
|
color: #f44336;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.removeFile:hover {
|
||||||
|
background: rgba(244, 67, 54, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Signer List */
|
||||||
|
.signerList {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signerItem {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 1rem;
|
||||||
|
background: rgba(255, 255, 255, 0.03);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signerNumber {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(255, 215, 0, 0.2);
|
||||||
|
border: 2px solid rgba(255, 215, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #ffd700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signerDetails {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signerName {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #fff;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signerEmail {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.removeSigner {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: #f44336;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.removeSigner:hover {
|
||||||
|
color: #ff6b6b;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Practice Area Cards */
|
||||||
|
.practiceAreaGrid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.practiceAreaCard {
|
||||||
|
padding: 1.5rem;
|
||||||
|
background: rgba(255, 255, 255, 0.03);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 8px;
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.practiceAreaCard:hover {
|
||||||
|
border-color: rgba(255, 215, 0, 0.3);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.practiceAreaCard.selected {
|
||||||
|
background: rgba(255, 215, 0, 0.1);
|
||||||
|
border-color: rgba(255, 215, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.practiceAreaIcon {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.practiceAreaName {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
131
src/app/dashboard/documents/page.tsx
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import styles from './dms.module.css';
|
||||||
|
import SmartSearchModal from '@/components/dms/SmartSearchModal'; // We'll need to create this
|
||||||
|
|
||||||
|
export default function DMSDashboard() {
|
||||||
|
const [stats] = useState({
|
||||||
|
totalFiles: 1240,
|
||||||
|
usedStorage: '4.2 GB',
|
||||||
|
totalStorage: '10 GB',
|
||||||
|
recentUploads: 5
|
||||||
|
});
|
||||||
|
|
||||||
|
const recentDocs = [
|
||||||
|
{ id: 1, name: 'Project_Alpha_proposal.pdf', type: 'PDF', size: '2.4 MB', date: '2024-11-20' },
|
||||||
|
{ id: 2, name: 'Invoice_Nov_2024.pdf', type: 'PDF', size: '1.1 MB', date: '2024-11-19' },
|
||||||
|
{ id: 3, name: 'Site_Images_Nov.zip', type: 'ZIP', size: '15 MB', date: '2024-11-18' },
|
||||||
|
{ id: 4, name: 'Meeting_Notes_Q3.docx', type: 'DOCX', size: '450 KB', date: '2024-11-15' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const [showSearch, setShowSearch] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
{showSearch && <SmartSearchModal onClose={() => setShowSearch(false)} />}
|
||||||
|
|
||||||
|
{/* Header */}
|
||||||
|
<div className={styles.header}>
|
||||||
|
<div>
|
||||||
|
<h1>Documents</h1>
|
||||||
|
<p className={styles.subtitle}>Securely manage and organize your digital assets</p>
|
||||||
|
</div>
|
||||||
|
<div className={styles.actions}>
|
||||||
|
<Link href="/dashboard/documents/upload" className={styles.btnPrimary}>
|
||||||
|
+ Upload New
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Stats Cards */}
|
||||||
|
<div className={styles.statsGrid}>
|
||||||
|
<div className={styles.statCard}>
|
||||||
|
<span className={styles.statIcon}>📂</span>
|
||||||
|
<div className={styles.statInfo}>
|
||||||
|
<h3>All Files</h3>
|
||||||
|
<p>{stats.totalFiles}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.statCard}>
|
||||||
|
<span className={styles.statIcon}>☁️</span>
|
||||||
|
<div className={styles.statInfo}>
|
||||||
|
<h3>Storage Used</h3>
|
||||||
|
<p>{stats.usedStorage} <span className={styles.subStat}>/ {stats.totalStorage}</span></p>
|
||||||
|
</div>
|
||||||
|
{/* Simple Progress Bar */}
|
||||||
|
<div className={styles.progressBar}>
|
||||||
|
<div style={{ width: '42%' }}></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.statCard}>
|
||||||
|
<span className={styles.statIcon}>🕒</span>
|
||||||
|
<div className={styles.statInfo}>
|
||||||
|
<h3>Recent Uploads</h3>
|
||||||
|
<p>{stats.recentUploads} <span className={styles.subStat}>this week</span></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Recent Documents Table */}
|
||||||
|
<div className={styles.section}>
|
||||||
|
<div className={styles.sectionHeader}>
|
||||||
|
<h2>Recent Documents</h2>
|
||||||
|
<Link href="/dashboard/documents/all" className={styles.link}>View All</Link>
|
||||||
|
</div>
|
||||||
|
<div className={styles.tableContainer}>
|
||||||
|
<table className={styles.table}>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>Size</th>
|
||||||
|
<th>Date</th>
|
||||||
|
<th>Action</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{recentDocs.map(doc => (
|
||||||
|
<tr key={doc.id}>
|
||||||
|
<td>
|
||||||
|
<div className={styles.fileName}>
|
||||||
|
<span className={styles.fileIcon}>📄</span>
|
||||||
|
{doc.name}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>{doc.type}</td>
|
||||||
|
<td>{doc.size}</td>
|
||||||
|
<td>{doc.date}</td>
|
||||||
|
<td>
|
||||||
|
<button className={styles.iconBtn}>⋮</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Quick Actions Grid */}
|
||||||
|
<h2 style={{ marginTop: '2rem', marginBottom: '1rem' }}>Quick Actions</h2>
|
||||||
|
<div className={styles.quickActions}>
|
||||||
|
<div className={styles.actionCard} onClick={() => setShowSearch(true)}>
|
||||||
|
<span className={styles.actionIcon}>🔍</span>
|
||||||
|
<h3>Smart Search</h3>
|
||||||
|
<p>Find documents by content or tags</p>
|
||||||
|
</div>
|
||||||
|
<div className={styles.actionCard}>
|
||||||
|
<span className={styles.actionIcon}>🏷️</span>
|
||||||
|
<h3>Manage Tags</h3>
|
||||||
|
<p>Organize files with custom labels</p>
|
||||||
|
</div>
|
||||||
|
<div className={styles.actionCard}>
|
||||||
|
<span className={styles.actionIcon}>🗑️</span>
|
||||||
|
<h3>Trash</h3>
|
||||||
|
<p>Recover deleted items</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
489
src/app/dashboard/documents/sign/page.tsx
Normal file
@@ -0,0 +1,489 @@
|
|||||||
|
'use client';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import styles from '../legal.module.css';
|
||||||
|
import hamStyles from '../../ham/ham.module.css';
|
||||||
|
import shared from '../../shared/components.module.css';
|
||||||
|
import { useToast } from '@/context/ToastContext';
|
||||||
|
|
||||||
|
const MOCK_DOCUMENTS = [
|
||||||
|
{ id: 1, name: 'Employment Contract - John Doe.pdf', uploadedBy: 'HR Team', date: '2024-12-15', status: 'Completed', signers: 2, signed: 2 },
|
||||||
|
{ id: 2, name: 'NDA Agreement - Tech Corp.pdf', uploadedBy: 'Legal Team', date: '2024-12-18', status: 'Pending', signers: 3, signed: 1 },
|
||||||
|
{ id: 3, name: 'Service Agreement - Client A.docx', uploadedBy: 'Sales Team', date: '2024-12-19', status: 'Pending', signers: 2, signed: 0 },
|
||||||
|
{ id: 4, name: 'Partnership Agreement.pdf', uploadedBy: 'Admin', date: '2024-12-10', status: 'Completed', signers: 4, signed: 4 },
|
||||||
|
{ id: 5, name: 'Lease Agreement - Office Space.pdf', uploadedBy: 'Finance', date: '2024-12-20', status: 'Draft', signers: 0, signed: 0 },
|
||||||
|
{ id: 6, name: 'Consulting Agreement.pdf', uploadedBy: 'HR Team', date: '2024-12-05', status: 'Expired', signers: 2, signed: 1 }
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function SignDocumentsPage() {
|
||||||
|
const { showToast } = useToast();
|
||||||
|
const [statusFilter, setStatusFilter] = useState('All');
|
||||||
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
const [showViewModal, setShowViewModal] = useState(false);
|
||||||
|
const [showSignModal, setShowSignModal] = useState(false);
|
||||||
|
const [selectedDocument, setSelectedDocument] = useState<typeof MOCK_DOCUMENTS[0] | null>(null);
|
||||||
|
const [signatureMode, setSignatureMode] = useState<'draw' | 'type' | 'upload'>('draw');
|
||||||
|
const [signature, setSignature] = useState('');
|
||||||
|
|
||||||
|
const filteredDocuments = MOCK_DOCUMENTS.filter(doc => {
|
||||||
|
const matchesStatus = statusFilter === 'All' || doc.status === statusFilter;
|
||||||
|
const matchesSearch = doc.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
|
doc.uploadedBy.toLowerCase().includes(searchQuery.toLowerCase());
|
||||||
|
return matchesStatus && matchesSearch;
|
||||||
|
});
|
||||||
|
|
||||||
|
const getStatusBadge = (status: string) => {
|
||||||
|
const statusMap: { [key: string]: string } = {
|
||||||
|
'Draft': styles.statusDraft,
|
||||||
|
'Pending': styles.statusPending,
|
||||||
|
'Completed': styles.statusCompleted,
|
||||||
|
'Expired': styles.statusExpired
|
||||||
|
};
|
||||||
|
return `${hamStyles.badge} ${statusMap[status] || ''}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleView = (doc: typeof MOCK_DOCUMENTS[0]) => {
|
||||||
|
setSelectedDocument(doc);
|
||||||
|
setShowViewModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSign = (doc: typeof MOCK_DOCUMENTS[0]) => {
|
||||||
|
setSelectedDocument(doc);
|
||||||
|
setShowSignModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCompleteSignature = () => {
|
||||||
|
if (!signature) {
|
||||||
|
showToast('Please provide your signature', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
showToast('✅ Document signed successfully!', 'success');
|
||||||
|
setShowSignModal(false);
|
||||||
|
setSignature('');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={hamStyles.hamContainer}>
|
||||||
|
{/* View Document Modal */}
|
||||||
|
{showViewModal && selectedDocument && (
|
||||||
|
<div style={{
|
||||||
|
position: 'fixed',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
background: 'rgba(0,0,0,0.9)',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
zIndex: 9999,
|
||||||
|
padding: '2rem'
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
|
background: 'linear-gradient(135deg, rgba(15,20,30,0.98) 0%, rgba(25,30,40,0.98) 100%)',
|
||||||
|
border: '1px solid rgba(255,215,0,0.2)',
|
||||||
|
borderRadius: '16px',
|
||||||
|
maxWidth: '900px',
|
||||||
|
width: '100%',
|
||||||
|
maxHeight: '90vh',
|
||||||
|
overflow: 'hidden',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column'
|
||||||
|
}}>
|
||||||
|
{/* Header */}
|
||||||
|
<div style={{
|
||||||
|
padding: '1.5rem 2rem',
|
||||||
|
borderBottom: '1px solid rgba(255,215,0,0.2)',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center'
|
||||||
|
}}>
|
||||||
|
<div>
|
||||||
|
<h3 style={{ color: '#ffd700', margin: 0, fontSize: '1.3rem' }}>
|
||||||
|
📄 {selectedDocument.name}
|
||||||
|
</h3>
|
||||||
|
<p style={{ color: '#888', margin: '0.5rem 0 0 0', fontSize: '0.9rem' }}>
|
||||||
|
Uploaded by {selectedDocument.uploadedBy} on {selectedDocument.date}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowViewModal(false)}
|
||||||
|
style={{
|
||||||
|
background: 'transparent',
|
||||||
|
border: 'none',
|
||||||
|
color: '#fff',
|
||||||
|
fontSize: '2rem',
|
||||||
|
cursor: 'pointer',
|
||||||
|
padding: '0.5rem'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Document Viewer */}
|
||||||
|
<div style={{
|
||||||
|
flex: 1,
|
||||||
|
overflow: 'auto',
|
||||||
|
padding: '2rem',
|
||||||
|
background: '#f5f5f5'
|
||||||
|
}}>
|
||||||
|
<div className={styles.documentViewer}>
|
||||||
|
<div style={{ textAlign: 'center', padding: '4rem 2rem' }}>
|
||||||
|
<div style={{ fontSize: '4rem', marginBottom: '1rem' }}>
|
||||||
|
{selectedDocument.name.endsWith('.pdf') ? '📕' : '📘'}
|
||||||
|
</div>
|
||||||
|
<h4 style={{ color: '#666', marginBottom: '1rem' }}>Document Preview</h4>
|
||||||
|
<p style={{ color: '#999' }}>
|
||||||
|
{selectedDocument.name}
|
||||||
|
</p>
|
||||||
|
<div style={{ marginTop: '2rem', display: 'flex', gap: '1rem', justifyContent: 'center' }}>
|
||||||
|
<button className={hamStyles.btnSecondary}>
|
||||||
|
📥 Download
|
||||||
|
</button>
|
||||||
|
<button className={hamStyles.btnSecondary}>
|
||||||
|
🖨️ Print
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<div style={{
|
||||||
|
padding: '1.5rem 2rem',
|
||||||
|
borderTop: '1px solid rgba(255,255,255,0.1)',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'flex-end'
|
||||||
|
}}>
|
||||||
|
<button
|
||||||
|
className={hamStyles.btnSecondary}
|
||||||
|
onClick={() => setShowViewModal(false)}
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Sign Document Modal */}
|
||||||
|
{showSignModal && selectedDocument && (
|
||||||
|
<div style={{
|
||||||
|
position: 'fixed',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
background: 'rgba(0,0,0,0.9)',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
zIndex: 9999,
|
||||||
|
padding: '2rem'
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
|
background: 'linear-gradient(135deg, rgba(15,20,30,0.98) 0%, rgba(25,30,40,0.98) 100%)',
|
||||||
|
border: '1px solid rgba(255,215,0,0.2)',
|
||||||
|
borderRadius: '16px',
|
||||||
|
maxWidth: '700px',
|
||||||
|
width: '100%',
|
||||||
|
overflow: 'hidden'
|
||||||
|
}}>
|
||||||
|
{/* Header */}
|
||||||
|
<div style={{
|
||||||
|
padding: '1.5rem 2rem',
|
||||||
|
borderBottom: '1px solid rgba(255,215,0,0.2)'
|
||||||
|
}}>
|
||||||
|
<h3 style={{ color: '#ffd700', margin: 0, fontSize: '1.3rem' }}>
|
||||||
|
✍️ Sign Document
|
||||||
|
</h3>
|
||||||
|
<p style={{ color: '#888', margin: '0.5rem 0 0 0', fontSize: '0.9rem' }}>
|
||||||
|
{selectedDocument.name}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Body */}
|
||||||
|
<div style={{ padding: '2rem' }}>
|
||||||
|
{/* Signature Mode Selector */}
|
||||||
|
<div className={styles.signatureTools}>
|
||||||
|
<button
|
||||||
|
className={`${styles.signatureTool} ${signatureMode === 'draw' ? styles.active : ''}`}
|
||||||
|
onClick={() => setSignatureMode('draw')}
|
||||||
|
>
|
||||||
|
✏️ Draw
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={`${styles.signatureTool} ${signatureMode === 'type' ? styles.active : ''}`}
|
||||||
|
onClick={() => setSignatureMode('type')}
|
||||||
|
>
|
||||||
|
⌨️ Type
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={`${styles.signatureTool} ${signatureMode === 'upload' ? styles.active : ''}`}
|
||||||
|
onClick={() => setSignatureMode('upload')}
|
||||||
|
>
|
||||||
|
📤 Upload
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Signature Input */}
|
||||||
|
{signatureMode === 'draw' && (
|
||||||
|
<div>
|
||||||
|
<canvas
|
||||||
|
className={styles.signatureCanvas}
|
||||||
|
width={600}
|
||||||
|
height={200}
|
||||||
|
style={{ width: '100%', height: '200px' }}
|
||||||
|
onMouseDown={() => setSignature('drawn')}
|
||||||
|
/>
|
||||||
|
<div style={{ marginTop: '1rem', display: 'flex', gap: '1rem' }}>
|
||||||
|
<button
|
||||||
|
className={hamStyles.btnSecondary}
|
||||||
|
onClick={() => setSignature('')}
|
||||||
|
style={{ fontSize: '0.9rem', padding: '0.5rem 1rem' }}
|
||||||
|
>
|
||||||
|
Clear
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{signatureMode === 'type' && (
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className={hamStyles.inputField}
|
||||||
|
placeholder="Type your full name"
|
||||||
|
value={signature}
|
||||||
|
onChange={(e) => setSignature(e.target.value)}
|
||||||
|
style={{ fontSize: '2rem', fontFamily: 'cursive', textAlign: 'center' }}
|
||||||
|
/>
|
||||||
|
{signature && (
|
||||||
|
<div style={{
|
||||||
|
marginTop: '1rem',
|
||||||
|
padding: '2rem',
|
||||||
|
background: 'rgba(255,255,255,0.05)',
|
||||||
|
border: '1px solid rgba(255,215,0,0.2)',
|
||||||
|
borderRadius: '8px',
|
||||||
|
textAlign: 'center'
|
||||||
|
}}>
|
||||||
|
<p style={{ fontSize: '2.5rem', fontFamily: 'cursive', color: '#ffd700', margin: 0 }}>
|
||||||
|
{signature}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{signatureMode === 'upload' && (
|
||||||
|
<div className={styles.fileUploadZone}>
|
||||||
|
<div className={styles.uploadIcon}>📤</div>
|
||||||
|
<p style={{ color: '#888', marginBottom: '0.5rem' }}>
|
||||||
|
Upload your signature image
|
||||||
|
</p>
|
||||||
|
<p style={{ color: '#666', fontSize: '0.85rem' }}>
|
||||||
|
PNG, JPG up to 5MB
|
||||||
|
</p>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
onChange={(e) => {
|
||||||
|
if (e.target.files?.[0]) {
|
||||||
|
setSignature(e.target.files[0].name);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
style={{ marginTop: '1rem' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<div style={{
|
||||||
|
padding: '1.5rem 2rem',
|
||||||
|
borderTop: '1px solid rgba(255,255,255,0.1)',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
gap: '1rem'
|
||||||
|
}}>
|
||||||
|
<button
|
||||||
|
className={hamStyles.btnSecondary}
|
||||||
|
onClick={() => {
|
||||||
|
setShowSignModal(false);
|
||||||
|
setSignature('');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={hamStyles.btnPrimary}
|
||||||
|
onClick={handleCompleteSignature}
|
||||||
|
>
|
||||||
|
Complete Signature
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={hamStyles.sectionHeader}>
|
||||||
|
<div>
|
||||||
|
<h1 className={hamStyles.pageTitle}>Sign Documents</h1>
|
||||||
|
<p className={hamStyles.pageSubtitle}>Manage document signatures and requests</p>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', gap: '1rem' }}>
|
||||||
|
<Link href="/dashboard/documents/upload">
|
||||||
|
<button className={hamStyles.btnSecondary}>📤 Upload Document</button>
|
||||||
|
</Link>
|
||||||
|
<Link href="/dashboard/documents/sign/request">
|
||||||
|
<button className={hamStyles.btnPrimary}>✍️ Request Signature</button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Filters */}
|
||||||
|
<div className={hamStyles.filterBar}>
|
||||||
|
<div className={hamStyles.filterGroup}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className={hamStyles.inputField}
|
||||||
|
placeholder="Search documents..."
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
style={{ minWidth: '300px' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={hamStyles.filterGroup}>
|
||||||
|
<label className={hamStyles.filterLabel}>Status</label>
|
||||||
|
<select
|
||||||
|
className={hamStyles.filterSelect}
|
||||||
|
value={statusFilter}
|
||||||
|
onChange={(e) => setStatusFilter(e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="All">All Documents</option>
|
||||||
|
<option value="Draft">Drafts</option>
|
||||||
|
<option value="Pending">Pending Signature</option>
|
||||||
|
<option value="Completed">Completed</option>
|
||||||
|
<option value="Expired">Expired</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Stats */}
|
||||||
|
<div className={hamStyles.widgetsGrid}>
|
||||||
|
<div className={hamStyles.widget}>
|
||||||
|
<div className={hamStyles.widgetIcon}>⏳</div>
|
||||||
|
<div className={hamStyles.widgetValue}>{MOCK_DOCUMENTS.filter(d => d.status === 'Pending').length}</div>
|
||||||
|
<div className={hamStyles.widgetLabel}>Waiting for Signature</div>
|
||||||
|
</div>
|
||||||
|
<div className={hamStyles.widget}>
|
||||||
|
<div className={hamStyles.widgetIcon}>✅</div>
|
||||||
|
<div className={hamStyles.widgetValue}>{MOCK_DOCUMENTS.filter(d => d.status === 'Completed').length}</div>
|
||||||
|
<div className={hamStyles.widgetLabel}>Completed</div>
|
||||||
|
</div>
|
||||||
|
<div className={hamStyles.widget}>
|
||||||
|
<div className={hamStyles.widgetIcon}>📝</div>
|
||||||
|
<div className={hamStyles.widgetValue}>{MOCK_DOCUMENTS.filter(d => d.status === 'Draft').length}</div>
|
||||||
|
<div className={hamStyles.widgetLabel}>Drafts</div>
|
||||||
|
</div>
|
||||||
|
<div className={hamStyles.widget}>
|
||||||
|
<div className={hamStyles.widgetIcon}>⚠️</div>
|
||||||
|
<div className={hamStyles.widgetValue}>{MOCK_DOCUMENTS.filter(d => d.status === 'Expired').length}</div>
|
||||||
|
<div className={hamStyles.widgetLabel}>Expired</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Documents Table */}
|
||||||
|
<div className={hamStyles.section}>
|
||||||
|
<h3 style={{ color: '#ffd700', marginBottom: '1.5rem' }}>Documents</h3>
|
||||||
|
{filteredDocuments.length === 0 ? (
|
||||||
|
<div className={hamStyles.emptyState}>
|
||||||
|
<div style={{ fontSize: '3rem', marginBottom: '1rem' }}>📄</div>
|
||||||
|
<p>No documents found</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<table className={hamStyles.table}>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Document Name</th>
|
||||||
|
<th>Uploaded By</th>
|
||||||
|
<th>Date</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Progress</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{filteredDocuments.map(doc => (
|
||||||
|
<tr key={doc.id}>
|
||||||
|
<td style={{ fontWeight: 600 }}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
|
||||||
|
<span>{doc.name.endsWith('.pdf') ? '📕' : '📘'}</span>
|
||||||
|
{doc.name}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>{doc.uploadedBy}</td>
|
||||||
|
<td>{doc.date}</td>
|
||||||
|
<td>
|
||||||
|
<span className={getStatusBadge(doc.status)}>{doc.status}</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
|
||||||
|
<div style={{
|
||||||
|
flex: 1,
|
||||||
|
height: '8px',
|
||||||
|
background: 'rgba(255,255,255,0.1)',
|
||||||
|
borderRadius: '4px',
|
||||||
|
overflow: 'hidden'
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
|
width: `${doc.signers > 0 ? (doc.signed / doc.signers) * 100 : 0}%`,
|
||||||
|
height: '100%',
|
||||||
|
background: 'linear-gradient(90deg, #ffd700 0%, #ffed4e 100%)',
|
||||||
|
transition: 'width 0.3s ease'
|
||||||
|
}}></div>
|
||||||
|
</div>
|
||||||
|
<span style={{ fontSize: '0.85rem', color: '#888' }}>
|
||||||
|
{doc.signed}/{doc.signers}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div style={{ display: 'flex', gap: '0.5rem' }}>
|
||||||
|
<button
|
||||||
|
className={hamStyles.btnSecondary}
|
||||||
|
style={{ padding: '0.4rem 0.8rem', fontSize: '0.8rem' }}
|
||||||
|
onClick={() => handleView(doc)}
|
||||||
|
>
|
||||||
|
View
|
||||||
|
</button>
|
||||||
|
{doc.status === 'Pending' && (
|
||||||
|
<button
|
||||||
|
className={hamStyles.btnPrimary}
|
||||||
|
style={{ padding: '0.4rem 0.8rem', fontSize: '0.8rem' }}
|
||||||
|
onClick={() => handleSign(doc)}
|
||||||
|
>
|
||||||
|
Sign
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{doc.status === 'Draft' && (
|
||||||
|
<Link href={`/dashboard/documents/sign/request?doc=${doc.id}`}>
|
||||||
|
<button
|
||||||
|
className={hamStyles.btnPrimary}
|
||||||
|
style={{ padding: '0.4rem 0.8rem', fontSize: '0.8rem' }}
|
||||||
|
>
|
||||||
|
Request
|
||||||
|
</button>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
363
src/app/dashboard/documents/sign/request/page.tsx
Normal file
@@ -0,0 +1,363 @@
|
|||||||
|
'use client';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import styles from '../../legal.module.css';
|
||||||
|
import hamStyles from '../../../ham/ham.module.css';
|
||||||
|
import appointmentStyles from '../../../appointments/appointments.module.css';
|
||||||
|
import { useToast } from '@/context/ToastContext';
|
||||||
|
|
||||||
|
export default function RequestSignaturePage() {
|
||||||
|
const router = useRouter();
|
||||||
|
const { showToast } = useToast();
|
||||||
|
const [currentStep, setCurrentStep] = useState(0);
|
||||||
|
const [signers, setSigners] = useState<Array<{ name: string; email: string; order: number }>>([]);
|
||||||
|
const [newSigner, setNewSigner] = useState({ name: '', email: '' });
|
||||||
|
const [signatureFields, setSignatureFields] = useState<Array<{ type: string; x: number; y: number; width: number; height: number; signerId: number }>>([]);
|
||||||
|
const [message, setMessage] = useState('');
|
||||||
|
const [showSuccess, setShowSuccess] = useState(false);
|
||||||
|
const [shareLink, setShareLink] = useState('');
|
||||||
|
|
||||||
|
const steps = ['Add Signers', 'Place Fields', 'Review & Send'];
|
||||||
|
|
||||||
|
const handleAddSigner = () => {
|
||||||
|
if (!newSigner.name || !newSigner.email) {
|
||||||
|
showToast('Please enter name and email', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||||
|
if (!emailRegex.test(newSigner.email)) {
|
||||||
|
showToast('Please enter a valid email address', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (signers.some(s => s.email === newSigner.email)) {
|
||||||
|
showToast('This email is already added', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSigners([...signers, { ...newSigner, order: signers.length + 1 }]);
|
||||||
|
setNewSigner({ name: '', email: '' });
|
||||||
|
showToast('Signer added successfully', 'success');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveSigner = (index: number) => {
|
||||||
|
setSigners(signers.filter((_, i) => i !== index));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNext = () => {
|
||||||
|
if (currentStep === 0 && signers.length === 0) {
|
||||||
|
showToast('Please add at least one signer', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (currentStep === 1 && signatureFields.length === 0) {
|
||||||
|
showToast('Please place at least one signature field', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setCurrentStep(currentStep + 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBack = () => {
|
||||||
|
setCurrentStep(currentStep - 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSend = () => {
|
||||||
|
const link = `${window.location.origin}/sign/${Math.random().toString(36).substring(7)}`;
|
||||||
|
setShareLink(link);
|
||||||
|
setShowSuccess(true);
|
||||||
|
showToast('✅ Signature request sent successfully!', 'success');
|
||||||
|
};
|
||||||
|
|
||||||
|
const addSignatureField = (type: string) => {
|
||||||
|
setSignatureFields([...signatureFields, {
|
||||||
|
type,
|
||||||
|
x: 100,
|
||||||
|
y: 100 + (signatureFields.length * 60),
|
||||||
|
width: type === 'signature' ? 200 : type === 'date' ? 120 : 150,
|
||||||
|
height: 50,
|
||||||
|
signerId: 0
|
||||||
|
}]);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (showSuccess) {
|
||||||
|
return (
|
||||||
|
<div className={hamStyles.hamContainer}>
|
||||||
|
<div className={appointmentStyles.confirmationScreen}>
|
||||||
|
<div className={appointmentStyles.confirmationIcon}>✅</div>
|
||||||
|
<h1 className={appointmentStyles.confirmationTitle}>Signature Request Sent!</h1>
|
||||||
|
|
||||||
|
<div style={{ margin: '2rem 0', padding: '1.5rem', background: 'rgba(255,215,0,0.1)', border: '1px solid rgba(255,215,0,0.3)', borderRadius: '8px' }}>
|
||||||
|
<p style={{ color: '#888', marginBottom: '1rem' }}>Share this link with signers:</p>
|
||||||
|
<div style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={shareLink}
|
||||||
|
readOnly
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
background: 'rgba(0,0,0,0.3)',
|
||||||
|
border: '1px solid rgba(255,255,255,0.1)',
|
||||||
|
borderRadius: '6px',
|
||||||
|
padding: '0.75rem',
|
||||||
|
color: '#ffd700',
|
||||||
|
fontSize: '0.9rem'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className={hamStyles.btnPrimary}
|
||||||
|
onClick={() => {
|
||||||
|
navigator.clipboard.writeText(shareLink);
|
||||||
|
showToast('Link copied to clipboard!', 'success');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
📋 Copy
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={appointmentStyles.confirmationDetails}>
|
||||||
|
<div className={appointmentStyles.confirmationRow}>
|
||||||
|
<span className={appointmentStyles.confirmationLabel}>Total Signers:</span>
|
||||||
|
<span className={appointmentStyles.confirmationValue}>{signers.length}</span>
|
||||||
|
</div>
|
||||||
|
<div className={appointmentStyles.confirmationRow}>
|
||||||
|
<span className={appointmentStyles.confirmationLabel}>Signature Fields:</span>
|
||||||
|
<span className={appointmentStyles.confirmationValue}>{signatureFields.length}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', gap: '1rem', marginTop: '2rem' }}>
|
||||||
|
<button
|
||||||
|
className={hamStyles.btnSecondary}
|
||||||
|
onClick={() => router.push('/dashboard/documents/sign')}
|
||||||
|
>
|
||||||
|
Back to Documents
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={hamStyles.btnPrimary}
|
||||||
|
onClick={() => {
|
||||||
|
navigator.clipboard.writeText(shareLink);
|
||||||
|
showToast('Link copied!', 'success');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Copy Link Again
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={hamStyles.hamContainer}>
|
||||||
|
<div className={hamStyles.sectionHeader}>
|
||||||
|
<div>
|
||||||
|
<h1 className={hamStyles.pageTitle}>Request Signature</h1>
|
||||||
|
<p className={hamStyles.pageSubtitle}>Create a signature request workflow</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Wizard Steps */}
|
||||||
|
<div className={appointmentStyles.wizard}>
|
||||||
|
{steps.map((step, index) => (
|
||||||
|
<div key={index} className={`${appointmentStyles.wizardStep} ${index === currentStep ? appointmentStyles.active : ''} ${index < currentStep ? appointmentStyles.completed : ''}`}>
|
||||||
|
<div className={appointmentStyles.wizardStepCircle}>
|
||||||
|
{index < currentStep ? '✓' : index + 1}
|
||||||
|
</div>
|
||||||
|
<div className={appointmentStyles.wizardStepLabel}>{step}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Step Content */}
|
||||||
|
<div className={hamStyles.section}>
|
||||||
|
{/* Step 0: Add Signers */}
|
||||||
|
{currentStep === 0 && (
|
||||||
|
<div>
|
||||||
|
<h3 style={{ color: '#ffd700', marginBottom: '1.5rem' }}>Add Signers</h3>
|
||||||
|
|
||||||
|
{/* Add Signer Form */}
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr auto', gap: '1rem', marginBottom: '2rem', maxWidth: '800px' }}>
|
||||||
|
<div>
|
||||||
|
<label className={hamStyles.filterLabel}>Name *</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className={hamStyles.inputField}
|
||||||
|
value={newSigner.name}
|
||||||
|
onChange={(e) => setNewSigner({ ...newSigner, name: e.target.value })}
|
||||||
|
placeholder="John Doe"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={hamStyles.filterLabel}>Email *</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
className={hamStyles.inputField}
|
||||||
|
value={newSigner.email}
|
||||||
|
onChange={(e) => setNewSigner({ ...newSigner, email: e.target.value })}
|
||||||
|
placeholder="john@example.com"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'flex-end' }}>
|
||||||
|
<button className={hamStyles.btnPrimary} onClick={handleAddSigner}>
|
||||||
|
+ Add
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Signers List */}
|
||||||
|
{signers.length > 0 && (
|
||||||
|
<div className={styles.signerList}>
|
||||||
|
{signers.map((signer, index) => (
|
||||||
|
<div key={index} className={styles.signerItem}>
|
||||||
|
<div className={styles.signerNumber}>{signer.order}</div>
|
||||||
|
<div className={styles.signerDetails}>
|
||||||
|
<div className={styles.signerName}>{signer.name}</div>
|
||||||
|
<div className={styles.signerEmail}>{signer.email}</div>
|
||||||
|
</div>
|
||||||
|
<button className={styles.removeSigner} onClick={() => handleRemoveSigner(index)}>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{signers.length === 0 && (
|
||||||
|
<div className={hamStyles.emptyState}>
|
||||||
|
<div style={{ fontSize: '3rem', marginBottom: '1rem' }}>✍️</div>
|
||||||
|
<p>No signers added yet</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Step 1: Place Signature Fields */}
|
||||||
|
{currentStep === 1 && (
|
||||||
|
<div>
|
||||||
|
<h3 style={{ color: '#ffd700', marginBottom: '1.5rem' }}>Place Signature Fields</h3>
|
||||||
|
|
||||||
|
<div className={styles.signatureTools}>
|
||||||
|
<button className={styles.signatureTool} onClick={() => addSignatureField('signature')}>
|
||||||
|
✍️ Signature
|
||||||
|
</button>
|
||||||
|
<button className={styles.signatureTool} onClick={() => addSignatureField('date')}>
|
||||||
|
📅 Date
|
||||||
|
</button>
|
||||||
|
<button className={styles.signatureTool} onClick={() => addSignatureField('initials')}>
|
||||||
|
🔤 Initials
|
||||||
|
</button>
|
||||||
|
<button className={styles.signatureTool} onClick={() => addSignatureField('text')}>
|
||||||
|
📝 Text
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.documentViewer}>
|
||||||
|
<div style={{ position: 'relative', minHeight: '600px', background: '#f5f5f5', borderRadius: '8px', padding: '2rem' }}>
|
||||||
|
<div style={{ textAlign: 'center', color: '#666', marginBottom: '2rem' }}>
|
||||||
|
<p style={{ fontSize: '1.2rem', marginBottom: '0.5rem' }}>📄 Document Preview</p>
|
||||||
|
<p style={{ fontSize: '0.9rem' }}>Click the buttons above to add signature fields</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{signatureFields.map((field, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className={styles.signatureField}
|
||||||
|
style={{
|
||||||
|
left: `${field.x}px`,
|
||||||
|
top: `${field.y}px`,
|
||||||
|
width: `${field.width}px`,
|
||||||
|
height: `${field.height}px`
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{field.type === 'signature' && '✍️ Signature'}
|
||||||
|
{field.type === 'date' && '📅 Date'}
|
||||||
|
{field.type === 'initials' && '🔤 Initials'}
|
||||||
|
{field.type === 'text' && '📝 Text'}
|
||||||
|
<div className={styles.fieldHandle}></div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Step 2: Review & Send */}
|
||||||
|
{currentStep === 2 && (
|
||||||
|
<div>
|
||||||
|
<h3 style={{ color: '#ffd700', marginBottom: '1.5rem' }}>Review & Send</h3>
|
||||||
|
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '2rem' }}>
|
||||||
|
<div>
|
||||||
|
<h4 style={{ color: '#fff', marginBottom: '1rem' }}>Signers ({signers.length})</h4>
|
||||||
|
<div className={styles.signerList}>
|
||||||
|
{signers.map((signer, index) => (
|
||||||
|
<div key={index} className={styles.signerItem}>
|
||||||
|
<div className={styles.signerNumber}>{signer.order}</div>
|
||||||
|
<div className={styles.signerDetails}>
|
||||||
|
<div className={styles.signerName}>{signer.name}</div>
|
||||||
|
<div className={styles.signerEmail}>{signer.email}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h4 style={{ color: '#fff', marginBottom: '1rem' }}>Signature Fields ({signatureFields.length})</h4>
|
||||||
|
<div style={{ background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '8px', padding: '1rem' }}>
|
||||||
|
{signatureFields.map((field, index) => (
|
||||||
|
<div key={index} style={{ padding: '0.5rem 0', borderBottom: index < signatureFields.length - 1 ? '1px solid rgba(255,255,255,0.1)' : 'none' }}>
|
||||||
|
<span style={{ color: '#ffd700' }}>
|
||||||
|
{field.type === 'signature' && '✍️ Signature'}
|
||||||
|
{field.type === 'date' && '📅 Date'}
|
||||||
|
{field.type === 'initials' && '🔤 Initials'}
|
||||||
|
{field.type === 'text' && '📝 Text'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ marginTop: '2rem' }}>
|
||||||
|
<label className={hamStyles.filterLabel}>Message to Signers (Optional)</label>
|
||||||
|
<textarea
|
||||||
|
className={hamStyles.inputField}
|
||||||
|
value={message}
|
||||||
|
onChange={(e) => setMessage(e.target.value)}
|
||||||
|
placeholder="Add a message for the signers..."
|
||||||
|
rows={4}
|
||||||
|
style={{ resize: 'vertical' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Navigation Buttons */}
|
||||||
|
<div style={{ display: 'flex', gap: '1rem', justifyContent: 'flex-end', marginTop: '2rem' }}>
|
||||||
|
{currentStep > 0 && (
|
||||||
|
<button className={hamStyles.btnSecondary} onClick={handleBack}>
|
||||||
|
← Back
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{currentStep < 2 && (
|
||||||
|
<button className={hamStyles.btnPrimary} onClick={handleNext}>
|
||||||
|
Next →
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{currentStep === 2 && (
|
||||||
|
<>
|
||||||
|
<button className={hamStyles.btnSecondary} onClick={() => showToast('Saved as draft', 'success')}>
|
||||||
|
Save as Draft
|
||||||
|
</button>
|
||||||
|
<button className={hamStyles.btnPrimary} onClick={handleSend}>
|
||||||
|
Send for Signature
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
148
src/app/dashboard/documents/templates/page.tsx
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
'use client';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import styles from '../legal.module.css';
|
||||||
|
import hamStyles from '../../ham/ham.module.css';
|
||||||
|
import { useToast } from '@/context/ToastContext';
|
||||||
|
|
||||||
|
const MOCK_TEMPLATES = [
|
||||||
|
{ id: 1, name: 'Employment Contract Template', category: 'HR', fields: 8, lastUsed: '2024-12-18', uses: 45 },
|
||||||
|
{ id: 2, name: 'NDA Agreement', category: 'Legal', fields: 5, lastUsed: '2024-12-20', uses: 120 },
|
||||||
|
{ id: 3, name: 'Service Agreement', category: 'Sales', fields: 12, lastUsed: '2024-12-15', uses: 67 },
|
||||||
|
{ id: 4, name: 'Consulting Agreement', category: 'HR', fields: 10, lastUsed: '2024-12-10', uses: 34 },
|
||||||
|
{ id: 5, name: 'Partnership Agreement', category: 'Legal', fields: 15, lastUsed: '2024-12-19', uses: 23 }
|
||||||
|
];
|
||||||
|
|
||||||
|
const CATEGORIES = ['All', 'HR', 'Legal', 'Sales'];
|
||||||
|
|
||||||
|
export default function TemplatesPage() {
|
||||||
|
const { showToast } = useToast();
|
||||||
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
const [categoryFilter, setCategoryFilter] = useState('All');
|
||||||
|
|
||||||
|
const filteredTemplates = MOCK_TEMPLATES.filter(template => {
|
||||||
|
const matchesSearch = template.name.toLowerCase().includes(searchQuery.toLowerCase());
|
||||||
|
const matchesCategory = categoryFilter === 'All' || template.category === categoryFilter;
|
||||||
|
return matchesSearch && matchesCategory;
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={hamStyles.hamContainer}>
|
||||||
|
<div className={hamStyles.sectionHeader}>
|
||||||
|
<div>
|
||||||
|
<h1 className={hamStyles.pageTitle}>Document Templates</h1>
|
||||||
|
<p className={hamStyles.pageSubtitle}>Reusable document templates with pre-configured signature fields</p>
|
||||||
|
</div>
|
||||||
|
<button className={hamStyles.btnPrimary} onClick={() => showToast('Create template feature coming soon', 'info')}>
|
||||||
|
+ Create Template
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Filters */}
|
||||||
|
<div className={hamStyles.filterBar}>
|
||||||
|
<div className={hamStyles.filterGroup}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className={hamStyles.inputField}
|
||||||
|
placeholder="Search templates..."
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
style={{ minWidth: '300px' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={hamStyles.filterGroup}>
|
||||||
|
<label className={hamStyles.filterLabel}>Category</label>
|
||||||
|
<select
|
||||||
|
className={hamStyles.filterSelect}
|
||||||
|
value={categoryFilter}
|
||||||
|
onChange={(e) => setCategoryFilter(e.target.value)}
|
||||||
|
>
|
||||||
|
{CATEGORIES.map(cat => (
|
||||||
|
<option key={cat} value={cat}>{cat}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Stats */}
|
||||||
|
<div className={hamStyles.widgetsGrid}>
|
||||||
|
<div className={hamStyles.widget}>
|
||||||
|
<div className={hamStyles.widgetIcon}>📋</div>
|
||||||
|
<div className={hamStyles.widgetValue}>{MOCK_TEMPLATES.length}</div>
|
||||||
|
<div className={hamStyles.widgetLabel}>Total Templates</div>
|
||||||
|
</div>
|
||||||
|
<div className={hamStyles.widget}>
|
||||||
|
<div className={hamStyles.widgetIcon}>🔥</div>
|
||||||
|
<div className={hamStyles.widgetValue}>{MOCK_TEMPLATES.reduce((sum, t) => sum + t.uses, 0)}</div>
|
||||||
|
<div className={hamStyles.widgetLabel}>Total Uses</div>
|
||||||
|
</div>
|
||||||
|
<div className={hamStyles.widget}>
|
||||||
|
<div className={hamStyles.widgetIcon}>⭐</div>
|
||||||
|
<div className={hamStyles.widgetValue}>{MOCK_TEMPLATES.sort((a, b) => b.uses - a.uses)[0]?.name.split(' ')[0]}</div>
|
||||||
|
<div className={hamStyles.widgetLabel}>Most Popular</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Templates Grid */}
|
||||||
|
<div className={hamStyles.section}>
|
||||||
|
<h3 style={{ color: '#ffd700', marginBottom: '1.5rem' }}>Templates</h3>
|
||||||
|
{filteredTemplates.length === 0 ? (
|
||||||
|
<div className={hamStyles.emptyState}>
|
||||||
|
<div style={{ fontSize: '3rem', marginBottom: '1rem' }}>📋</div>
|
||||||
|
<p>No templates found</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(350px, 1fr))', gap: '1.5rem' }}>
|
||||||
|
{filteredTemplates.map(template => (
|
||||||
|
<div key={template.id} className={styles.consultationCard}>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'start', marginBottom: '1rem' }}>
|
||||||
|
<div>
|
||||||
|
<h4 style={{ color: '#fff', margin: '0 0 0.5rem 0', fontSize: '1.1rem' }}>
|
||||||
|
{template.name}
|
||||||
|
</h4>
|
||||||
|
<span className={hamStyles.badge} style={{ background: 'rgba(255,215,0,0.2)', color: '#ffd700' }}>
|
||||||
|
{template.category}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: '2rem' }}>📄</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem', marginBottom: '1rem', padding: '1rem 0', borderTop: '1px solid rgba(255,255,255,0.1)', borderBottom: '1px solid rgba(255,255,255,0.1)' }}>
|
||||||
|
<div>
|
||||||
|
<div style={{ color: '#888', fontSize: '0.85rem', marginBottom: '0.25rem' }}>Signature Fields</div>
|
||||||
|
<div style={{ color: '#fff', fontWeight: 600 }}>{template.fields}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div style={{ color: '#888', fontSize: '0.85rem', marginBottom: '0.25rem' }}>Times Used</div>
|
||||||
|
<div style={{ color: '#ffd700', fontWeight: 600 }}>{template.uses}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ color: '#888', fontSize: '0.85rem', marginBottom: '1rem' }}>
|
||||||
|
Last used: {template.lastUsed}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', gap: '0.5rem' }}>
|
||||||
|
<button
|
||||||
|
className={hamStyles.btnPrimary}
|
||||||
|
style={{ flex: 1, padding: '0.6rem' }}
|
||||||
|
onClick={() => showToast('Use template', 'info')}
|
||||||
|
>
|
||||||
|
Use Template
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={hamStyles.btnSecondary}
|
||||||
|
style={{ padding: '0.6rem 1rem' }}
|
||||||
|
onClick={() => showToast('Edit template', 'info')}
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
175
src/app/dashboard/documents/upload/page.tsx
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import styles from '../dms.module.css';
|
||||||
|
|
||||||
|
export default function UploadPage() {
|
||||||
|
const [dragActive, setDragActive] = useState(false);
|
||||||
|
const [fileStats, setFileStats] = useState<{ name: string, size: string } | null>(null);
|
||||||
|
const [isUploading, setIsUploading] = useState(false);
|
||||||
|
const [uploadComplete, setUploadComplete] = useState(false);
|
||||||
|
|
||||||
|
const handleDrag = (e: React.DragEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
if (e.type === "dragenter" || e.type === "dragover") {
|
||||||
|
setDragActive(true);
|
||||||
|
} else if (e.type === "dragleave") {
|
||||||
|
setDragActive(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDrop = (e: React.DragEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setDragActive(false);
|
||||||
|
if (e.dataTransfer.files && e.dataTransfer.files[0]) {
|
||||||
|
handleFile(e.dataTransfer.files[0]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (e.target.files && e.target.files[0]) {
|
||||||
|
handleFile(e.target.files[0]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFile = (file: File) => {
|
||||||
|
setFileStats({
|
||||||
|
name: file.name,
|
||||||
|
size: (file.size / 1024 / 1024).toFixed(2) + ' MB'
|
||||||
|
});
|
||||||
|
setUploadComplete(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpload = () => {
|
||||||
|
if (!fileStats) return;
|
||||||
|
setIsUploading(true);
|
||||||
|
// Simulate upload
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsUploading(false);
|
||||||
|
setUploadComplete(true);
|
||||||
|
setFileStats(null);
|
||||||
|
}, 2000);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<div className={styles.header}>
|
||||||
|
<div>
|
||||||
|
<h1>Upload Document</h1>
|
||||||
|
<p className={styles.subtitle}>Add new files to your DMS</p>
|
||||||
|
</div>
|
||||||
|
<div className={styles.actions}>
|
||||||
|
<Link href="/dashboard/documents" className={styles.link} style={{ marginRight: '1rem' }}>
|
||||||
|
Cancel
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.section} style={{ minHeight: '400px', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center' }}>
|
||||||
|
|
||||||
|
{!fileStats && !uploadComplete && (
|
||||||
|
<div
|
||||||
|
onDragEnter={handleDrag}
|
||||||
|
onDragLeave={handleDrag}
|
||||||
|
onDragOver={handleDrag}
|
||||||
|
onDrop={handleDrop}
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
maxWidth: '600px',
|
||||||
|
height: '300px',
|
||||||
|
border: `2px dashed ${dragActive ? '#FFD700' : 'rgba(255,255,255,0.2)'}`,
|
||||||
|
borderRadius: '16px',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
background: dragActive ? 'rgba(255,215,0,0.05)' : 'transparent',
|
||||||
|
transition: 'all 0.2s',
|
||||||
|
position: 'relative'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
id="file-upload"
|
||||||
|
onChange={handleChange}
|
||||||
|
style={{ display: 'none' }}
|
||||||
|
/>
|
||||||
|
<label htmlFor="file-upload" style={{ cursor: 'pointer', textAlign: 'center' }}>
|
||||||
|
<span style={{ fontSize: '3rem', display: 'block', marginBottom: '1rem' }}>☁️</span>
|
||||||
|
<h3 style={{ marginBottom: '0.5rem' }}>Drag & Drop your file here</h3>
|
||||||
|
<p style={{ color: '#888', marginBottom: '1.5rem' }}>or click to browse</p>
|
||||||
|
<span className={styles.btnPrimary}>Browse Files</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{fileStats && !isUploading && !uploadComplete && (
|
||||||
|
<div style={{ textAlign: 'center', animation: 'fadeIn 0.3s' }}>
|
||||||
|
<span style={{ fontSize: '3rem', display: 'block', marginBottom: '1rem' }}>📄</span>
|
||||||
|
<h3 style={{ marginBottom: '0.5rem' }}>{fileStats.name}</h3>
|
||||||
|
<p style={{ color: '#888', marginBottom: '1.5rem' }}>{fileStats.size}</p>
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', gap: '1rem', justifyContent: 'center' }}>
|
||||||
|
<button
|
||||||
|
onClick={() => setFileStats(null)}
|
||||||
|
style={{
|
||||||
|
background: 'transparent',
|
||||||
|
border: '1px solid rgba(255,255,255,0.2)',
|
||||||
|
color: '#fff',
|
||||||
|
padding: '10px 20px',
|
||||||
|
borderRadius: '8px',
|
||||||
|
cursor: 'pointer'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</button>
|
||||||
|
<button onClick={handleUpload} className={styles.btnPrimary}>
|
||||||
|
Start Upload
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isUploading && (
|
||||||
|
<div style={{ textAlign: 'center', width: '100%', maxWidth: '400px' }}>
|
||||||
|
<h3 style={{ marginBottom: '1rem' }}>Uploading...</h3>
|
||||||
|
<div style={{
|
||||||
|
width: '100%',
|
||||||
|
height: '6px',
|
||||||
|
background: 'rgba(255,255,255,0.1)',
|
||||||
|
borderRadius: '3px',
|
||||||
|
overflow: 'hidden'
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
|
height: '100%',
|
||||||
|
background: 'linear-gradient(90deg, #FFD700, #FFAA00)',
|
||||||
|
width: '60%', // Static to mock, ideally animated
|
||||||
|
borderRadius: '3px'
|
||||||
|
}}></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{uploadComplete && (
|
||||||
|
<div style={{ textAlign: 'center' }}>
|
||||||
|
<span style={{ fontSize: '3rem', display: 'block', marginBottom: '1rem' }}>✅</span>
|
||||||
|
<h3 style={{ marginBottom: '0.5rem', color: '#4CAF50' }}>Upload Successful!</h3>
|
||||||
|
<p style={{ color: '#888', marginBottom: '1.5rem' }}>Your file has been processed.</p>
|
||||||
|
<button onClick={() => setUploadComplete(false)} className={styles.btnPrimary}>
|
||||||
|
Upload Another
|
||||||
|
</button>
|
||||||
|
<br />
|
||||||
|
<Link href="/dashboard/documents/all" className={styles.link} style={{ display: 'inline-block', marginTop: '1rem' }}>
|
||||||
|
View All Documents
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
131
src/app/dashboard/ham/analytics/page.tsx
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
'use client';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import styles from '../ham.module.css';
|
||||||
|
|
||||||
|
export default function AnalyticsPage() {
|
||||||
|
const [dateRange, setDateRange] = useState('this-month');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.hamContainer}>
|
||||||
|
<div className={styles.sectionHeader}>
|
||||||
|
<div>
|
||||||
|
<h1 className={styles.pageTitle}>HR Analytics</h1>
|
||||||
|
<p className={styles.pageSubtitle}>Insights and reports</p>
|
||||||
|
</div>
|
||||||
|
<div className={styles.filterGroup}>
|
||||||
|
<select
|
||||||
|
className={styles.filterSelect}
|
||||||
|
value={dateRange}
|
||||||
|
onChange={(e) => setDateRange(e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="this-week">This Week</option>
|
||||||
|
<option value="this-month">This Month</option>
|
||||||
|
<option value="this-quarter">This Quarter</option>
|
||||||
|
<option value="this-year">This Year</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Key Metrics */}
|
||||||
|
<div className={styles.widgetsGrid}>
|
||||||
|
<div className={styles.widget}>
|
||||||
|
<span className={styles.widgetIcon}>📊</span>
|
||||||
|
<div className={styles.widgetValue}>92%</div>
|
||||||
|
<div className={styles.widgetLabel}>Avg Attendance</div>
|
||||||
|
<div className={`${styles.widgetChange} ${styles.positive}`}>↑ 3% from last month</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.widget}>
|
||||||
|
<span className={styles.widgetIcon}>📉</span>
|
||||||
|
<div className={styles.widgetValue}>8%</div>
|
||||||
|
<div className={styles.widgetLabel}>Attrition Rate</div>
|
||||||
|
<div className={`${styles.widgetChange} ${styles.negative}`}>↑ 2% from last quarter</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.widget}>
|
||||||
|
<span className={styles.widgetIcon}>⭐</span>
|
||||||
|
<div className={styles.widgetValue}>4.3</div>
|
||||||
|
<div className={styles.widgetLabel}>Avg Performance</div>
|
||||||
|
<div className={`${styles.widgetChange} ${styles.positive}`}>↑ 0.2 from last quarter</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.widget}>
|
||||||
|
<span className={styles.widgetIcon}>🎯</span>
|
||||||
|
<div className={styles.widgetValue}>78%</div>
|
||||||
|
<div className={styles.widgetLabel}>Goal Completion</div>
|
||||||
|
<div className={`${styles.widgetChange} ${styles.positive}`}>↑ 5% from last quarter</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Charts Placeholder */}
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(400px, 1fr))', gap: '1.5rem' }}>
|
||||||
|
<div className={styles.section}>
|
||||||
|
<h3 style={{ color: '#ffd700', marginBottom: '1rem' }}>📈 Attendance Trend</h3>
|
||||||
|
<div style={{ padding: '4rem 2rem', textAlign: 'center', background: 'rgba(255,255,255,0.02)', borderRadius: '8px' }}>
|
||||||
|
<div style={{ fontSize: '3rem', marginBottom: '1rem' }}>📊</div>
|
||||||
|
<p style={{ color: '#888' }}>Chart visualization coming soon</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.section}>
|
||||||
|
<h3 style={{ color: '#ffd700', marginBottom: '1rem' }}>🎯 Performance Distribution</h3>
|
||||||
|
<div style={{ padding: '4rem 2rem', textAlign: 'center', background: 'rgba(255,255,255,0.02)', borderRadius: '8px' }}>
|
||||||
|
<div style={{ fontSize: '3rem', marginBottom: '1rem' }}>📊</div>
|
||||||
|
<p style={{ color: '#888' }}>Chart visualization coming soon</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.section}>
|
||||||
|
<h3 style={{ color: '#ffd700', marginBottom: '1rem' }}>👥 Department Headcount</h3>
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
|
||||||
|
{[
|
||||||
|
{ dept: 'Engineering', count: 85, color: '#58a6ff' },
|
||||||
|
{ dept: 'Sales', count: 62, color: '#ffd700' },
|
||||||
|
{ dept: 'Marketing', count: 45, color: '#ff9500' },
|
||||||
|
{ dept: 'HR', count: 28, color: '#00ff9d' },
|
||||||
|
{ dept: 'Finance', count: 27, color: '#ff4444' }
|
||||||
|
].map(item => (
|
||||||
|
<div key={item.dept} style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
|
||||||
|
<div style={{ minWidth: '100px', color: '#888' }}>{item.dept}</div>
|
||||||
|
<div style={{ flex: 1, background: 'rgba(255,255,255,0.1)', borderRadius: '10px', height: '24px', overflow: 'hidden' }}>
|
||||||
|
<div style={{
|
||||||
|
width: `${(item.count / 85) * 100}%`,
|
||||||
|
height: '100%',
|
||||||
|
background: item.color,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
paddingRight: '0.5rem',
|
||||||
|
fontSize: '0.8rem',
|
||||||
|
fontWeight: 600
|
||||||
|
}}>
|
||||||
|
{item.count}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.section}>
|
||||||
|
<h3 style={{ color: '#ffd700', marginBottom: '1rem' }}>📋 Quick Reports</h3>
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
|
||||||
|
<button className={styles.btnSecondary} style={{ justifyContent: 'space-between', display: 'flex', width: '100%' }}>
|
||||||
|
<span>Attendance Report</span>
|
||||||
|
<span>⬇️</span>
|
||||||
|
</button>
|
||||||
|
<button className={styles.btnSecondary} style={{ justifyContent: 'space-between', display: 'flex', width: '100%' }}>
|
||||||
|
<span>Leave Report</span>
|
||||||
|
<span>⬇️</span>
|
||||||
|
</button>
|
||||||
|
<button className={styles.btnSecondary} style={{ justifyContent: 'space-between', display: 'flex', width: '100%' }}>
|
||||||
|
<span>Performance Report</span>
|
||||||
|
<span>⬇️</span>
|
||||||
|
</button>
|
||||||
|
<button className={styles.btnSecondary} style={{ justifyContent: 'space-between', display: 'flex', width: '100%' }}>
|
||||||
|
<span>Payroll Summary</span>
|
||||||
|
<span>⬇️</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
164
src/app/dashboard/ham/attendance/page.tsx
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
'use client';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import styles from '../ham.module.css';
|
||||||
|
import { useToast } from '@/context/ToastContext';
|
||||||
|
|
||||||
|
export default function AttendancePage() {
|
||||||
|
const { showToast } = useToast();
|
||||||
|
const [view, setView] = useState<'daily' | 'weekly' | 'monthly'>('monthly');
|
||||||
|
const [selectedDate, setSelectedDate] = useState(new Date());
|
||||||
|
const [isCheckedIn, setIsCheckedIn] = useState(false);
|
||||||
|
|
||||||
|
// Mock calendar data for December 2024
|
||||||
|
const generateCalendar = () => {
|
||||||
|
const days = [];
|
||||||
|
const statuses = ['present', 'present', 'present', 'absent', 'leave', 'present', 'present'];
|
||||||
|
|
||||||
|
for (let i = 1; i <= 31; i++) {
|
||||||
|
const status = i > 20 ? '' : statuses[i % 7];
|
||||||
|
days.push({ day: i, status });
|
||||||
|
}
|
||||||
|
return days;
|
||||||
|
};
|
||||||
|
|
||||||
|
const calendarDays = generateCalendar();
|
||||||
|
|
||||||
|
const handleCheckIn = () => {
|
||||||
|
setIsCheckedIn(true);
|
||||||
|
showToast('✅ Checked in successfully!', 'success');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCheckOut = () => {
|
||||||
|
setIsCheckedIn(false);
|
||||||
|
showToast('👋 Checked out successfully!', 'success');
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusClass = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'present':
|
||||||
|
return styles.present;
|
||||||
|
case 'absent':
|
||||||
|
return styles.absent;
|
||||||
|
case 'leave':
|
||||||
|
return styles.leave;
|
||||||
|
default:
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.hamContainer}>
|
||||||
|
<div className={styles.sectionHeader}>
|
||||||
|
<div>
|
||||||
|
<h1 className={styles.pageTitle}>Attendance Management</h1>
|
||||||
|
<p className={styles.pageSubtitle}>Track and manage attendance</p>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', gap: '1rem' }}>
|
||||||
|
{!isCheckedIn ? (
|
||||||
|
<button className={styles.btnPrimary} onClick={handleCheckIn}>
|
||||||
|
✓ Check In
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<button className={styles.btnSecondary} onClick={handleCheckOut}>
|
||||||
|
→ Check Out
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Stats */}
|
||||||
|
<div className={styles.widgetsGrid}>
|
||||||
|
<div className={styles.widget}>
|
||||||
|
<span className={styles.widgetIcon}>✅</span>
|
||||||
|
<div className={styles.widgetValue}>18</div>
|
||||||
|
<div className={styles.widgetLabel}>Present Days</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.widget}>
|
||||||
|
<span className={styles.widgetIcon}>❌</span>
|
||||||
|
<div className={styles.widgetValue}>2</div>
|
||||||
|
<div className={styles.widgetLabel}>Absent Days</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.widget}>
|
||||||
|
<span className={styles.widgetIcon}>🏖️</span>
|
||||||
|
<div className={styles.widgetValue}>3</div>
|
||||||
|
<div className={styles.widgetLabel}>Leave Days</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.widget}>
|
||||||
|
<span className={styles.widgetIcon}>📊</span>
|
||||||
|
<div className={styles.widgetValue}>90%</div>
|
||||||
|
<div className={styles.widgetLabel}>Attendance Rate</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* View Selector */}
|
||||||
|
<div className={styles.section}>
|
||||||
|
<div style={{ display: 'flex', gap: '1rem', marginBottom: '1.5rem' }}>
|
||||||
|
{['daily', 'weekly', 'monthly'].map((v) => (
|
||||||
|
<button
|
||||||
|
key={v}
|
||||||
|
className={view === v ? styles.btnPrimary : styles.btnSecondary}
|
||||||
|
onClick={() => setView(v as any)}
|
||||||
|
style={{ padding: '0.5rem 1rem', textTransform: 'capitalize' }}
|
||||||
|
>
|
||||||
|
{v}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Calendar View */}
|
||||||
|
{view === 'monthly' && (
|
||||||
|
<>
|
||||||
|
<h3 style={{ color: '#ffd700', marginBottom: '1rem' }}>December 2024</h3>
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)', gap: '0.5rem', marginBottom: '1rem' }}>
|
||||||
|
{['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].map(day => (
|
||||||
|
<div key={day} style={{ textAlign: 'center', color: '#888', fontSize: '0.85rem', padding: '0.5rem' }}>
|
||||||
|
{day}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className={styles.calendar}>
|
||||||
|
{calendarDays.map(({ day, status }) => (
|
||||||
|
<div
|
||||||
|
key={day}
|
||||||
|
className={`${styles.calendarDay} ${getStatusClass(status)}`}
|
||||||
|
>
|
||||||
|
{day}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Legend */}
|
||||||
|
<div style={{ display: 'flex', gap: '2rem', marginTop: '1.5rem', justifyContent: 'center' }}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
|
||||||
|
<div style={{ width: '20px', height: '20px', borderRadius: '4px', background: 'rgba(0,255,157,0.2)', border: '1px solid #00ff9d' }}></div>
|
||||||
|
<span style={{ color: '#888', fontSize: '0.85rem' }}>Present</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
|
||||||
|
<div style={{ width: '20px', height: '20px', borderRadius: '4px', background: 'rgba(255,68,68,0.2)', border: '1px solid #ff4444' }}></div>
|
||||||
|
<span style={{ color: '#888', fontSize: '0.85rem' }}>Absent</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
|
||||||
|
<div style={{ width: '20px', height: '20px', borderRadius: '4px', background: 'rgba(88,166,255,0.2)', border: '1px solid #58a6ff' }}></div>
|
||||||
|
<span style={{ color: '#888', fontSize: '0.85rem' }}>Leave</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{view === 'daily' && (
|
||||||
|
<div style={{ padding: '3rem', textAlign: 'center', color: '#888' }}>
|
||||||
|
<div style={{ fontSize: '3rem', marginBottom: '1rem' }}>📅</div>
|
||||||
|
<p>Daily view will show detailed check-in/check-out times</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{view === 'weekly' && (
|
||||||
|
<div style={{ padding: '3rem', textAlign: 'center', color: '#888' }}>
|
||||||
|
<div style={{ fontSize: '3rem', marginBottom: '1rem' }}>📊</div>
|
||||||
|
<p>Weekly view will show attendance summary for the week</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
418
src/app/dashboard/ham/employees/page.tsx
Normal file
@@ -0,0 +1,418 @@
|
|||||||
|
'use client';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import styles from '../ham.module.css';
|
||||||
|
import { useToast } from '@/context/ToastContext';
|
||||||
|
|
||||||
|
// Mock data
|
||||||
|
const MOCK_EMPLOYEES = [
|
||||||
|
{ id: 'EMP-001', name: 'Vikram Singh', department: 'Engineering', role: 'Senior Developer', status: 'Active', email: 'vikram@pennyzo.com', phone: '+91 98765 43210' },
|
||||||
|
{ id: 'EMP-002', name: 'Priya Patel', department: 'Marketing', role: 'Marketing Manager', status: 'Active', email: 'priya@pennyzo.com', phone: '+91 98765 43211' },
|
||||||
|
{ id: 'EMP-003', name: 'Rahul Sharma', department: 'Sales', role: 'Sales Executive', status: 'Active', email: 'rahul@pennyzo.com', phone: '+91 98765 43212' },
|
||||||
|
{ id: 'EMP-004', name: 'Sarah Williams', department: 'HR', role: 'HR Manager', status: 'On Leave', email: 'sarah@pennyzo.com', phone: '+91 98765 43213' },
|
||||||
|
{ id: 'EMP-005', name: 'John Doe', department: 'Engineering', role: 'Junior Developer', status: 'Active', email: 'john@pennyzo.com', phone: '+91 98765 43214' }
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function EmployeesPage() {
|
||||||
|
const { showToast } = useToast();
|
||||||
|
const [employees, setEmployees] = useState(MOCK_EMPLOYEES);
|
||||||
|
const [filterDepartment, setFilterDepartment] = useState('all');
|
||||||
|
const [filterStatus, setFilterStatus] = useState('all');
|
||||||
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
const [isAddingEmployee, setIsAddingEmployee] = useState(false);
|
||||||
|
const [currentTab, setCurrentTab] = useState(0);
|
||||||
|
|
||||||
|
// Form state
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
name: '',
|
||||||
|
email: '',
|
||||||
|
phone: '',
|
||||||
|
department: '',
|
||||||
|
role: '',
|
||||||
|
doj: '',
|
||||||
|
salary: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const filteredEmployees = employees.filter(emp => {
|
||||||
|
if (filterDepartment !== 'all' && emp.department !== filterDepartment) return false;
|
||||||
|
if (filterStatus !== 'all' && emp.status !== filterStatus) return false;
|
||||||
|
if (searchQuery && !emp.name.toLowerCase().includes(searchQuery.toLowerCase()) &&
|
||||||
|
!emp.email.toLowerCase().includes(searchQuery.toLowerCase()) &&
|
||||||
|
!emp.id.toLowerCase().includes(searchQuery.toLowerCase())) return false;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleAddEmployee = () => {
|
||||||
|
setIsAddingEmployee(true);
|
||||||
|
setCurrentTab(0);
|
||||||
|
setFormData({
|
||||||
|
name: '',
|
||||||
|
email: '',
|
||||||
|
phone: '',
|
||||||
|
department: '',
|
||||||
|
role: '',
|
||||||
|
doj: '',
|
||||||
|
salary: ''
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmitEmployee = () => {
|
||||||
|
// Validation
|
||||||
|
if (!formData.name || !formData.email || !formData.department || !formData.role) {
|
||||||
|
showToast('Please fill in all required fields', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Email validation
|
||||||
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||||
|
if (!emailRegex.test(formData.email)) {
|
||||||
|
showToast('Please enter a valid email address', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate new employee
|
||||||
|
const newEmployee = {
|
||||||
|
id: `EMP-${String(employees.length + 1).padStart(3, '0')}`,
|
||||||
|
name: formData.name,
|
||||||
|
email: formData.email,
|
||||||
|
phone: formData.phone,
|
||||||
|
department: formData.department,
|
||||||
|
role: formData.role,
|
||||||
|
status: 'Active'
|
||||||
|
};
|
||||||
|
|
||||||
|
setEmployees([newEmployee, ...employees]);
|
||||||
|
setIsAddingEmployee(false);
|
||||||
|
showToast(`✅ Employee ${formData.name} added successfully!`, 'success');
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusBadge = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'Active':
|
||||||
|
return <span className={`${styles.badge} ${styles.statusActive}`}>{status}</span>;
|
||||||
|
case 'On Leave':
|
||||||
|
return <span className={`${styles.badge} ${styles.statusPending}`}>{status}</span>;
|
||||||
|
case 'Inactive':
|
||||||
|
return <span className={`${styles.badge} ${styles.statusInactive}`}>{status}</span>;
|
||||||
|
default:
|
||||||
|
return <span className={styles.badge}>{status}</span>;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.hamContainer}>
|
||||||
|
{/* Add Employee Modal */}
|
||||||
|
{isAddingEmployee && (
|
||||||
|
<div style={{
|
||||||
|
position: 'fixed',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
background: 'rgba(0,0,0,0.7)',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
zIndex: 9999,
|
||||||
|
backdropFilter: 'blur(4px)'
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
|
background: 'linear-gradient(135deg, rgba(20,25,35,0.95) 0%, rgba(30,35,45,0.95) 100%)',
|
||||||
|
border: '1px solid rgba(255,215,0,0.3)',
|
||||||
|
borderRadius: '12px',
|
||||||
|
padding: '2rem',
|
||||||
|
maxWidth: '600px',
|
||||||
|
width: '90%',
|
||||||
|
maxHeight: '90vh',
|
||||||
|
overflow: 'auto',
|
||||||
|
boxShadow: '0 20px 60px rgba(0,0,0,0.5)'
|
||||||
|
}}>
|
||||||
|
<h3 style={{ margin: '0 0 1.5rem 0', color: '#ffd700', fontSize: '1.5rem' }}>
|
||||||
|
Add New Employee
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
{/* Tabs */}
|
||||||
|
<div style={{ display: 'flex', gap: '1rem', marginBottom: '1.5rem', borderBottom: '1px solid rgba(255,255,255,0.1)' }}>
|
||||||
|
{['Personal Details', 'Job Details', 'Documents', 'System Access'].map((tab, index) => (
|
||||||
|
<button
|
||||||
|
key={index}
|
||||||
|
onClick={() => setCurrentTab(index)}
|
||||||
|
style={{
|
||||||
|
background: currentTab === index ? 'rgba(255,215,0,0.2)' : 'transparent',
|
||||||
|
border: 'none',
|
||||||
|
color: currentTab === index ? '#ffd700' : '#888',
|
||||||
|
padding: '0.75rem 1rem',
|
||||||
|
cursor: 'pointer',
|
||||||
|
borderBottom: currentTab === index ? '2px solid #ffd700' : 'none',
|
||||||
|
fontSize: '0.9rem',
|
||||||
|
fontWeight: 600
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tab}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tab Content */}
|
||||||
|
{currentTab === 0 && (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
|
||||||
|
<div>
|
||||||
|
<label style={{ display: 'block', marginBottom: '0.5rem', color: '#888', fontSize: '0.85rem' }}>
|
||||||
|
Full Name *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className={styles.inputField}
|
||||||
|
value={formData.name}
|
||||||
|
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||||
|
placeholder="e.g., John Doe"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label style={{ display: 'block', marginBottom: '0.5rem', color: '#888', fontSize: '0.85rem' }}>
|
||||||
|
Email Address *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
className={styles.inputField}
|
||||||
|
value={formData.email}
|
||||||
|
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
||||||
|
placeholder="john.doe@company.com"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label style={{ display: 'block', marginBottom: '0.5rem', color: '#888', fontSize: '0.85rem' }}>
|
||||||
|
Phone Number
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="tel"
|
||||||
|
className={styles.inputField}
|
||||||
|
value={formData.phone}
|
||||||
|
onChange={(e) => setFormData({ ...formData, phone: e.target.value })}
|
||||||
|
placeholder="+91 98765 43210"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{currentTab === 1 && (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
|
||||||
|
<div>
|
||||||
|
<label style={{ display: 'block', marginBottom: '0.5rem', color: '#888', fontSize: '0.85rem' }}>
|
||||||
|
Department *
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
className={styles.filterSelect}
|
||||||
|
value={formData.department}
|
||||||
|
onChange={(e) => setFormData({ ...formData, department: e.target.value })}
|
||||||
|
>
|
||||||
|
<option value="">Select Department</option>
|
||||||
|
<option value="Engineering">Engineering</option>
|
||||||
|
<option value="Marketing">Marketing</option>
|
||||||
|
<option value="Sales">Sales</option>
|
||||||
|
<option value="HR">HR</option>
|
||||||
|
<option value="Finance">Finance</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label style={{ display: 'block', marginBottom: '0.5rem', color: '#888', fontSize: '0.85rem' }}>
|
||||||
|
Role/Designation *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className={styles.inputField}
|
||||||
|
value={formData.role}
|
||||||
|
onChange={(e) => setFormData({ ...formData, role: e.target.value })}
|
||||||
|
placeholder="e.g., Senior Developer"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label style={{ display: 'block', marginBottom: '0.5rem', color: '#888', fontSize: '0.85rem' }}>
|
||||||
|
Date of Joining
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
className={styles.inputField}
|
||||||
|
value={formData.doj}
|
||||||
|
onChange={(e) => setFormData({ ...formData, doj: e.target.value })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label style={{ display: 'block', marginBottom: '0.5rem', color: '#888', fontSize: '0.85rem' }}>
|
||||||
|
Salary (₹)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
className={styles.inputField}
|
||||||
|
value={formData.salary}
|
||||||
|
onChange={(e) => setFormData({ ...formData, salary: e.target.value })}
|
||||||
|
placeholder="500000"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{currentTab === 2 && (
|
||||||
|
<div style={{ padding: '2rem', textAlign: 'center', color: '#888' }}>
|
||||||
|
<div style={{ fontSize: '3rem', marginBottom: '1rem' }}>📄</div>
|
||||||
|
<p>Document upload functionality will be available soon</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{currentTab === 3 && (
|
||||||
|
<div style={{ padding: '2rem', textAlign: 'center', color: '#888' }}>
|
||||||
|
<div style={{ fontSize: '3rem', marginBottom: '1rem' }}>🔐</div>
|
||||||
|
<p>System access configuration will be available soon</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', gap: '1rem', justifyContent: 'flex-end', marginTop: '2rem' }}>
|
||||||
|
<button
|
||||||
|
className={styles.btnSecondary}
|
||||||
|
onClick={() => setIsAddingEmployee(false)}
|
||||||
|
style={{ padding: '0.6rem 1.5rem' }}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
{currentTab < 3 && (
|
||||||
|
<button
|
||||||
|
className={styles.btnPrimary}
|
||||||
|
onClick={() => setCurrentTab(currentTab + 1)}
|
||||||
|
style={{ padding: '0.6rem 1.5rem' }}
|
||||||
|
>
|
||||||
|
Next →
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{currentTab === 3 && (
|
||||||
|
<button
|
||||||
|
className={styles.btnPrimary}
|
||||||
|
onClick={handleSubmitEmployee}
|
||||||
|
style={{ padding: '0.6rem 1.5rem' }}
|
||||||
|
>
|
||||||
|
Add Employee
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className={styles.sectionHeader}>
|
||||||
|
<div>
|
||||||
|
<h1 className={styles.pageTitle}>Employee Directory</h1>
|
||||||
|
<p className={styles.pageSubtitle}>Manage your workforce</p>
|
||||||
|
</div>
|
||||||
|
<button className={styles.btnPrimary} onClick={handleAddEmployee}>
|
||||||
|
+ Add Employee
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Filters */}
|
||||||
|
<div className={styles.filterBar}>
|
||||||
|
<div className={styles.filterGroup}>
|
||||||
|
<label className={styles.filterLabel}>Search</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className={styles.inputField}
|
||||||
|
placeholder="Name, Email, ID..."
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
style={{ minWidth: '250px' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.filterGroup}>
|
||||||
|
<label className={styles.filterLabel}>Department</label>
|
||||||
|
<select
|
||||||
|
className={styles.filterSelect}
|
||||||
|
value={filterDepartment}
|
||||||
|
onChange={(e) => setFilterDepartment(e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="all">All Departments</option>
|
||||||
|
<option value="Engineering">Engineering</option>
|
||||||
|
<option value="Marketing">Marketing</option>
|
||||||
|
<option value="Sales">Sales</option>
|
||||||
|
<option value="HR">HR</option>
|
||||||
|
<option value="Finance">Finance</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.filterGroup}>
|
||||||
|
<label className={styles.filterLabel}>Status</label>
|
||||||
|
<select
|
||||||
|
className={styles.filterSelect}
|
||||||
|
value={filterStatus}
|
||||||
|
onChange={(e) => setFilterStatus(e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="all">All Status</option>
|
||||||
|
<option value="Active">Active</option>
|
||||||
|
<option value="On Leave">On Leave</option>
|
||||||
|
<option value="Inactive">Inactive</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Employee Table */}
|
||||||
|
<div className={styles.section}>
|
||||||
|
<table className={styles.table}>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Employee ID</th>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Department</th>
|
||||||
|
<th>Role</th>
|
||||||
|
<th>Email</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{filteredEmployees.map(emp => (
|
||||||
|
<tr key={emp.id}>
|
||||||
|
<td style={{ color: '#888', fontSize: '0.9rem', fontWeight: 600 }}>{emp.id}</td>
|
||||||
|
<td style={{ fontWeight: 600 }}>{emp.name}</td>
|
||||||
|
<td>{emp.department}</td>
|
||||||
|
<td>{emp.role}</td>
|
||||||
|
<td style={{ color: '#888', fontSize: '0.85rem' }}>{emp.email}</td>
|
||||||
|
<td>{getStatusBadge(emp.status)}</td>
|
||||||
|
<td>
|
||||||
|
<div style={{ display: 'flex', gap: '0.5rem' }}>
|
||||||
|
<button
|
||||||
|
className={styles.btnSecondary}
|
||||||
|
style={{ padding: '0.4rem 0.8rem', fontSize: '0.8rem' }}
|
||||||
|
onClick={() => showToast('View employee details', 'info')}
|
||||||
|
>
|
||||||
|
View
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={styles.btnPrimary}
|
||||||
|
style={{ padding: '0.4rem 0.8rem', fontSize: '0.8rem' }}
|
||||||
|
onClick={() => showToast('Edit employee', 'info')}
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
{filteredEmployees.length === 0 && (
|
||||||
|
<div className={styles.emptyState}>
|
||||||
|
<div className={styles.emptyStateIcon}>👥</div>
|
||||||
|
<div className={styles.emptyStateTitle}>No employees found</div>
|
||||||
|
<div className={styles.emptyStateText}>Try adjusting your filters or add a new employee</div>
|
||||||
|
<button className={styles.btnPrimary} onClick={handleAddEmployee}>
|
||||||
|
+ Add Employee
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
310
src/app/dashboard/ham/ham.module.css
Normal file
@@ -0,0 +1,310 @@
|
|||||||
|
/* HAM Module - Luxury Tech Theme */
|
||||||
|
|
||||||
|
.hamContainer {
|
||||||
|
padding: 2rem;
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Section Header */
|
||||||
|
.sectionHeader {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pageTitle {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
background: linear-gradient(135deg, #ffd700 0%, #ffed4e 100%);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pageSubtitle {
|
||||||
|
color: #888;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
margin: 0.5rem 0 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dashboard Widgets Grid */
|
||||||
|
.widgetsGrid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||||
|
gap: 1.5rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.widget {
|
||||||
|
background: linear-gradient(135deg, rgba(255, 255, 255, 0.05) 0%, rgba(255, 255, 255, 0.02) 100%);
|
||||||
|
background: rgba(255, 255, 255, 0.03);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
-webkit-backdrop-filter: blur(10px);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.widget:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border-color: rgba(255, 215, 0, 0.3);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.widgetIcon {
|
||||||
|
font-size: 2rem;
|
||||||
|
opacity: 0.8;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.widgetValue {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #fff;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.widgetLabel {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #888;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.widgetChange {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.widgetChange.positive {
|
||||||
|
color: #00ff9d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.widgetChange.negative {
|
||||||
|
color: #ff4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Section Card */
|
||||||
|
.section {
|
||||||
|
background: linear-gradient(135deg, rgba(255, 255, 255, 0.05) 0%, rgba(255, 255, 255, 0.02) 100%);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Table Styles */
|
||||||
|
.table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table thead {
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table th {
|
||||||
|
text-align: left;
|
||||||
|
padding: 1rem;
|
||||||
|
color: #888;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table td {
|
||||||
|
padding: 1rem;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table tr:hover {
|
||||||
|
background: rgba(255, 215, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Buttons */
|
||||||
|
.btnPrimary {
|
||||||
|
background: linear-gradient(135deg, #ffd700 0%, #ffed4e 100%);
|
||||||
|
color: #000;
|
||||||
|
border: none;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btnPrimary:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 8px 16px rgba(255, 215, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btnSecondary {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
color: #fff;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btnSecondary:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.15);
|
||||||
|
border-color: rgba(255, 215, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Badge */
|
||||||
|
.badge {
|
||||||
|
padding: 0.4rem 0.8rem;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Status Colors */
|
||||||
|
.statusActive {
|
||||||
|
background: rgba(0, 255, 157, 0.2);
|
||||||
|
color: #00ff9d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statusPending {
|
||||||
|
background: rgba(255, 215, 0, 0.2);
|
||||||
|
color: #ffd700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statusInactive {
|
||||||
|
background: rgba(255, 68, 68, 0.2);
|
||||||
|
color: #ff4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Filter Bar */
|
||||||
|
.filterBar {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filterGroup {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filterLabel {
|
||||||
|
color: #888;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filterSelect,
|
||||||
|
.inputField {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
color: #fff;
|
||||||
|
padding: 0.6rem 1rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filterSelect:focus,
|
||||||
|
.inputField:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: rgba(255, 215, 0, 0.5);
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Calendar View */
|
||||||
|
.calendar {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(7, 1fr);
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendarDay {
|
||||||
|
aspect-ratio: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendarDay:hover {
|
||||||
|
background: rgba(255, 215, 0, 0.1);
|
||||||
|
border-color: rgba(255, 215, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendarDay.present {
|
||||||
|
background: rgba(0, 255, 157, 0.2);
|
||||||
|
border-color: #00ff9d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendarDay.absent {
|
||||||
|
background: rgba(255, 68, 68, 0.2);
|
||||||
|
border-color: #ff4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendarDay.leave {
|
||||||
|
background: rgba(88, 166, 255, 0.2);
|
||||||
|
border-color: #58a6ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Empty State */
|
||||||
|
.emptyState {
|
||||||
|
text-align: center;
|
||||||
|
padding: 4rem 2rem;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emptyStateIcon {
|
||||||
|
font-size: 4rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emptyStateTitle {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #888;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emptyStateText {
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animations */
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fadeIn {
|
||||||
|
animation: fadeIn 0.3s ease-out;
|
||||||
|
}
|
||||||
276
src/app/dashboard/ham/leave/page.tsx
Normal file
@@ -0,0 +1,276 @@
|
|||||||
|
'use client';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import styles from '../ham.module.css';
|
||||||
|
import { useToast } from '@/context/ToastContext';
|
||||||
|
|
||||||
|
// Mock data
|
||||||
|
const MOCK_LEAVES = [
|
||||||
|
{ id: 'L-001', employee: 'Vikram Singh', type: 'Casual Leave', startDate: '2024-12-25', endDate: '2024-12-27', days: 3, status: 'Pending', reason: 'Family function' },
|
||||||
|
{ id: 'L-002', employee: 'Priya Patel', type: 'Sick Leave', startDate: '2024-12-20', endDate: '2024-12-21', days: 2, status: 'Approved', reason: 'Medical checkup' },
|
||||||
|
{ id: 'L-003', employee: 'Rahul Sharma', type: 'Earned Leave', startDate: '2024-12-30', endDate: '2025-01-02', days: 4, status: 'Pending', reason: 'Year-end vacation' }
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function LeavePage() {
|
||||||
|
const { showToast } = useToast();
|
||||||
|
const [leaves, setLeaves] = useState(MOCK_LEAVES);
|
||||||
|
const [isApplyingLeave, setIsApplyingLeave] = useState(false);
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
type: '',
|
||||||
|
startDate: '',
|
||||||
|
endDate: '',
|
||||||
|
reason: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleApplyLeave = () => {
|
||||||
|
setIsApplyingLeave(true);
|
||||||
|
setFormData({ type: '', startDate: '', endDate: '', reason: '' });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmitLeave = () => {
|
||||||
|
if (!formData.type || !formData.startDate || !formData.endDate) {
|
||||||
|
showToast('Please fill in all required fields', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const start = new Date(formData.startDate);
|
||||||
|
const end = new Date(formData.endDate);
|
||||||
|
if (end < start) {
|
||||||
|
showToast('End date must be after start date', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const days = Math.ceil((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24)) + 1;
|
||||||
|
|
||||||
|
const newLeave = {
|
||||||
|
id: `L-${String(leaves.length + 1).padStart(3, '0')}`,
|
||||||
|
employee: 'Current User',
|
||||||
|
type: formData.type,
|
||||||
|
startDate: formData.startDate,
|
||||||
|
endDate: formData.endDate,
|
||||||
|
days,
|
||||||
|
status: 'Pending',
|
||||||
|
reason: formData.reason
|
||||||
|
};
|
||||||
|
|
||||||
|
setLeaves([newLeave, ...leaves]);
|
||||||
|
setIsApplyingLeave(false);
|
||||||
|
showToast('✅ Leave application submitted successfully!', 'success');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleApprove = (id: string) => {
|
||||||
|
setLeaves(leaves.map(l => l.id === id ? { ...l, status: 'Approved' } : l));
|
||||||
|
showToast('✅ Leave approved', 'success');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReject = (id: string) => {
|
||||||
|
setLeaves(leaves.map(l => l.id === id ? { ...l, status: 'Rejected' } : l));
|
||||||
|
showToast('❌ Leave rejected', 'warning');
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusBadge = (status: string) => {
|
||||||
|
const styles_map = {
|
||||||
|
'Pending': styles.statusPending,
|
||||||
|
'Approved': styles.statusActive,
|
||||||
|
'Rejected': styles.statusInactive
|
||||||
|
};
|
||||||
|
return <span className={`${styles.badge} ${styles_map[status as keyof typeof styles_map]}`}>{status}</span>;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.hamContainer}>
|
||||||
|
{/* Apply Leave Modal */}
|
||||||
|
{isApplyingLeave && (
|
||||||
|
<div style={{
|
||||||
|
position: 'fixed',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
background: 'rgba(0,0,0,0.7)',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
zIndex: 9999,
|
||||||
|
backdropFilter: 'blur(4px)'
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
|
background: 'linear-gradient(135deg, rgba(20,25,35,0.95) 0%, rgba(30,35,45,0.95) 100%)',
|
||||||
|
border: '1px solid rgba(255,215,0,0.3)',
|
||||||
|
borderRadius: '12px',
|
||||||
|
padding: '2rem',
|
||||||
|
maxWidth: '500px',
|
||||||
|
width: '90%',
|
||||||
|
boxShadow: '0 20px 60px rgba(0,0,0,0.5)'
|
||||||
|
}}>
|
||||||
|
<h3 style={{ margin: '0 0 1.5rem 0', color: '#ffd700', fontSize: '1.5rem' }}>
|
||||||
|
Apply for Leave
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
|
||||||
|
<div>
|
||||||
|
<label style={{ display: 'block', marginBottom: '0.5rem', color: '#888', fontSize: '0.85rem' }}>
|
||||||
|
Leave Type *
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
className={styles.filterSelect}
|
||||||
|
value={formData.type}
|
||||||
|
onChange={(e) => setFormData({ ...formData, type: e.target.value })}
|
||||||
|
>
|
||||||
|
<option value="">Select Type</option>
|
||||||
|
<option value="Casual Leave">Casual Leave</option>
|
||||||
|
<option value="Sick Leave">Sick Leave</option>
|
||||||
|
<option value="Earned Leave">Earned Leave</option>
|
||||||
|
<option value="Maternity Leave">Maternity Leave</option>
|
||||||
|
<option value="Paternity Leave">Paternity Leave</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem' }}>
|
||||||
|
<div>
|
||||||
|
<label style={{ display: 'block', marginBottom: '0.5rem', color: '#888', fontSize: '0.85rem' }}>
|
||||||
|
Start Date *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
className={styles.inputField}
|
||||||
|
value={formData.startDate}
|
||||||
|
onChange={(e) => setFormData({ ...formData, startDate: e.target.value })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label style={{ display: 'block', marginBottom: '0.5rem', color: '#888', fontSize: '0.85rem' }}>
|
||||||
|
End Date *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
className={styles.inputField}
|
||||||
|
value={formData.endDate}
|
||||||
|
onChange={(e) => setFormData({ ...formData, endDate: e.target.value })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label style={{ display: 'block', marginBottom: '0.5rem', color: '#888', fontSize: '0.85rem' }}>
|
||||||
|
Reason
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
className={styles.inputField}
|
||||||
|
value={formData.reason}
|
||||||
|
onChange={(e) => setFormData({ ...formData, reason: e.target.value })}
|
||||||
|
placeholder="Optional"
|
||||||
|
rows={3}
|
||||||
|
style={{ resize: 'vertical' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', gap: '1rem', justifyContent: 'flex-end', marginTop: '2rem' }}>
|
||||||
|
<button
|
||||||
|
className={styles.btnSecondary}
|
||||||
|
onClick={() => setIsApplyingLeave(false)}
|
||||||
|
style={{ padding: '0.6rem 1.5rem' }}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={styles.btnPrimary}
|
||||||
|
onClick={handleSubmitLeave}
|
||||||
|
style={{ padding: '0.6rem 1.5rem' }}
|
||||||
|
>
|
||||||
|
Submit Application
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className={styles.sectionHeader}>
|
||||||
|
<div>
|
||||||
|
<h1 className={styles.pageTitle}>Leave Management</h1>
|
||||||
|
<p className={styles.pageSubtitle}>Apply and manage leave requests</p>
|
||||||
|
</div>
|
||||||
|
<button className={styles.btnPrimary} onClick={handleApplyLeave}>
|
||||||
|
+ Apply Leave
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Leave Balance */}
|
||||||
|
<div className={styles.widgetsGrid}>
|
||||||
|
<div className={styles.widget}>
|
||||||
|
<span className={styles.widgetIcon}>🏖️</span>
|
||||||
|
<div className={styles.widgetValue}>12</div>
|
||||||
|
<div className={styles.widgetLabel}>Casual Leave</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.widget}>
|
||||||
|
<span className={styles.widgetIcon}>🤒</span>
|
||||||
|
<div className={styles.widgetValue}>8</div>
|
||||||
|
<div className={styles.widgetLabel}>Sick Leave</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.widget}>
|
||||||
|
<span className={styles.widgetIcon}>✈️</span>
|
||||||
|
<div className={styles.widgetValue}>15</div>
|
||||||
|
<div className={styles.widgetLabel}>Earned Leave</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.widget}>
|
||||||
|
<span className={styles.widgetIcon}>⏳</span>
|
||||||
|
<div className={styles.widgetValue}>{leaves.filter(l => l.status === 'Pending').length}</div>
|
||||||
|
<div className={styles.widgetLabel}>Pending Approvals</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Leave Applications */}
|
||||||
|
<div className={styles.section}>
|
||||||
|
<h3 style={{ color: '#ffd700', marginBottom: '1rem' }}>Leave Applications</h3>
|
||||||
|
<table className={styles.table}>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Leave ID</th>
|
||||||
|
<th>Employee</th>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>Start Date</th>
|
||||||
|
<th>End Date</th>
|
||||||
|
<th>Days</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{leaves.map(leave => (
|
||||||
|
<tr key={leave.id}>
|
||||||
|
<td style={{ color: '#888', fontSize: '0.9rem', fontWeight: 600 }}>{leave.id}</td>
|
||||||
|
<td style={{ fontWeight: 600 }}>{leave.employee}</td>
|
||||||
|
<td>{leave.type}</td>
|
||||||
|
<td>{leave.startDate}</td>
|
||||||
|
<td>{leave.endDate}</td>
|
||||||
|
<td>{leave.days}</td>
|
||||||
|
<td>{getStatusBadge(leave.status)}</td>
|
||||||
|
<td>
|
||||||
|
{leave.status === 'Pending' && (
|
||||||
|
<div style={{ display: 'flex', gap: '0.5rem' }}>
|
||||||
|
<button
|
||||||
|
className={styles.btnPrimary}
|
||||||
|
style={{ padding: '0.4rem 0.8rem', fontSize: '0.8rem' }}
|
||||||
|
onClick={() => handleApprove(leave.id)}
|
||||||
|
>
|
||||||
|
✓ Approve
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={styles.btnSecondary}
|
||||||
|
style={{ padding: '0.4rem 0.8rem', fontSize: '0.8rem' }}
|
||||||
|
onClick={() => handleReject(leave.id)}
|
||||||
|
>
|
||||||
|
✗ Reject
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
169
src/app/dashboard/ham/page.tsx
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
'use client';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import styles from './ham.module.css';
|
||||||
|
import shared from '../shared/components.module.css';
|
||||||
|
|
||||||
|
// Mock data
|
||||||
|
const MOCK_STATS = {
|
||||||
|
totalEmployees: 247,
|
||||||
|
presentToday: 231,
|
||||||
|
onLeaveToday: 12,
|
||||||
|
pendingApprovals: 8,
|
||||||
|
upcomingBirthdays: 5,
|
||||||
|
reviewsDue: 15,
|
||||||
|
payrollStatus: 'Processed'
|
||||||
|
};
|
||||||
|
|
||||||
|
const MOCK_BIRTHDAYS = [
|
||||||
|
{ id: 1, name: 'Rahul Sharma', date: 'Tomorrow', department: 'Engineering' },
|
||||||
|
{ id: 2, name: 'Priya Patel', date: 'Dec 23', department: 'Marketing' },
|
||||||
|
{ id: 3, name: 'Amit Kumar', date: 'Dec 25', department: 'Sales' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const MOCK_RECENT_ACTIVITIES = [
|
||||||
|
{ id: 1, type: 'leave', message: 'Vikram Singh applied for leave', time: '10 mins ago' },
|
||||||
|
{ id: 2, type: 'attendance', message: 'Sarah Williams checked in', time: '25 mins ago' },
|
||||||
|
{ id: 3, type: 'review', message: 'Performance review completed for John Doe', time: '1 hour ago' },
|
||||||
|
{ id: 4, type: 'employee', message: 'New employee onboarded: Jane Smith', time: '2 hours ago' }
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function HAMDashboard() {
|
||||||
|
const [stats] = useState(MOCK_STATS);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.hamContainer}>
|
||||||
|
<div className={styles.sectionHeader}>
|
||||||
|
<div>
|
||||||
|
<h1 className={styles.pageTitle}>HAM Dashboard</h1>
|
||||||
|
<p className={styles.pageSubtitle}>Human Asset Management Overview</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Stats Widgets */}
|
||||||
|
<div className={styles.widgetsGrid}>
|
||||||
|
<div className={styles.widget}>
|
||||||
|
<span className={styles.widgetIcon}>👥</span>
|
||||||
|
<div className={styles.widgetValue}>{stats.totalEmployees}</div>
|
||||||
|
<div className={styles.widgetLabel}>Total Employees</div>
|
||||||
|
<div className={`${styles.widgetChange} ${styles.positive}`}>
|
||||||
|
↑ 5 this month
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.widget}>
|
||||||
|
<span className={styles.widgetIcon}>✅</span>
|
||||||
|
<div className={styles.widgetValue}>{stats.presentToday}</div>
|
||||||
|
<div className={styles.widgetLabel}>Present Today</div>
|
||||||
|
<div className={`${styles.widgetChange} ${styles.positive}`}>
|
||||||
|
{((stats.presentToday / stats.totalEmployees) * 100).toFixed(1)}% attendance
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.widget}>
|
||||||
|
<span className={styles.widgetIcon}>🏖️</span>
|
||||||
|
<div className={styles.widgetValue}>{stats.onLeaveToday}</div>
|
||||||
|
<div className={styles.widgetLabel}>On Leave Today</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.widget}>
|
||||||
|
<span className={styles.widgetIcon}>⏳</span>
|
||||||
|
<div className={styles.widgetValue}>{stats.pendingApprovals}</div>
|
||||||
|
<div className={styles.widgetLabel}>Pending Approvals</div>
|
||||||
|
<Link href="/dashboard/ham/leave" style={{ fontSize: '0.85rem', color: '#ffd700', marginTop: '0.5rem', display: 'block' }}>
|
||||||
|
View All →
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.widget}>
|
||||||
|
<span className={styles.widgetIcon}>🎂</span>
|
||||||
|
<div className={styles.widgetValue}>{stats.upcomingBirthdays}</div>
|
||||||
|
<div className={styles.widgetLabel}>Upcoming Birthdays</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.widget}>
|
||||||
|
<span className={styles.widgetIcon}>📊</span>
|
||||||
|
<div className={styles.widgetValue}>{stats.reviewsDue}</div>
|
||||||
|
<div className={styles.widgetLabel}>Reviews Due</div>
|
||||||
|
<Link href="/dashboard/ham/performance" style={{ fontSize: '0.85rem', color: '#ffd700', marginTop: '0.5rem', display: 'block' }}>
|
||||||
|
View All →
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.widget}>
|
||||||
|
<span className={styles.widgetIcon}>💰</span>
|
||||||
|
<div className={styles.widgetValue} style={{ fontSize: '1.5rem' }}>{stats.payrollStatus}</div>
|
||||||
|
<div className={styles.widgetLabel}>Payroll Status</div>
|
||||||
|
<div className={`${styles.widgetChange} ${styles.positive}`}>
|
||||||
|
December 2024
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Quick Actions */}
|
||||||
|
<div className={styles.section}>
|
||||||
|
<h3 style={{ color: '#ffd700', marginBottom: '1rem' }}>Quick Actions</h3>
|
||||||
|
<div style={{ display: 'flex', gap: '1rem', flexWrap: 'wrap' }}>
|
||||||
|
<Link href="/dashboard/ham/employees">
|
||||||
|
<button className={styles.btnPrimary}>+ Add Employee</button>
|
||||||
|
</Link>
|
||||||
|
<Link href="/dashboard/ham/attendance">
|
||||||
|
<button className={styles.btnSecondary}>Mark Attendance</button>
|
||||||
|
</Link>
|
||||||
|
<Link href="/dashboard/ham/leave">
|
||||||
|
<button className={styles.btnSecondary}>Approve Leaves</button>
|
||||||
|
</Link>
|
||||||
|
<Link href="/dashboard/ham/payroll">
|
||||||
|
<button className={styles.btnSecondary}>View Payroll</button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Two Column Layout */}
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(400px, 1fr))', gap: '1.5rem' }}>
|
||||||
|
{/* Upcoming Birthdays */}
|
||||||
|
<div className={styles.section}>
|
||||||
|
<h3 style={{ color: '#ffd700', marginBottom: '1rem' }}>🎂 Upcoming Birthdays</h3>
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
|
||||||
|
{MOCK_BIRTHDAYS.map(birthday => (
|
||||||
|
<div key={birthday.id} style={{
|
||||||
|
padding: '1rem',
|
||||||
|
background: 'rgba(255,255,255,0.03)',
|
||||||
|
borderRadius: '8px',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center'
|
||||||
|
}}>
|
||||||
|
<div>
|
||||||
|
<div style={{ fontWeight: 600, color: '#fff' }}>{birthday.name}</div>
|
||||||
|
<div style={{ fontSize: '0.85rem', color: '#888' }}>{birthday.department}</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ color: '#ffd700', fontWeight: 600 }}>{birthday.date}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Recent Activities */}
|
||||||
|
<div className={styles.section}>
|
||||||
|
<h3 style={{ color: '#ffd700', marginBottom: '1rem' }}>📋 Recent Activities</h3>
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
|
||||||
|
{MOCK_RECENT_ACTIVITIES.map(activity => (
|
||||||
|
<div key={activity.id} style={{
|
||||||
|
padding: '1rem',
|
||||||
|
background: 'rgba(255,255,255,0.03)',
|
||||||
|
borderRadius: '8px',
|
||||||
|
borderLeft: '3px solid #ffd700'
|
||||||
|
}}>
|
||||||
|
<div style={{ fontWeight: 600, color: '#fff', marginBottom: '0.3rem' }}>
|
||||||
|
{activity.message}
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: '0.8rem', color: '#888' }}>{activity.time}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
121
src/app/dashboard/ham/payroll/page.tsx
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
'use client';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import styles from '../ham.module.css';
|
||||||
|
import { useToast } from '@/context/ToastContext';
|
||||||
|
|
||||||
|
export default function PayrollPage() {
|
||||||
|
const { showToast } = useToast();
|
||||||
|
const [selectedMonth, setSelectedMonth] = useState('2024-12');
|
||||||
|
|
||||||
|
const payrollData = {
|
||||||
|
basicSalary: 500000,
|
||||||
|
hra: 150000,
|
||||||
|
allowances: 50000,
|
||||||
|
grossSalary: 700000,
|
||||||
|
pf: 60000,
|
||||||
|
tax: 100000,
|
||||||
|
otherDeductions: 10000,
|
||||||
|
totalDeductions: 170000,
|
||||||
|
netSalary: 530000
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDownloadSlip = () => {
|
||||||
|
showToast('📄 Downloading payroll slip...', 'success');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.hamContainer}>
|
||||||
|
<div className={styles.sectionHeader}>
|
||||||
|
<div>
|
||||||
|
<h1 className={styles.pageTitle}>Payroll</h1>
|
||||||
|
<p className={styles.pageSubtitle}>View your salary details</p>
|
||||||
|
</div>
|
||||||
|
<button className={styles.btnPrimary} onClick={handleDownloadSlip}>
|
||||||
|
⬇️ Download Slip
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Month Selector */}
|
||||||
|
<div className={styles.section}>
|
||||||
|
<div className={styles.filterGroup}>
|
||||||
|
<label className={styles.filterLabel}>Select Month</label>
|
||||||
|
<select
|
||||||
|
className={styles.filterSelect}
|
||||||
|
value={selectedMonth}
|
||||||
|
onChange={(e) => setSelectedMonth(e.target.value)}
|
||||||
|
style={{ maxWidth: '200px' }}
|
||||||
|
>
|
||||||
|
<option value="2024-12">December 2024</option>
|
||||||
|
<option value="2024-11">November 2024</option>
|
||||||
|
<option value="2024-10">October 2024</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Payroll Summary */}
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))', gap: '1.5rem' }}>
|
||||||
|
{/* Earnings */}
|
||||||
|
<div className={styles.section}>
|
||||||
|
<h3 style={{ color: '#ffd700', marginBottom: '1rem' }}>💰 Earnings</h3>
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', padding: '0.5rem 0', borderBottom: '1px solid rgba(255,255,255,0.05)' }}>
|
||||||
|
<span style={{ color: '#888' }}>Basic Salary</span>
|
||||||
|
<span style={{ fontWeight: 600 }}>₹{payrollData.basicSalary.toLocaleString('en-IN')}</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', padding: '0.5rem 0', borderBottom: '1px solid rgba(255,255,255,0.05)' }}>
|
||||||
|
<span style={{ color: '#888' }}>HRA</span>
|
||||||
|
<span style={{ fontWeight: 600 }}>₹{payrollData.hra.toLocaleString('en-IN')}</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', padding: '0.5rem 0', borderBottom: '1px solid rgba(255,255,255,0.05)' }}>
|
||||||
|
<span style={{ color: '#888' }}>Allowances</span>
|
||||||
|
<span style={{ fontWeight: 600 }}>₹{payrollData.allowances.toLocaleString('en-IN')}</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', padding: '0.75rem 0', marginTop: '0.5rem' }}>
|
||||||
|
<span style={{ color: '#ffd700', fontWeight: 700 }}>Gross Salary</span>
|
||||||
|
<span style={{ color: '#ffd700', fontWeight: 700, fontSize: '1.2rem' }}>₹{payrollData.grossSalary.toLocaleString('en-IN')}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Deductions */}
|
||||||
|
<div className={styles.section}>
|
||||||
|
<h3 style={{ color: '#ff4444', marginBottom: '1rem' }}>📉 Deductions</h3>
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', padding: '0.5rem 0', borderBottom: '1px solid rgba(255,255,255,0.05)' }}>
|
||||||
|
<span style={{ color: '#888' }}>Provident Fund</span>
|
||||||
|
<span style={{ fontWeight: 600 }}>₹{payrollData.pf.toLocaleString('en-IN')}</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', padding: '0.5rem 0', borderBottom: '1px solid rgba(255,255,255,0.05)' }}>
|
||||||
|
<span style={{ color: '#888' }}>Tax (TDS)</span>
|
||||||
|
<span style={{ fontWeight: 600 }}>₹{payrollData.tax.toLocaleString('en-IN')}</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', padding: '0.5rem 0', borderBottom: '1px solid rgba(255,255,255,0.05)' }}>
|
||||||
|
<span style={{ color: '#888' }}>Other Deductions</span>
|
||||||
|
<span style={{ fontWeight: 600 }}>₹{payrollData.otherDeductions.toLocaleString('en-IN')}</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', padding: '0.75rem 0', marginTop: '0.5rem' }}>
|
||||||
|
<span style={{ color: '#ff4444', fontWeight: 700 }}>Total Deductions</span>
|
||||||
|
<span style={{ color: '#ff4444', fontWeight: 700, fontSize: '1.2rem' }}>₹{payrollData.totalDeductions.toLocaleString('en-IN')}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Net Salary */}
|
||||||
|
<div className={styles.section} style={{
|
||||||
|
background: 'linear-gradient(135deg, rgba(255,215,0,0.1) 0%, rgba(255,215,0,0.05) 100%)',
|
||||||
|
border: '1px solid rgba(255,215,0,0.3)'
|
||||||
|
}}>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
|
<div>
|
||||||
|
<div style={{ color: '#888', fontSize: '0.9rem', marginBottom: '0.5rem' }}>Net Salary (Take Home)</div>
|
||||||
|
<div style={{ color: '#ffd700', fontSize: '2.5rem', fontWeight: 700 }}>
|
||||||
|
₹{payrollData.netSalary.toLocaleString('en-IN')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: '4rem' }}>💵</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
124
src/app/dashboard/ham/performance/page.tsx
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
'use client';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import styles from '../ham.module.css';
|
||||||
|
import { useToast } from '@/context/ToastContext';
|
||||||
|
|
||||||
|
// Mock data
|
||||||
|
const MOCK_GOALS = [
|
||||||
|
{ id: 'G-001', title: 'Complete Q4 Project Deliverables', weightage: 40, progress: 75, timeline: 'Dec 31, 2024' },
|
||||||
|
{ id: 'G-002', title: 'Improve Code Quality Metrics', weightage: 30, progress: 60, timeline: 'Dec 31, 2024' },
|
||||||
|
{ id: 'G-003', title: 'Mentor Junior Developers', weightage: 30, progress: 85, timeline: 'Dec 31, 2024' }
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function PerformancePage() {
|
||||||
|
const { showToast } = useToast();
|
||||||
|
const [goals] = useState(MOCK_GOALS);
|
||||||
|
const [isAddingGoal, setIsAddingGoal] = useState(false);
|
||||||
|
|
||||||
|
const totalProgress = goals.reduce((sum, goal) => sum + (goal.progress * goal.weightage / 100), 0);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.hamContainer}>
|
||||||
|
<div className={styles.sectionHeader}>
|
||||||
|
<div>
|
||||||
|
<h1 className={styles.pageTitle}>Performance Management</h1>
|
||||||
|
<p className={styles.pageSubtitle}>Track goals and reviews</p>
|
||||||
|
</div>
|
||||||
|
<button className={styles.btnPrimary} onClick={() => setIsAddingGoal(true)}>
|
||||||
|
+ Set Goal
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Performance Overview */}
|
||||||
|
<div className={styles.widgetsGrid}>
|
||||||
|
<div className={styles.widget}>
|
||||||
|
<span className={styles.widgetIcon}>🎯</span>
|
||||||
|
<div className={styles.widgetValue}>{totalProgress.toFixed(0)}%</div>
|
||||||
|
<div className={styles.widgetLabel}>Overall Progress</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.widget}>
|
||||||
|
<span className={styles.widgetIcon}>⭐</span>
|
||||||
|
<div className={styles.widgetValue}>4.5</div>
|
||||||
|
<div className={styles.widgetLabel}>Last Review Score</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.widget}>
|
||||||
|
<span className={styles.widgetIcon}>📅</span>
|
||||||
|
<div className={styles.widgetValue} style={{ fontSize: '1.2rem' }}>Jan 15</div>
|
||||||
|
<div className={styles.widgetLabel}>Next Review Date</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.widget}>
|
||||||
|
<span className={styles.widgetIcon}>🏆</span>
|
||||||
|
<div className={styles.widgetValue}>{goals.length}</div>
|
||||||
|
<div className={styles.widgetLabel}>Active Goals</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Goals */}
|
||||||
|
<div className={styles.section}>
|
||||||
|
<h3 style={{ color: '#ffd700', marginBottom: '1rem' }}>Current Goals</h3>
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
|
||||||
|
{goals.map(goal => (
|
||||||
|
<div key={goal.id} style={{
|
||||||
|
padding: '1.5rem',
|
||||||
|
background: 'rgba(255,255,255,0.03)',
|
||||||
|
borderRadius: '8px',
|
||||||
|
border: '1px solid rgba(255,255,255,0.1)'
|
||||||
|
}}>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '1rem' }}>
|
||||||
|
<div>
|
||||||
|
<div style={{ fontWeight: 600, color: '#fff', fontSize: '1.1rem' }}>{goal.title}</div>
|
||||||
|
<div style={{ fontSize: '0.85rem', color: '#888', marginTop: '0.3rem' }}>
|
||||||
|
Weightage: {goal.weightage}% • Due: {goal.timeline}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: '1.5rem', fontWeight: 700, color: '#ffd700' }}>
|
||||||
|
{goal.progress}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ background: 'rgba(255,255,255,0.1)', borderRadius: '10px', height: '10px', overflow: 'hidden' }}>
|
||||||
|
<div style={{
|
||||||
|
width: `${goal.progress}%`,
|
||||||
|
height: '100%',
|
||||||
|
background: 'linear-gradient(90deg, #ffd700, #ffed4e)',
|
||||||
|
transition: 'width 0.3s ease'
|
||||||
|
}}></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Review History */}
|
||||||
|
<div className={styles.section}>
|
||||||
|
<h3 style={{ color: '#ffd700', marginBottom: '1rem' }}>Review History</h3>
|
||||||
|
<table className={styles.table}>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Review Period</th>
|
||||||
|
<th>Self Rating</th>
|
||||||
|
<th>Manager Rating</th>
|
||||||
|
<th>Final Score</th>
|
||||||
|
<th>Status</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>Q3 2024</td>
|
||||||
|
<td>4.5</td>
|
||||||
|
<td>4.5</td>
|
||||||
|
<td style={{ color: '#ffd700', fontWeight: 700 }}>4.5</td>
|
||||||
|
<td><span className={`${styles.badge} ${styles.statusActive}`}>Completed</span></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Q2 2024</td>
|
||||||
|
<td>4.0</td>
|
||||||
|
<td>4.2</td>
|
||||||
|
<td style={{ color: '#ffd700', fontWeight: 700 }}>4.1</td>
|
||||||
|
<td><span className={`${styles.badge} ${styles.statusActive}`}>Completed</span></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
338
src/app/dashboard/layout.tsx
Normal file
@@ -0,0 +1,338 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import Image from 'next/image';
|
||||||
|
import { useRouter, usePathname } from 'next/navigation';
|
||||||
|
import { useLanguage } from '@/context/LanguageContext';
|
||||||
|
import styles from './dashboard.module.css';
|
||||||
|
|
||||||
|
export default function DashboardLayout({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
const router = useRouter();
|
||||||
|
const pathname = usePathname();
|
||||||
|
const { currentLanguage, setLanguage, t } = useLanguage();
|
||||||
|
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
||||||
|
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||||
|
const [accountsExpanded, setAccountsExpanded] = useState(false);
|
||||||
|
const [docsExpanded, setDocsExpanded] = useState(false);
|
||||||
|
const [crmExpanded, setCrmExpanded] = useState(false);
|
||||||
|
const [legalExpanded, setLegalExpanded] = useState(false);
|
||||||
|
const [hamExpanded, setHamExpanded] = useState(false);
|
||||||
|
const [appointmentsExpanded, setAppointmentsExpanded] = useState(false);
|
||||||
|
const [user] = useState({ name: 'Vikram Singh', role: 'Founder', avatar: 'VS' });
|
||||||
|
const [currentCompany] = useState({ name: 'TechFlow Solutions', logo: '/logo_symbol.png' });
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
// Clear any stored authentication data
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
localStorage.removeItem('authToken');
|
||||||
|
localStorage.removeItem('user');
|
||||||
|
sessionStorage.clear();
|
||||||
|
}
|
||||||
|
// Redirect to main page
|
||||||
|
router.push('/');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.dashboardContainer}>
|
||||||
|
|
||||||
|
{/* Top Navbar */}
|
||||||
|
<nav className={styles.topNavbar}>
|
||||||
|
|
||||||
|
{/* Left: User Profile */}
|
||||||
|
<div className={styles.navLeft}>
|
||||||
|
{/* Mobile Menu Button */}
|
||||||
|
<button
|
||||||
|
className={styles.mobileMenuBtn}
|
||||||
|
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
||||||
|
>
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className={styles.userProfile}>
|
||||||
|
<div className={styles.userAvatar}>{user.avatar}</div>
|
||||||
|
<div>
|
||||||
|
<div className={styles.userName}>{user.name}</div>
|
||||||
|
<div className={styles.userRole}>{user.role}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Center: Company Switcher */}
|
||||||
|
<div className={styles.navCenter}>
|
||||||
|
<div className={styles.companySwitcher}>
|
||||||
|
<Image
|
||||||
|
src={currentCompany.logo}
|
||||||
|
alt="Company Logo"
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
className={styles.companyLogo}
|
||||||
|
/>
|
||||||
|
<span className={styles.companyName}>{currentCompany.name}</span>
|
||||||
|
<span className={styles.dropdownArrow}>▼</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right: Language Selector & Onboarding Link */}
|
||||||
|
<div className={styles.navRight}>
|
||||||
|
{/* Language Selector */}
|
||||||
|
<select
|
||||||
|
className={styles.languageSelector}
|
||||||
|
value={currentLanguage}
|
||||||
|
onChange={(e) => setLanguage(e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="en">🇬🇧 English</option>
|
||||||
|
<option value="hi">🇮🇳 हिंदी</option>
|
||||||
|
<option value="bn">🇮🇳 বাংলা</option>
|
||||||
|
<option value="te">🇮🇳 తెలుగు</option>
|
||||||
|
<option value="mr">🇮🇳 मराठी</option>
|
||||||
|
<option value="ta">🇮🇳 தமிழ்</option>
|
||||||
|
<option value="gu">🇮🇳 ગુજરાતી</option>
|
||||||
|
<option value="kn">🇮🇳 ಕನ್ನಡ</option>
|
||||||
|
<option value="ml">🇮🇳 മലയാളം</option>
|
||||||
|
<option value="pa">🇮🇳 ਪੰਜਾਬੀ</option>
|
||||||
|
<option value="or">🇮🇳 ଓଡ଼ିଆ</option>
|
||||||
|
<option value="as">🇮🇳 অসমীয়া</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<Link href="/onboarding" className={styles.onboardingLink}>
|
||||||
|
<span>+</span>
|
||||||
|
<span>New / Join</span>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{/* Main Layout Area */}
|
||||||
|
<div className={styles.mainLayout}>
|
||||||
|
|
||||||
|
{/* Left Sidebar (Minimizable) */}
|
||||||
|
<aside className={`${styles.leftSidebar} ${sidebarCollapsed ? styles.collapsed : ''} ${mobileMenuOpen ? styles.mobileOpen : ''}`}>
|
||||||
|
<button
|
||||||
|
className={styles.toggleBtn}
|
||||||
|
onClick={() => setSidebarCollapsed(!sidebarCollapsed)}
|
||||||
|
>
|
||||||
|
{sidebarCollapsed ? '»' : '«'}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<nav className={styles.sidebarMenu}>
|
||||||
|
<Link href="/dashboard" className={`${styles.menuItem} ${pathname === '/dashboard' ? styles.active : ''}`}>
|
||||||
|
<span className={styles.menuIcon}>📊</span>
|
||||||
|
{!sidebarCollapsed && <span>{t('nav.dashboard')}</span>}
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<div style={{ margin: '1rem 0', borderTop: '1px solid rgba(255,255,255,0.1)' }}></div>
|
||||||
|
|
||||||
|
{/* Accounts Section */}
|
||||||
|
<div
|
||||||
|
className={`${styles.menuItem} ${styles.hasSubmenu} ${accountsExpanded ? styles.expanded : ''}`}
|
||||||
|
onClick={() => !sidebarCollapsed && setAccountsExpanded(!accountsExpanded)}
|
||||||
|
>
|
||||||
|
<span className={styles.menuIcon}>📒</span>
|
||||||
|
{!sidebarCollapsed && (
|
||||||
|
<>
|
||||||
|
<span>{t('nav.accounts')}</span>
|
||||||
|
<span className={styles.expandIcon}>▼</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{/* Submenu */}
|
||||||
|
<div className={`${styles.subMenu} ${accountsExpanded && !sidebarCollapsed ? styles.expanded : ''}`}>
|
||||||
|
<Link href="/dashboard/accounts" className={`${styles.subMenuItem} ${pathname === '/dashboard/accounts' ? styles.active : ''}`}>
|
||||||
|
<span>•</span> {t('accounts.overview')}
|
||||||
|
</Link>
|
||||||
|
<Link href="/dashboard/accounts/invoices" className={`${styles.subMenuItem} ${pathname === '/dashboard/accounts/invoices' ? styles.active : ''}`}>
|
||||||
|
<span>•</span> {t('accounts.invoices')}
|
||||||
|
</Link>
|
||||||
|
<Link href="/dashboard/accounts/journal" className={`${styles.subMenuItem} ${pathname === '/dashboard/accounts/journal' ? styles.active : ''}`}>
|
||||||
|
<span>•</span> {t('accounts.journal')}
|
||||||
|
</Link>
|
||||||
|
<Link href="/dashboard/accounts/ledger" className={`${styles.subMenuItem} ${pathname === '/dashboard/accounts/ledger' ? styles.active : ''}`}>
|
||||||
|
<span>•</span> {t('accounts.ledger')}
|
||||||
|
</Link>
|
||||||
|
<Link href="/dashboard/accounts/reports" className={`${styles.subMenuItem} ${pathname === '/dashboard/accounts/reports' ? styles.active : ''}`}>
|
||||||
|
<span>•</span> {t('accounts.reports')}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* CRM Section */}
|
||||||
|
<div
|
||||||
|
className={`${styles.menuItem} ${styles.hasSubmenu} ${crmExpanded ? styles.expanded : ''}`}
|
||||||
|
onClick={() => !sidebarCollapsed && setCrmExpanded(!crmExpanded)}
|
||||||
|
>
|
||||||
|
<span className={styles.menuIcon}>🎯</span>
|
||||||
|
{!sidebarCollapsed && (
|
||||||
|
<>
|
||||||
|
<span>{t('nav.crm')}</span>
|
||||||
|
<span className={styles.expandIcon}>▼</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{/* Submenu */}
|
||||||
|
<div className={`${styles.subMenu} ${crmExpanded && !sidebarCollapsed ? styles.expanded : ''}`}>
|
||||||
|
<Link href="/dashboard/crm" className={`${styles.subMenuItem} ${pathname === '/dashboard/crm' ? styles.active : ''}`}>
|
||||||
|
<span>•</span> {t('crm.dashboard')}
|
||||||
|
</Link>
|
||||||
|
<Link href="/dashboard/crm/leads" className={`${styles.subMenuItem} ${pathname === '/dashboard/crm/leads' ? styles.active : ''}`}>
|
||||||
|
<span>•</span> {t('crm.leads')}
|
||||||
|
</Link>
|
||||||
|
<Link href="/dashboard/crm/opportunities" className={`${styles.subMenuItem} ${pathname === '/dashboard/crm/opportunities' ? styles.active : ''}`}>
|
||||||
|
<span>•</span> {t('crm.opportunities')}
|
||||||
|
</Link>
|
||||||
|
<Link href="/dashboard/crm/quotes" className={`${styles.subMenuItem} ${pathname === '/dashboard/crm/quotes' ? styles.active : ''}`}>
|
||||||
|
<span>•</span> {t('crm.quotes')}
|
||||||
|
</Link>
|
||||||
|
<Link href="/dashboard/crm/analytics" className={`${styles.subMenuItem} ${pathname === '/dashboard/crm/analytics' ? styles.active : ''}`}>
|
||||||
|
<span>•</span> {t('crm.analytics')}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* HAM Section */}
|
||||||
|
<div
|
||||||
|
className={`${styles.menuItem} ${styles.hasSubmenu} ${hamExpanded ? styles.expanded : ''}`}
|
||||||
|
onClick={() => !sidebarCollapsed && setHamExpanded(!hamExpanded)}
|
||||||
|
>
|
||||||
|
<span className={styles.menuIcon}>👤</span>
|
||||||
|
{!sidebarCollapsed && (
|
||||||
|
<>
|
||||||
|
<span>{t('nav.ham')}</span>
|
||||||
|
<span className={styles.expandIcon}>▼</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{/* Submenu */}
|
||||||
|
<div className={`${styles.subMenu} ${hamExpanded && !sidebarCollapsed ? styles.expanded : ''}`}>
|
||||||
|
<Link href="/dashboard/ham" className={`${styles.subMenuItem} ${pathname === '/dashboard/ham' ? styles.active : ''}`}>
|
||||||
|
<span>•</span> {t('ham.dashboard')}
|
||||||
|
</Link>
|
||||||
|
<Link href="/dashboard/ham/employees" className={`${styles.subMenuItem} ${pathname === '/dashboard/ham/employees' ? styles.active : ''}`}>
|
||||||
|
<span>•</span> {t('ham.employees')}
|
||||||
|
</Link>
|
||||||
|
<Link href="/dashboard/ham/attendance" className={`${styles.subMenuItem} ${pathname === '/dashboard/ham/attendance' ? styles.active : ''}`}>
|
||||||
|
<span>•</span> {t('ham.attendance')}
|
||||||
|
</Link>
|
||||||
|
<Link href="/dashboard/ham/leave" className={`${styles.subMenuItem} ${pathname === '/dashboard/ham/leave' ? styles.active : ''}`}>
|
||||||
|
<span>•</span> {t('ham.leave')}
|
||||||
|
</Link>
|
||||||
|
<Link href="/dashboard/ham/performance" className={`${styles.subMenuItem} ${pathname === '/dashboard/ham/performance' ? styles.active : ''}`}>
|
||||||
|
<span>•</span> {t('ham.performance')}
|
||||||
|
</Link>
|
||||||
|
<Link href="/dashboard/ham/payroll" className={`${styles.subMenuItem} ${pathname === '/dashboard/ham/payroll' ? styles.active : ''}`}>
|
||||||
|
<span>•</span> {t('ham.payroll')}
|
||||||
|
</Link>
|
||||||
|
<Link href="/dashboard/ham/analytics" className={`${styles.subMenuItem} ${pathname === '/dashboard/ham/analytics' ? styles.active : ''}`}>
|
||||||
|
<span>•</span> {t('ham.analytics')}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Appointments Section */}
|
||||||
|
<div
|
||||||
|
className={`${styles.menuItem} ${styles.hasSubmenu} ${appointmentsExpanded ? styles.expanded : ''}`}
|
||||||
|
onClick={() => !sidebarCollapsed && setAppointmentsExpanded(!appointmentsExpanded)}
|
||||||
|
>
|
||||||
|
<span className={styles.menuIcon}>🗓️</span>
|
||||||
|
{!sidebarCollapsed && (
|
||||||
|
<>
|
||||||
|
<span>{t('nav.appointments')}</span>
|
||||||
|
<span className={styles.expandIcon}>▼</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{/* Submenu */}
|
||||||
|
<div className={`${styles.subMenu} ${appointmentsExpanded && !sidebarCollapsed ? styles.expanded : ''}`}>
|
||||||
|
<Link href="/dashboard/appointments" className={`${styles.subMenuItem} ${pathname === '/dashboard/appointments' ? styles.active : ''}`}>
|
||||||
|
<span>•</span> {t('appointments.dashboard')}
|
||||||
|
</Link>
|
||||||
|
<Link href="/dashboard/appointments/book" className={`${styles.subMenuItem} ${pathname === '/dashboard/appointments/book' ? styles.active : ''}`}>
|
||||||
|
<span>•</span> {t('appointments.book')}
|
||||||
|
</Link>
|
||||||
|
<Link href="/dashboard/appointments/my-appointments" className={`${styles.subMenuItem} ${pathname === '/dashboard/appointments/my-appointments' ? styles.active : ''}`}>
|
||||||
|
<span>•</span> {t('appointments.myAppointments')}
|
||||||
|
</Link>
|
||||||
|
<Link href="/dashboard/appointments/calendar" className={`${styles.subMenuItem} ${pathname === '/dashboard/appointments/calendar' ? styles.active : ''}`}>
|
||||||
|
<span>•</span> {t('appointments.calendar')}
|
||||||
|
</Link>
|
||||||
|
<Link href="/dashboard/appointments/types" className={`${styles.subMenuItem} ${pathname === '/dashboard/appointments/types' ? styles.active : ''}`}>
|
||||||
|
<span>•</span> {t('appointments.types')}
|
||||||
|
</Link>
|
||||||
|
<Link href="/dashboard/appointments/settings" className={`${styles.subMenuItem} ${pathname === '/dashboard/appointments/settings' ? styles.active : ''}`}>
|
||||||
|
<span>•</span> {t('appointments.settings')}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Link href="/dashboard/projects" className={`${styles.menuItem} ${pathname?.startsWith('/dashboard/projects') ? styles.active : ''}`}>
|
||||||
|
<span className={styles.menuIcon}>🚀</span>
|
||||||
|
{!sidebarCollapsed && <span>{t('nav.projects')}</span>}
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
{/* Documents Section */}
|
||||||
|
<div
|
||||||
|
className={`${styles.menuItem} ${styles.hasSubmenu} ${docsExpanded ? styles.expanded : ''}`}
|
||||||
|
onClick={() => !sidebarCollapsed && setDocsExpanded(!docsExpanded)}
|
||||||
|
>
|
||||||
|
<span className={styles.menuIcon}>📄</span>
|
||||||
|
{!sidebarCollapsed && (
|
||||||
|
<>
|
||||||
|
<span>{t('nav.documents')}</span>
|
||||||
|
<span className={styles.expandIcon}>▼</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{/* Submenu */}
|
||||||
|
<div className={`${styles.subMenu} ${docsExpanded && !sidebarCollapsed ? styles.expanded : ''}`}>
|
||||||
|
<Link href="/dashboard/documents" className={`${styles.subMenuItem} ${pathname === '/dashboard/documents' ? styles.active : ''}`}>
|
||||||
|
<span>•</span> {t('documents.dashboard')}
|
||||||
|
</Link>
|
||||||
|
<Link href="/dashboard/documents/all" className={`${styles.subMenuItem} ${pathname === '/dashboard/documents/all' ? styles.active : ''}`}>
|
||||||
|
<span>•</span> {t('documents.allFiles')}
|
||||||
|
</Link>
|
||||||
|
<Link href="/dashboard/documents/upload" className={`${styles.subMenuItem} ${pathname === '/dashboard/documents/upload' ? styles.active : ''}`}>
|
||||||
|
<span>•</span> {t('documents.upload')}
|
||||||
|
</Link>
|
||||||
|
<div style={{ margin: '0.5rem 0', borderTop: '1px solid rgba(255,215,0,0.2)' }}></div>
|
||||||
|
<Link href="/dashboard/documents/sign" className={`${styles.subMenuItem} ${pathname?.startsWith('/dashboard/documents/sign') ? styles.active : ''}`}>
|
||||||
|
<span>•</span> {t('documents.sign')}
|
||||||
|
</Link>
|
||||||
|
<Link href="/dashboard/documents/templates" className={`${styles.subMenuItem} ${pathname === '/dashboard/documents/templates' ? styles.active : ''}`}>
|
||||||
|
<span>•</span> {t('documents.templates')}
|
||||||
|
</Link>
|
||||||
|
<Link href="/dashboard/documents/activity" className={`${styles.subMenuItem} ${pathname === '/dashboard/documents/activity' ? styles.active : ''}`}>
|
||||||
|
<span>•</span> {t('documents.activity')}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<div style={{ margin: '1rem 0', borderTop: '1px solid rgba(255,255,255,0.1)' }}></div>
|
||||||
|
<a href="#" className={styles.menuItem}>
|
||||||
|
<span className={styles.menuIcon}>⚙️</span>
|
||||||
|
{!sidebarCollapsed && <span>{t('nav.settings')}</span>}
|
||||||
|
</a>
|
||||||
|
<button
|
||||||
|
onClick={handleLogout}
|
||||||
|
className={styles.menuItem}
|
||||||
|
style={{
|
||||||
|
background: 'none',
|
||||||
|
border: 'none',
|
||||||
|
width: '100%',
|
||||||
|
cursor: 'pointer',
|
||||||
|
color: '#ff6b6b'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className={styles.menuIcon}>🚪</span>
|
||||||
|
{!sidebarCollapsed && <span>{t('nav.logout')}</span>}
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
{/* Main Content Area (Right aligned) */}
|
||||||
|
<main className={styles.contentArea}>
|
||||||
|
{children}
|
||||||
|
</main>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
92
src/app/dashboard/page.tsx
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useLanguage } from '@/context/LanguageContext';
|
||||||
|
import styles from './dashboard.module.css';
|
||||||
|
|
||||||
|
export default function Dashboard() {
|
||||||
|
const { t } = useLanguage();
|
||||||
|
// In a real app, user data would come from an auth context
|
||||||
|
const [user] = useState({ name: 'Vikram Singh', role: 'Founder' });
|
||||||
|
|
||||||
|
// Mock Data for Dashboard - using translations
|
||||||
|
const stats = [
|
||||||
|
{ label: t('dashboard.stats.totalRevenue'), value: '₹45.2L', change: '+12.5%', isPositive: true },
|
||||||
|
{ label: t('dashboard.stats.activeProjects'), value: '12', change: '+2', isPositive: true },
|
||||||
|
{ label: t('dashboard.stats.teamSize'), value: '24', change: '+4', isPositive: true },
|
||||||
|
{ label: t('dashboard.stats.pendingTasks'), value: '8', change: '-2', isPositive: true },
|
||||||
|
];
|
||||||
|
|
||||||
|
const recentActivities = [
|
||||||
|
{ title: 'New contract signed with Apex Corp', time: '2 hours ago' },
|
||||||
|
{ title: 'Project "Alpha" phase 1 completed', time: '5 hours ago' },
|
||||||
|
{ title: 'New team member joined: Sarah Jones', time: '1 day ago' }
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.dashboardContainer}>
|
||||||
|
{/* Header Section */}
|
||||||
|
<div className={styles.sectionHeader}>
|
||||||
|
<div>
|
||||||
|
<h1 className={styles.pageTitle}>{t('dashboard.title')}</h1>
|
||||||
|
<p className={styles.pageSubtitle}>{t('dashboard.welcome')}, {user.name}</p>
|
||||||
|
</div>
|
||||||
|
<div className={styles.headerActions}>
|
||||||
|
<button className={styles.btnPrimary}>{t('dashboard.actions.createInvoice')}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Stats Grid */}
|
||||||
|
<div className={styles.statsGrid}>
|
||||||
|
{stats.map((stat, index) => (
|
||||||
|
<div key={index} className={styles.glassCard}>
|
||||||
|
<div className={styles.statLabel}>{stat.label}</div>
|
||||||
|
<div className={styles.statValue} style={{ color: index === 0 ? 'var(--color-gold)' : index === 2 ? 'var(--color-cyan)' : 'inherit' }}>
|
||||||
|
{stat.value}
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: '0.8rem', color: stat.isPositive ? '#4caf50' : '#f44336' }}>
|
||||||
|
{stat.isPositive ? '▲' : '▼'} {stat.change} {t('dashboard.stats.sinceLastMonth')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Content Grid */}
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: '2fr 1fr', gap: '1.5rem', marginTop: '2rem' }}>
|
||||||
|
{/* Recent Activity */}
|
||||||
|
<div className={styles.glassCard}>
|
||||||
|
<h3 style={{ marginBottom: '1rem', borderBottom: '1px solid rgba(255,255,255,0.1)', paddingBottom: '0.5rem' }}>
|
||||||
|
{t('dashboard.sections.recentActivity')}
|
||||||
|
</h3>
|
||||||
|
{recentActivities.map((activity, i) => (
|
||||||
|
<div key={i} style={{ display: 'flex', gap: '1rem', marginBottom: '1rem', alignItems: 'center' }}>
|
||||||
|
<div style={{ width: '8px', height: '8px', borderRadius: '50%', background: 'var(--color-gold)' }}></div>
|
||||||
|
<div>
|
||||||
|
<div style={{ fontSize: '0.95rem' }}>{activity.title}</div>
|
||||||
|
<div style={{ fontSize: '0.8rem', color: 'rgba(255,255,255,0.5)' }}>{activity.time}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Quick Actions */}
|
||||||
|
<div className={styles.glassCard}>
|
||||||
|
<h3 style={{ marginBottom: '1rem', borderBottom: '1px solid rgba(255,255,255,0.1)', paddingBottom: '0.5rem' }}>
|
||||||
|
{t('dashboard.sections.quickActions')}
|
||||||
|
</h3>
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.8rem' }}>
|
||||||
|
<button style={{ background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', padding: '0.8rem', borderRadius: '8px', color: '#fff', textAlign: 'left', cursor: 'pointer' }}>
|
||||||
|
+ {t('dashboard.actions.addLead')}
|
||||||
|
</button>
|
||||||
|
<button style={{ background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', padding: '0.8rem', borderRadius: '8px', color: '#fff', textAlign: 'left', cursor: 'pointer' }}>
|
||||||
|
{t('dashboard.actions.bookAppointment')}
|
||||||
|
</button>
|
||||||
|
<button style={{ background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', padding: '0.8rem', borderRadius: '8px', color: '#fff', textAlign: 'left', cursor: 'pointer' }}>
|
||||||
|
{t('dashboard.actions.addEmployee')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
188
src/app/dashboard/projects/[id]/closure/page.tsx
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
'use client';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import styles from '../../projects.module.css';
|
||||||
|
import { useToast } from '@/context/ToastContext';
|
||||||
|
|
||||||
|
export default function ClosurePage() {
|
||||||
|
const { showToast } = useToast();
|
||||||
|
const [isPreviewOpen, setIsPreviewOpen] = useState(false);
|
||||||
|
const [isClosureConfirmed, setIsClosureConfirmed] = useState(false);
|
||||||
|
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
|
||||||
|
|
||||||
|
const handlePreview = () => {
|
||||||
|
setIsPreviewOpen(true);
|
||||||
|
showToast('Opening closure report preview...', 'info');
|
||||||
|
setTimeout(() => setIsPreviewOpen(false), 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDownloadPDF = () => {
|
||||||
|
showToast('Downloading: Alpha Redesign_Closure.pdf', 'success');
|
||||||
|
// Simulate download
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = '#'; // Would be actual PDF blob URL
|
||||||
|
link.download = 'Alpha_Redesign_Closure.pdf';
|
||||||
|
// link.click(); // Commented out to avoid actual download in demo
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirmClosure = () => {
|
||||||
|
setShowConfirmDialog(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmClosureAction = () => {
|
||||||
|
setIsClosureConfirmed(true);
|
||||||
|
setShowConfirmDialog(false);
|
||||||
|
showToast('✅ Project closure confirmed! The project has been archived.', 'success');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ animation: 'fadeIn 0.5s ease-out' }}>
|
||||||
|
{/* Confirmation Dialog */}
|
||||||
|
{showConfirmDialog && (
|
||||||
|
<div style={{
|
||||||
|
position: 'fixed',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
background: 'rgba(0,0,0,0.7)',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
zIndex: 9999,
|
||||||
|
backdropFilter: 'blur(4px)'
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
|
background: 'linear-gradient(135deg, rgba(20,25,35,0.95) 0%, rgba(30,35,45,0.95) 100%)',
|
||||||
|
border: '1px solid rgba(255,215,0,0.3)',
|
||||||
|
borderRadius: '12px',
|
||||||
|
padding: '2rem',
|
||||||
|
maxWidth: '400px',
|
||||||
|
boxShadow: '0 20px 60px rgba(0,0,0,0.5)',
|
||||||
|
animation: 'fadeIn 0.3s ease-out'
|
||||||
|
}}>
|
||||||
|
<h3 style={{ margin: '0 0 1rem 0', color: '#ffd700', fontSize: '1.3rem' }}>Confirm Project Closure</h3>
|
||||||
|
<p style={{ margin: '0 0 1.5rem 0', color: '#ccc', lineHeight: '1.6' }}>
|
||||||
|
Are you sure you want to confirm project closure? This action cannot be undone.
|
||||||
|
</p>
|
||||||
|
<div style={{ display: 'flex', gap: '1rem', justifyContent: 'flex-end' }}>
|
||||||
|
<button
|
||||||
|
className={styles.btnSecondary}
|
||||||
|
onClick={() => setShowConfirmDialog(false)}
|
||||||
|
style={{ padding: '0.6rem 1.5rem' }}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={styles.btnPrimary}
|
||||||
|
onClick={confirmClosureAction}
|
||||||
|
style={{ background: '#00ff9d', color: '#000', padding: '0.6rem 1.5rem' }}
|
||||||
|
>
|
||||||
|
Confirm
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className={styles.sectionHeader}>
|
||||||
|
<div>
|
||||||
|
<h2 className={styles.pageTitle}>Project Closure</h2>
|
||||||
|
<p className={styles.pageSubtitle}>Finalize deliverables, document learnings, and archive.</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className={styles.btnPrimary}
|
||||||
|
style={{
|
||||||
|
background: isClosureConfirmed ? '#888' : '#00ff9d',
|
||||||
|
color: '#000',
|
||||||
|
cursor: isClosureConfirmed ? 'not-allowed' : 'pointer'
|
||||||
|
}}
|
||||||
|
onClick={handleConfirmClosure}
|
||||||
|
disabled={isClosureConfirmed}
|
||||||
|
>
|
||||||
|
{isClosureConfirmed ? '✓ Closure Confirmed' : 'Confirm Closure'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '2rem' }}>
|
||||||
|
|
||||||
|
{/* 7.1 Closure Checklist */}
|
||||||
|
<div className={styles.section}>
|
||||||
|
<h3 className={styles.sectionTitle}>✅ Closure Checklist</h3>
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.8rem' }}>
|
||||||
|
<label style={{ display: 'flex', alignItems: 'center', gap: '0.8rem', cursor: 'pointer', padding: '0.5rem', background: 'rgba(255,255,255,0.03)', borderRadius: '6px' }}>
|
||||||
|
<input type="checkbox" defaultChecked />
|
||||||
|
<span style={{ color: '#ddd' }}>All Deliverables Accepted</span>
|
||||||
|
</label>
|
||||||
|
<label style={{ display: 'flex', alignItems: 'center', gap: '0.8rem', cursor: 'pointer', padding: '0.5rem', background: 'rgba(255,255,255,0.03)', borderRadius: '6px' }}>
|
||||||
|
<input type="checkbox" defaultChecked />
|
||||||
|
<span style={{ color: '#ddd' }}>Final Invoice Sent</span>
|
||||||
|
</label>
|
||||||
|
<label style={{ display: 'flex', alignItems: 'center', gap: '0.8rem', cursor: 'pointer', padding: '0.5rem', background: 'rgba(255,255,255,0.03)', borderRadius: '6px' }}>
|
||||||
|
<input type="checkbox" />
|
||||||
|
<span style={{ color: '#ddd' }}>Vendor Contracts Terminated</span>
|
||||||
|
</label>
|
||||||
|
<label style={{ display: 'flex', alignItems: 'center', gap: '0.8rem', cursor: 'pointer', padding: '0.5rem', background: 'rgba(255,255,255,0.03)', borderRadius: '6px' }}>
|
||||||
|
<input type="checkbox" />
|
||||||
|
<span style={{ color: '#ddd' }}>Team Released</span>
|
||||||
|
</label>
|
||||||
|
<label style={{ display: 'flex', alignItems: 'center', gap: '0.8rem', cursor: 'pointer', padding: '0.5rem', background: 'rgba(255,255,255,0.03)', borderRadius: '6px' }}>
|
||||||
|
<input type="checkbox" />
|
||||||
|
<span style={{ color: '#ddd' }}>Archived Documentation</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 7.3 Final Report */}
|
||||||
|
<div className={styles.section}>
|
||||||
|
<h3 className={styles.sectionTitle}>📄 Final Project Report</h3>
|
||||||
|
<div style={{ background: 'rgba(255,255,255,0.05)', padding: '1.5rem', borderRadius: '8px', textAlign: 'center', marginBottom: '1.5rem' }}>
|
||||||
|
<div style={{ fontSize: '3rem', marginBottom: '0.5rem' }}>📑</div>
|
||||||
|
<h4 style={{ margin: '0 0 0.5rem 0', color: '#fff' }}>Alpha Redesign_Closure.pdf</h4>
|
||||||
|
<div style={{ color: '#888', fontSize: '0.9rem' }}>Generated on Dec 20, 2025</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem' }}>
|
||||||
|
<button
|
||||||
|
className={styles.btnSecondary}
|
||||||
|
style={{ justifyContent: 'center' }}
|
||||||
|
onClick={handlePreview}
|
||||||
|
>
|
||||||
|
👁️ Preview
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={styles.btnPrimary}
|
||||||
|
style={{ justifyContent: 'center' }}
|
||||||
|
onClick={handleDownloadPDF}
|
||||||
|
>
|
||||||
|
⬇️ Download PDF
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 7.2 Lessons Learned */}
|
||||||
|
<div className={styles.section} style={{ gridColumn: 'span 2' }}>
|
||||||
|
<h3 className={styles.sectionTitle}>🧠 Lessons Learned Register</h3>
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: '1.5rem' }}>
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<label className={styles.label} style={{ color: '#00ff9d' }}>What went well?</label>
|
||||||
|
<textarea className={styles.readonlyField} rows={4} style={{ width: '100%', background: 'rgba(0,0,0,0.3)' }} defaultValue="- Daily standups improved communication.
|
||||||
|
- React components were highly reusable." />
|
||||||
|
</div>
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<label className={styles.label} style={{ color: '#ff4444' }}>What didn't go well?</label>
|
||||||
|
<textarea className={styles.readonlyField} rows={4} style={{ width: '100%', background: 'rgba(0,0,0,0.3)' }} defaultValue="- Vendor delay caused 1 week slippage.
|
||||||
|
- Scope creep in mobile views." />
|
||||||
|
</div>
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<label className={styles.label} style={{ color: 'var(--color-gold)' }}>Recommendations</label>
|
||||||
|
<textarea className={styles.readonlyField} rows={4} style={{ width: '100%', background: 'rgba(0,0,0,0.3)' }} defaultValue="- Include penalty clauses for vendors.
|
||||||
|
- Freeze design before coding starts." />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
257
src/app/dashboard/projects/[id]/control/page.tsx
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
'use client';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import styles from '../../projects.module.css';
|
||||||
|
|
||||||
|
// Mock Data
|
||||||
|
const MOCK_CHANGE_REQUESTS = [
|
||||||
|
{ id: 'CR-101', title: 'Add Dark Mode to Dashboard', requestor: 'Rohan (UX)', impact: 'Medium', status: 'pending' },
|
||||||
|
{ id: 'CR-102', title: 'Integrate Stripe Payments', requestor: 'Amit (Sales)', impact: 'High', status: 'approved' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const MOCK_ISSUES = [
|
||||||
|
{ id: 'ISS-201', title: 'Login API Timeout', reportedBy: 'System', severity: 'High', status: 'open' },
|
||||||
|
{ id: 'ISS-202', title: 'Logo Alignment on Mobile', reportedBy: 'Anjali (QA)', severity: 'Low', status: 'resolved' }
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function ControlPage() {
|
||||||
|
const [changeRequests, setChangeRequests] = useState(MOCK_CHANGE_REQUESTS);
|
||||||
|
const [issues, setIssues] = useState(MOCK_ISSUES);
|
||||||
|
|
||||||
|
// State for modals
|
||||||
|
const [isAddingCR, setIsAddingCR] = useState(false);
|
||||||
|
const [isLoggingIssue, setIsLoggingIssue] = useState(false);
|
||||||
|
|
||||||
|
// Form State
|
||||||
|
const [newCR, setNewCR] = useState({ title: '', requestor: '', impact: 'Low' });
|
||||||
|
const [newIssue, setNewIssue] = useState({ title: '', reportedBy: '', severity: 'Low' });
|
||||||
|
|
||||||
|
const handleAddCR = () => {
|
||||||
|
if (!newCR.title) return;
|
||||||
|
setChangeRequests([...changeRequests, {
|
||||||
|
id: `CR-${Math.floor(Math.random() * 1000)}`,
|
||||||
|
title: newCR.title,
|
||||||
|
requestor: newCR.requestor || 'Me',
|
||||||
|
impact: newCR.impact,
|
||||||
|
status: 'pending'
|
||||||
|
}]);
|
||||||
|
setNewCR({ title: '', requestor: '', impact: 'Low' });
|
||||||
|
setIsAddingCR(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLogIssue = () => {
|
||||||
|
if (!newIssue.title) return;
|
||||||
|
setIssues([...issues, {
|
||||||
|
id: `ISS-${Math.floor(Math.random() * 1000)}`,
|
||||||
|
title: newIssue.title,
|
||||||
|
reportedBy: newIssue.reportedBy || 'Me',
|
||||||
|
severity: newIssue.severity,
|
||||||
|
status: 'open'
|
||||||
|
}]);
|
||||||
|
setNewIssue({ title: '', reportedBy: '', severity: 'Low' });
|
||||||
|
setIsLoggingIssue(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ animation: 'fadeIn 0.5s ease-out' }}>
|
||||||
|
<div className={styles.sectionHeader}>
|
||||||
|
<div>
|
||||||
|
<h2 className={styles.pageTitle}>Monitoring & Control</h2>
|
||||||
|
<p className={styles.pageSubtitle}>Performance tracking and change management.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '2rem' }}>
|
||||||
|
|
||||||
|
{/* 6.1 Performance Dashboard */}
|
||||||
|
<div className={styles.section} style={{ gridColumn: 'span 2' }}>
|
||||||
|
<h3 className={styles.sectionTitle}>📊 Performance Indices</h3>
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: '1.5rem' }}>
|
||||||
|
<div style={{ background: 'rgba(0,0,0,0.2)', padding: '1rem', borderRadius: '8px', textAlign: 'center' }}>
|
||||||
|
<div style={{ fontSize: '0.8rem', color: '#888', marginBottom: '0.5rem' }}>SPI (Schedule)</div>
|
||||||
|
<div style={{ fontSize: '2rem', fontWeight: 700, color: '#00ff9d' }}>1.0</div>
|
||||||
|
<div style={{ fontSize: '0.75rem', color: '#00ff9d' }}>On Track</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ background: 'rgba(0,0,0,0.2)', padding: '1rem', borderRadius: '8px', textAlign: 'center' }}>
|
||||||
|
<div style={{ fontSize: '0.8rem', color: '#888', marginBottom: '0.5rem' }}>CPI (Cost)</div>
|
||||||
|
<div style={{ fontSize: '2rem', fontWeight: 700, color: '#ff4444' }}>0.92</div>
|
||||||
|
<div style={{ fontSize: '0.75rem', color: '#ff4444' }}>Over Budget</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ background: 'rgba(0,0,0,0.2)', padding: '1rem', borderRadius: '8px', textAlign: 'center' }}>
|
||||||
|
<div style={{ fontSize: '0.8rem', color: '#888', marginBottom: '0.5rem' }}>Active Issues</div>
|
||||||
|
<div style={{ fontSize: '2rem', fontWeight: 700, color: '#ffd700' }}>{issues.filter(i => i.status !== 'resolved').length}</div>
|
||||||
|
<div style={{ fontSize: '0.75rem', color: '#ffd700' }}>Need Attention</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ background: 'rgba(0,0,0,0.2)', padding: '1rem', borderRadius: '8px', textAlign: 'center' }}>
|
||||||
|
<div style={{ fontSize: '0.8rem', color: '#888', marginBottom: '0.5rem' }}>Change Requests</div>
|
||||||
|
<div style={{ fontSize: '2rem', fontWeight: 700, color: '#fff' }}>{changeRequests.filter(c => c.status === 'pending').length}</div>
|
||||||
|
<div style={{ fontSize: '0.75rem', color: '#888' }}>Pending Approval</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 6.2 Change Management */}
|
||||||
|
<div className={styles.section}>
|
||||||
|
<div className={styles.sectionHeader}>
|
||||||
|
<h3 className={styles.sectionTitle}>🔄 Change Requests</h3>
|
||||||
|
{!isAddingCR && (
|
||||||
|
<button
|
||||||
|
className={styles.btnPrimary}
|
||||||
|
style={{ fontSize: '0.85rem', padding: '0.4rem 1rem' }}
|
||||||
|
onClick={() => setIsAddingCR(true)}
|
||||||
|
>
|
||||||
|
+ New Request
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isAddingCR && (
|
||||||
|
<div style={{ background: 'rgba(255,255,255,0.05)', padding: '1rem', borderRadius: '8px', marginBottom: '1rem' }}>
|
||||||
|
<input
|
||||||
|
className={styles.inputField}
|
||||||
|
placeholder="Change Title"
|
||||||
|
style={{ marginBottom: '0.5rem' }}
|
||||||
|
value={newCR.title}
|
||||||
|
onChange={e => setNewCR({ ...newCR, title: e.target.value })}
|
||||||
|
/>
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem', marginBottom: '1rem' }}>
|
||||||
|
<input
|
||||||
|
className={styles.inputField}
|
||||||
|
placeholder="Requestor Name"
|
||||||
|
value={newCR.requestor}
|
||||||
|
onChange={e => setNewCR({ ...newCR, requestor: e.target.value })}
|
||||||
|
/>
|
||||||
|
<select
|
||||||
|
className={styles.inputField}
|
||||||
|
value={newCR.impact}
|
||||||
|
onChange={e => setNewCR({ ...newCR, impact: e.target.value })}
|
||||||
|
>
|
||||||
|
<option value="Low">Low Impact</option>
|
||||||
|
<option value="Medium">Medium Impact</option>
|
||||||
|
<option value="High">High Impact</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', gap: '0.5rem', justifyContent: 'flex-end' }}>
|
||||||
|
<button className={styles.btnSecondary} onClick={() => setIsAddingCR(false)}>Cancel</button>
|
||||||
|
<button className={styles.btnPrimary} onClick={handleAddCR}>Submit Request</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<table className={styles.table}>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Title</th>
|
||||||
|
<th>Impact</th>
|
||||||
|
<th>Status</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{changeRequests.map(cr => (
|
||||||
|
<tr key={cr.id}>
|
||||||
|
<td style={{ fontSize: '0.85rem', color: '#888' }}>{cr.id}</td>
|
||||||
|
<td>{cr.title}</td>
|
||||||
|
<td><span style={{ color: cr.impact === 'High' ? '#ff4444' : cr.impact === 'Medium' ? 'orange' : '#ccc' }}>{cr.impact}</span></td>
|
||||||
|
<td>
|
||||||
|
<span className={styles.statusBadge} style={{
|
||||||
|
background: cr.status === 'approved' ? 'rgba(0,255,157,0.1)' : 'rgba(255,255,255,0.1)',
|
||||||
|
color: cr.status === 'approved' ? '#00ff9d' : '#ccc'
|
||||||
|
}}>
|
||||||
|
{cr.status}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 6.3 Issue Log */}
|
||||||
|
<div className={styles.section}>
|
||||||
|
<div className={styles.sectionHeader}>
|
||||||
|
<h3 className={styles.sectionTitle}>🐞 Issue Log</h3>
|
||||||
|
{!isLoggingIssue && (
|
||||||
|
<button
|
||||||
|
className={styles.btnPrimary}
|
||||||
|
style={{ fontSize: '0.85rem', padding: '0.4rem 1rem', background: '#ff4444', color: '#fff' }}
|
||||||
|
onClick={() => setIsLoggingIssue(true)}
|
||||||
|
>
|
||||||
|
! Log Issue
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isLoggingIssue && (
|
||||||
|
<div style={{ background: 'rgba(255,50,50,0.1)', padding: '1rem', borderRadius: '8px', marginBottom: '1rem', border: '1px solid rgba(255,50,50,0.3)' }}>
|
||||||
|
<input
|
||||||
|
className={styles.inputField}
|
||||||
|
placeholder="Issue Title"
|
||||||
|
style={{ marginBottom: '0.5rem' }}
|
||||||
|
value={newIssue.title}
|
||||||
|
onChange={e => setNewIssue({ ...newIssue, title: e.target.value })}
|
||||||
|
/>
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem', marginBottom: '1rem' }}>
|
||||||
|
<input
|
||||||
|
className={styles.inputField}
|
||||||
|
placeholder="Reported By"
|
||||||
|
value={newIssue.reportedBy}
|
||||||
|
onChange={e => setNewIssue({ ...newIssue, reportedBy: e.target.value })}
|
||||||
|
/>
|
||||||
|
<select
|
||||||
|
className={styles.inputField}
|
||||||
|
value={newIssue.severity}
|
||||||
|
onChange={e => setNewIssue({ ...newIssue, severity: e.target.value })}
|
||||||
|
>
|
||||||
|
<option value="Low">Low Severity</option>
|
||||||
|
<option value="Medium">Medium Severity</option>
|
||||||
|
<option value="High">High Severity</option>
|
||||||
|
<option value="Critical">Critical</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', gap: '0.5rem', justifyContent: 'flex-end' }}>
|
||||||
|
<button className={styles.btnSecondary} onClick={() => setIsLoggingIssue(false)}>Cancel</button>
|
||||||
|
<button className={styles.btnPrimary} style={{ background: '#ff4444', color: '#fff' }} onClick={handleLogIssue}>Log Issue</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<table className={styles.table}>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Issue</th>
|
||||||
|
<th>Severity</th>
|
||||||
|
<th>Status</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{issues.map(iss => (
|
||||||
|
<tr key={iss.id}>
|
||||||
|
<td style={{ fontSize: '0.85rem', color: '#888' }}>{iss.id}</td>
|
||||||
|
<td>{iss.title}</td>
|
||||||
|
<td>
|
||||||
|
<span style={{
|
||||||
|
color: iss.severity === 'High' ? '#ff4444' : iss.severity === 'Critical' ? '#ff0000' : '#ccc',
|
||||||
|
fontWeight: iss.severity === 'Critical' ? 'bold' : 'normal'
|
||||||
|
}}>
|
||||||
|
{iss.severity}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span className={styles.statusBadge} style={{
|
||||||
|
background: iss.status === 'resolved' ? 'rgba(0,255,157,0.1)' : 'rgba(255,100,100,0.1)',
|
||||||
|
color: iss.status === 'resolved' ? '#00ff9d' : '#ff8888'
|
||||||
|
}}>
|
||||||
|
{iss.status}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
344
src/app/dashboard/projects/[id]/execution/page.tsx
Normal file
@@ -0,0 +1,344 @@
|
|||||||
|
'use client';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import styles from '../../projects.module.css';
|
||||||
|
|
||||||
|
// Mock Data for Kanban
|
||||||
|
const MOCK_KANBAN_TASKS = [
|
||||||
|
{ id: 'MKT-102', title: 'Draft User Survey Questions', assignee: 'RM', status: 'todo', color: '#ff8888' },
|
||||||
|
{ id: 'MKT-104', title: 'Competitor Analysis', assignee: 'JS', status: 'todo', color: '#88ccff' },
|
||||||
|
{ id: 'DSN-201', title: 'Design Landing Page Mockups', assignee: 'AG', status: 'in-progress', color: '#ccff88' },
|
||||||
|
{ id: 'MGT-001', title: 'Kickoff Meeting', assignee: 'VS', status: 'completed', color: '#ddd' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const MOCK_EMPLOYEES = [
|
||||||
|
{ id: 'RM', name: 'Rohan Mehta', color: '#ff8888' },
|
||||||
|
{ id: 'JS', name: 'John Sharma', color: '#88ccff' },
|
||||||
|
{ id: 'AG', name: 'Anjali Gupta', color: '#ccff88' },
|
||||||
|
{ id: 'VS', name: 'Vikram Singh', color: '#ddd' },
|
||||||
|
{ id: 'NK', name: 'Neha Kapoor', color: '#ffcc88' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const INITIAL_COLUMNS = [
|
||||||
|
{ id: 'todo', title: 'To Do', color: '#ccc', bg: 'rgba(255,255,255,0.1)' },
|
||||||
|
{ id: 'in-progress', title: 'In Progress', color: 'var(--color-gold)', bg: 'rgba(255,215,0,0.2)' },
|
||||||
|
{ id: 'completed', title: 'Completed', color: '#00ff9d', bg: 'rgba(0,255,157,0.1)' }
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function ExecutionPage() {
|
||||||
|
const [viewMode, setViewMode] = useState<'kanban' | 'list'>('kanban');
|
||||||
|
const [tasks, setTasks] = useState(MOCK_KANBAN_TASKS);
|
||||||
|
const [columns, setColumns] = useState(INITIAL_COLUMNS);
|
||||||
|
const [draggedTaskId, setDraggedTaskId] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// State for adding new things
|
||||||
|
const [isAddingColumn, setIsAddingColumn] = useState(false);
|
||||||
|
const [newColumnTitle, setNewColumnTitle] = useState('');
|
||||||
|
|
||||||
|
// State for adding task (tracks which column is being added to)
|
||||||
|
const [addingTaskToCol, setAddingTaskToCol] = useState<string | null>(null);
|
||||||
|
const [newTaskTitle, setNewTaskTitle] = useState('');
|
||||||
|
const [newTaskCode, setNewTaskCode] = useState('');
|
||||||
|
const [newTaskAssignee, setNewTaskAssignee] = useState(MOCK_EMPLOYEES[0].id);
|
||||||
|
|
||||||
|
const handleDragStart = (e: React.DragEvent, taskId: string) => {
|
||||||
|
setDraggedTaskId(taskId);
|
||||||
|
e.dataTransfer.effectAllowed = "move";
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDragOver = (e: React.DragEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDrop = (e: React.DragEvent, status: string) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!draggedTaskId) return;
|
||||||
|
|
||||||
|
setTasks(prev => prev.map(t =>
|
||||||
|
t.id === draggedTaskId ? { ...t, status } : t
|
||||||
|
));
|
||||||
|
setDraggedTaskId(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddColumn = () => {
|
||||||
|
if (!newColumnTitle.trim()) return;
|
||||||
|
const newId = newColumnTitle.toLowerCase().replace(/\s+/g, '-');
|
||||||
|
setColumns([...columns, {
|
||||||
|
id: newId,
|
||||||
|
title: newColumnTitle,
|
||||||
|
color: '#fff',
|
||||||
|
bg: 'rgba(255,255,255,0.1)'
|
||||||
|
}]);
|
||||||
|
setNewColumnTitle('');
|
||||||
|
setIsAddingColumn(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddTask = (columnId: string) => {
|
||||||
|
if (!newTaskTitle.trim()) return;
|
||||||
|
|
||||||
|
// Use provided code or generate one if empty
|
||||||
|
const finalId = newTaskCode.trim() || `TSK-${Math.floor(Math.random() * 1000)}`;
|
||||||
|
const assignee = MOCK_EMPLOYEES.find(e => e.id === newTaskAssignee);
|
||||||
|
|
||||||
|
setTasks([...tasks, {
|
||||||
|
id: finalId,
|
||||||
|
title: newTaskTitle,
|
||||||
|
assignee: assignee ? assignee.id : '??',
|
||||||
|
status: columnId,
|
||||||
|
color: assignee ? assignee.color : '#fff'
|
||||||
|
}]);
|
||||||
|
|
||||||
|
// Reset form
|
||||||
|
setNewTaskTitle('');
|
||||||
|
setNewTaskCode('');
|
||||||
|
setNewTaskAssignee(MOCK_EMPLOYEES[0].id);
|
||||||
|
setAddingTaskToCol(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTasksByStatus = (status: string) => tasks.filter(t => t.status === status);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ animation: 'fadeIn 0.5s ease-out' }}>
|
||||||
|
<div className={styles.sectionHeader}>
|
||||||
|
<div>
|
||||||
|
<h2 className={styles.pageTitle}>Execution Phase</h2>
|
||||||
|
<p className={styles.pageSubtitle}>Track tasks, stand-ups, and team communication.</p>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', gap: '0.5rem', background: 'rgba(0,0,0,0.3)', padding: '4px', borderRadius: '8px' }}>
|
||||||
|
<button
|
||||||
|
onClick={() => setViewMode('kanban')}
|
||||||
|
style={{
|
||||||
|
background: viewMode === 'kanban' ? 'rgba(255,255,255,0.1)' : 'transparent',
|
||||||
|
color: viewMode === 'kanban' ? '#fff' : '#888',
|
||||||
|
border: 'none', padding: '6px 12px', borderRadius: '6px', cursor: 'pointer'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
▚ Kanban
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setViewMode('list')}
|
||||||
|
style={{
|
||||||
|
background: viewMode === 'list' ? 'rgba(255,255,255,0.1)' : 'transparent',
|
||||||
|
color: viewMode === 'list' ? '#fff' : '#888',
|
||||||
|
border: 'none', padding: '6px 12px', borderRadius: '6px', cursor: 'pointer'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
☰ List
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: '3fr 1fr', gap: '2rem' }}>
|
||||||
|
|
||||||
|
{/* 5.1 Task Board */}
|
||||||
|
<div>
|
||||||
|
{viewMode === 'kanban' ? (
|
||||||
|
<div style={{ overflowX: 'auto', paddingBottom: '1rem' }}>
|
||||||
|
<div style={{ display: 'flex', gap: '1rem', minWidth: '100%' }}>
|
||||||
|
{columns.map(col => (
|
||||||
|
<div
|
||||||
|
key={col.id}
|
||||||
|
onDragOver={handleDragOver}
|
||||||
|
onDrop={(e) => handleDrop(e, col.id)}
|
||||||
|
style={{
|
||||||
|
background: 'rgba(255,255,255,0.03)',
|
||||||
|
padding: '1rem',
|
||||||
|
borderRadius: '8px',
|
||||||
|
minHeight: '400px',
|
||||||
|
minWidth: '280px',
|
||||||
|
flex: '0 0 280px',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<h3 style={{ fontSize: '1rem', marginBottom: '1rem', color: col.color, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
|
{col.title}
|
||||||
|
<span style={{ background: col.bg, padding: '2px 6px', borderRadius: '4px', fontSize: '0.8rem', color: col.color }}>
|
||||||
|
{getTasksByStatus(col.id).length}
|
||||||
|
</span>
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div style={{ flex: 1 }}>
|
||||||
|
{getTasksByStatus(col.id).map(task => (
|
||||||
|
<div
|
||||||
|
key={task.id}
|
||||||
|
draggable
|
||||||
|
onDragStart={(e) => handleDragStart(e, task.id)}
|
||||||
|
style={{ background: 'rgba(30,40,50,0.8)', padding: '1rem', borderRadius: '6px', marginBottom: '0.8rem', border: `1px solid ${col.color === '#fff' ? 'rgba(255,255,255,0.2)' : col.color}`, cursor: 'grab' }}
|
||||||
|
>
|
||||||
|
<div style={{ fontSize: '0.9rem', marginBottom: '0.5rem' }}>{task.title}</div>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
|
<span style={{ fontSize: '0.75rem', color: '#888' }}>{task.id}</span>
|
||||||
|
<div style={{ width: '24px', height: '24px', borderRadius: '50%', background: task.color, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '0.7rem', color: '#000', fontWeight: 600 }}>{task.assignee}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Add Task Input */}
|
||||||
|
{addingTaskToCol === col.id ? (
|
||||||
|
<div style={{ marginTop: '0.5rem', background: 'rgba(0,0,0,0.2)', padding: '0.5rem', borderRadius: '6px' }}>
|
||||||
|
<input
|
||||||
|
className={styles.inputField}
|
||||||
|
style={{ padding: '0.4rem', fontSize: '0.9rem', marginBottom: '0.5rem' }}
|
||||||
|
placeholder="Task Code (e.g. MKT-101)"
|
||||||
|
value={newTaskCode}
|
||||||
|
onChange={(e) => setNewTaskCode(e.target.value)}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
autoFocus
|
||||||
|
className={styles.inputField}
|
||||||
|
style={{ padding: '0.4rem', fontSize: '0.9rem', marginBottom: '0.5rem' }}
|
||||||
|
placeholder="Task title..."
|
||||||
|
value={newTaskTitle}
|
||||||
|
onChange={(e) => setNewTaskTitle(e.target.value)}
|
||||||
|
/>
|
||||||
|
<select
|
||||||
|
className={styles.inputField}
|
||||||
|
style={{ padding: '0.4rem', fontSize: '0.9rem', marginBottom: '0.5rem' }}
|
||||||
|
value={newTaskAssignee}
|
||||||
|
onChange={(e) => setNewTaskAssignee(e.target.value)}
|
||||||
|
>
|
||||||
|
{MOCK_EMPLOYEES.map(emp => (
|
||||||
|
<option key={emp.id} value={emp.id}>{emp.name}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', gap: '0.5rem' }}>
|
||||||
|
<button
|
||||||
|
onClick={() => handleAddTask(col.id)}
|
||||||
|
className={styles.btnPrimary}
|
||||||
|
style={{ padding: '0.2rem 0.6rem', fontSize: '0.8rem' }}
|
||||||
|
>Add</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setAddingTaskToCol(null)}
|
||||||
|
className={styles.btnSecondary}
|
||||||
|
style={{ padding: '0.2rem 0.6rem', fontSize: '0.8rem', border: 'none' }}
|
||||||
|
>Cancel</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setAddingTaskToCol(col.id);
|
||||||
|
setNewTaskTitle('');
|
||||||
|
setNewTaskCode('');
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
marginTop: '0.5rem',
|
||||||
|
background: 'transparent',
|
||||||
|
border: '1px dashed rgba(255,255,255,0.2)',
|
||||||
|
color: '#888',
|
||||||
|
width: '100%',
|
||||||
|
padding: '0.5rem',
|
||||||
|
borderRadius: '6px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: '0.9rem'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
+ Add Task
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Add Column Button/Form */}
|
||||||
|
<div style={{ minWidth: '280px', flex: '0 0 280px' }}>
|
||||||
|
{isAddingColumn ? (
|
||||||
|
<div style={{ background: 'rgba(255,255,255,0.03)', padding: '1rem', borderRadius: '8px' }}>
|
||||||
|
<input
|
||||||
|
autoFocus
|
||||||
|
className={styles.inputField}
|
||||||
|
placeholder="Column Title"
|
||||||
|
value={newColumnTitle}
|
||||||
|
onChange={(e) => setNewColumnTitle(e.target.value)}
|
||||||
|
style={{ marginBottom: '1rem' }}
|
||||||
|
/>
|
||||||
|
<div style={{ display: 'flex', gap: '0.5rem' }}>
|
||||||
|
<button className={styles.btnPrimary} onClick={handleAddColumn} style={{ width: '100%' }}>Add</button>
|
||||||
|
<button className={styles.btnSecondary} onClick={() => setIsAddingColumn(false)} style={{ width: '100%' }}>Cancel</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
onClick={() => setIsAddingColumn(true)}
|
||||||
|
style={{
|
||||||
|
background: 'rgba(255,255,255,0.05)',
|
||||||
|
border: 'none',
|
||||||
|
color: '#ccc',
|
||||||
|
width: '100%',
|
||||||
|
height: '50px',
|
||||||
|
borderRadius: '8px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: '1rem',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
gap: '0.5rem'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
+ Add Section
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className={styles.section}>
|
||||||
|
<table className={styles.table}>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Task Name</th>
|
||||||
|
<th>Assignee</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Due Date</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{tasks.map(task => {
|
||||||
|
const column = columns.find(col => col.id === task.status);
|
||||||
|
const assignee = MOCK_EMPLOYEES.find(e => e.id === task.assignee);
|
||||||
|
return (
|
||||||
|
<tr key={task.id}>
|
||||||
|
<td>{task.id}</td>
|
||||||
|
<td>{task.title}</td>
|
||||||
|
<td>{assignee ? assignee.name : task.assignee}</td>
|
||||||
|
<td>
|
||||||
|
<span className={styles.statusBadge} style={{
|
||||||
|
background: column?.bg || 'rgba(255,255,255,0.1)',
|
||||||
|
color: column?.color || '#ccc'
|
||||||
|
}}>
|
||||||
|
{task.status.replace('-', ' ')}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>15 Jan</td>
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 5.2 Stand-up & 5.3 Communication */}
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}>
|
||||||
|
|
||||||
|
{/* Daily Stand-up Widget */}
|
||||||
|
<div className={styles.section}>
|
||||||
|
<h3 className={styles.sectionTitle}>🌅 Daily Stand-up</h3>
|
||||||
|
<div style={{ padding: '1rem', background: 'rgba(255,255,255,0.05)', borderRadius: '8px', marginBottom: '1rem', borderLeft: '3px solid #00ff9d' }}>
|
||||||
|
<div style={{ fontSize: '0.85rem', fontWeight: 600, marginBottom: '0.5rem' }}>Rohan Mehta (Today)</div>
|
||||||
|
<ul className={styles.list} style={{ fontSize: '0.85rem', color: '#ccc' }}>
|
||||||
|
<li>• Working on survey questions.</li>
|
||||||
|
<li>• ⛔ Blocked: Need access to SurveyMonkey.</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<button className={styles.btnPrimary} style={{ width: '100%', fontSize: '0.9rem', justifyContent: 'center' }}>+ Add My Update</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
232
src/app/dashboard/projects/[id]/foundations/page.tsx
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
'use client';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import styles from '../../projects.module.css';
|
||||||
|
|
||||||
|
export default function FoundationsPage() {
|
||||||
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
businessObjective: 'To increase market share by 15% through a targeted marketing campaign in Q3, focusing on Gen-Z demographics.',
|
||||||
|
expectedOutcome: 'A 20% increase in brand engagement and 5000+ new leads generated within 3 months.',
|
||||||
|
successCriteria: [
|
||||||
|
'Acquire 5000 new qualified leads',
|
||||||
|
'Keep CAC under ₹500',
|
||||||
|
'Achieve 10% conversion rate on landing pages'
|
||||||
|
],
|
||||||
|
budgetCap: '5,00,000',
|
||||||
|
deadline: '2024-12-30',
|
||||||
|
department: 'Marketing & Sales',
|
||||||
|
strategicPillar: 'Strategic Growth',
|
||||||
|
methodology: 'Hybrid (Agile + Waterfall)',
|
||||||
|
keyAssumptions: [
|
||||||
|
'Marketing team availability is constant.',
|
||||||
|
'No major regulatory changes in Q3.',
|
||||||
|
'Vendor pricing remains stable.'
|
||||||
|
]
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
setIsEditing(false);
|
||||||
|
// data save logic would go here
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChange = (field: string, value: string) => {
|
||||||
|
setFormData(prev => ({ ...prev, [field]: value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleArrayChange = (field: 'successCriteria' | 'keyAssumptions', index: number, value: string) => {
|
||||||
|
const newArray = [...formData[field]];
|
||||||
|
newArray[index] = value;
|
||||||
|
setFormData(prev => ({ ...prev, [field]: newArray }));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ animation: 'fadeIn 0.5s ease-out' }}>
|
||||||
|
<div className={styles.sectionHeader}>
|
||||||
|
<div>
|
||||||
|
<h2 className={styles.pageTitle}>Project Foundations</h2>
|
||||||
|
<p className={styles.pageSubtitle}>Strategic alignment, purpose, and constraints.</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{isEditing ? (
|
||||||
|
<div style={{ display: 'flex', gap: '1rem' }}>
|
||||||
|
<button className={styles.btnSecondary} onClick={() => setIsEditing(false)}>Cancel</button>
|
||||||
|
<button className={styles.btnPrimary} onClick={handleSave}>Save Changes</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<button className={styles.btnPrimary} onClick={() => setIsEditing(true)}>Edit Foundations</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '2rem' }}>
|
||||||
|
|
||||||
|
{/* 2.1 Project Purpose */}
|
||||||
|
<div className={styles.section}>
|
||||||
|
<h3 className={styles.sectionTitle}>🎯 Project Purpose</h3>
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<label className={styles.label}>Business Objective</label>
|
||||||
|
{isEditing ? (
|
||||||
|
<textarea
|
||||||
|
className={styles.readonlyField}
|
||||||
|
style={{ width: '100%', background: 'rgba(0,0,0,0.4)', color: '#fff', minHeight: '80px' }}
|
||||||
|
value={formData.businessObjective}
|
||||||
|
onChange={(e) => handleChange('businessObjective', e.target.value)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className={styles.readonlyField}>{formData.businessObjective}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<label className={styles.label}>Expected Outcome</label>
|
||||||
|
{isEditing ? (
|
||||||
|
<textarea
|
||||||
|
className={styles.readonlyField}
|
||||||
|
style={{ width: '100%', background: 'rgba(0,0,0,0.4)', color: '#fff', minHeight: '80px' }}
|
||||||
|
value={formData.expectedOutcome}
|
||||||
|
onChange={(e) => handleChange('expectedOutcome', e.target.value)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className={styles.readonlyField}>{formData.expectedOutcome}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<label className={styles.label}>Success Criteria</label>
|
||||||
|
{isEditing ? (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
|
||||||
|
{formData.successCriteria.map((item, i) => (
|
||||||
|
<input
|
||||||
|
key={i}
|
||||||
|
className={styles.readonlyField}
|
||||||
|
style={{ width: '100%', background: 'rgba(0,0,0,0.4)', color: '#fff' }}
|
||||||
|
value={item}
|
||||||
|
onChange={(e) => handleArrayChange('successCriteria', i, e.target.value)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<ul className={styles.list}>
|
||||||
|
{formData.successCriteria.map((item, i) => (
|
||||||
|
<li key={i}>✔️ {item}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 2.2 Constraints & Assumptions */}
|
||||||
|
<div className={styles.section}>
|
||||||
|
<h3 className={styles.sectionTitle}>⛓️ Constraints & Assumptions</h3>
|
||||||
|
<div className={styles.formGrid}>
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<label className={styles.label}>Budget Cap (₹)</label>
|
||||||
|
{isEditing ? (
|
||||||
|
<input
|
||||||
|
className={styles.readonlyField}
|
||||||
|
style={{ width: '100%', background: 'rgba(0,0,0,0.4)', color: '#fff' }}
|
||||||
|
value={formData.budgetCap}
|
||||||
|
onChange={(e) => handleChange('budgetCap', e.target.value)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className={styles.readonlyField} style={{ color: '#00ff9d' }}>₹ {formData.budgetCap}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<label className={styles.label}>Deadline</label>
|
||||||
|
{isEditing ? (
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
className={styles.readonlyField}
|
||||||
|
style={{ width: '100%', background: 'rgba(0,0,0,0.4)', color: '#fff' }}
|
||||||
|
value={formData.deadline}
|
||||||
|
onChange={(e) => handleChange('deadline', e.target.value)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className={styles.readonlyField} style={{ color: '#ffd700' }}>{formData.deadline}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<label className={styles.label}>Key Assumptions</label>
|
||||||
|
{isEditing ? (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
|
||||||
|
{formData.keyAssumptions.map((item, i) => (
|
||||||
|
<input
|
||||||
|
key={i}
|
||||||
|
className={styles.readonlyField}
|
||||||
|
style={{ width: '100%', background: 'rgba(0,0,0,0.4)', color: '#fff' }}
|
||||||
|
value={item}
|
||||||
|
onChange={(e) => handleArrayChange('keyAssumptions', i, e.target.value)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<ul className={styles.list}>
|
||||||
|
{formData.keyAssumptions.map((item, i) => (
|
||||||
|
<li key={i}>• {item}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 2.3 Organizational Context */}
|
||||||
|
<div className={styles.section} style={{ gridColumn: 'span 2' }}>
|
||||||
|
<h3 className={styles.sectionTitle}>🏢 Organizational Context</h3>
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '1.5rem' }}>
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<label className={styles.label}>Department</label>
|
||||||
|
{isEditing ? (
|
||||||
|
<input
|
||||||
|
className={styles.readonlyField}
|
||||||
|
style={{ width: '100%', background: 'rgba(0,0,0,0.4)', color: '#fff' }}
|
||||||
|
value={formData.department}
|
||||||
|
onChange={(e) => handleChange('department', e.target.value)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className={styles.readonlyField}>{formData.department}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<label className={styles.label}>Strategic Pillar</label>
|
||||||
|
{isEditing ? (
|
||||||
|
<select
|
||||||
|
className={styles.readonlyField}
|
||||||
|
style={{ width: '100%', background: 'rgba(0,0,0,0.4)', color: '#fff' }}
|
||||||
|
value={formData.strategicPillar}
|
||||||
|
onChange={(e) => handleChange('strategicPillar', e.target.value)}
|
||||||
|
>
|
||||||
|
<option>Strategic Growth</option>
|
||||||
|
<option>Operational Efficiency</option>
|
||||||
|
<option>Innovation & R&D</option>
|
||||||
|
<option>Compliance & Security</option>
|
||||||
|
</select>
|
||||||
|
) : (
|
||||||
|
<div className={styles.readonlyField}>{formData.strategicPillar}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<label className={styles.label}>Methodology</label>
|
||||||
|
{isEditing ? (
|
||||||
|
<select
|
||||||
|
className={styles.readonlyField}
|
||||||
|
style={{ width: '100%', background: 'rgba(0,0,0,0.4)', color: '#fff' }}
|
||||||
|
value={formData.methodology}
|
||||||
|
onChange={(e) => handleChange('methodology', e.target.value)}
|
||||||
|
>
|
||||||
|
<option>Agile (Scrum)</option>
|
||||||
|
<option>Waterfall</option>
|
||||||
|
<option>Hybrid (Agile + Waterfall)</option>
|
||||||
|
<option>Kanban</option>
|
||||||
|
</select>
|
||||||
|
) : (
|
||||||
|
<div className={styles.readonlyField}>{formData.methodology}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
433
src/app/dashboard/projects/[id]/initiation/page.tsx
Normal file
@@ -0,0 +1,433 @@
|
|||||||
|
'use client';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import styles from '../../projects.module.css';
|
||||||
|
|
||||||
|
// Mock Data
|
||||||
|
const MOCK_CHARTER = {
|
||||||
|
businessCase: 'To increase market share by 15% through a targeted marketing campaign in Q3.',
|
||||||
|
scope: 'Social media ads, influencer partnerships, and email marketing sequence.',
|
||||||
|
objectives: '1. Acquire 5000 new leads.\n2. Convert 10% to paid customers.',
|
||||||
|
sponsor: 'Vikram Singh',
|
||||||
|
budget: 500000
|
||||||
|
};
|
||||||
|
|
||||||
|
const MOCK_STAKEHOLDERS = [
|
||||||
|
{ id: 1, name: 'Vikram Singh', role: 'Sponsor', influence: 'High', interest: 'High' },
|
||||||
|
{ id: 2, name: 'Anjali Gupta', role: 'Product Lead', influence: 'High', interest: 'Medium' },
|
||||||
|
{ id: 3, name: 'Marketing Team', role: 'Execution', influence: 'Medium', interest: 'High' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const MOCK_RISKS = [
|
||||||
|
{ id: 1, title: 'Budget Overrun', probability: 'Medium', impact: 'High', owner: 'Rohan Mehta' },
|
||||||
|
{ id: 2, title: 'Vendor Delay', probability: 'Low', impact: 'Medium', owner: 'Anjali Gupta' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const MOCK_EMPLOYEES = [
|
||||||
|
{ id: 101, name: 'Vikram Singh', role: 'Sponsor' },
|
||||||
|
{ id: 102, name: 'Anjali Gupta', role: 'Product Lead' },
|
||||||
|
{ id: 103, name: 'Rohan Mehta', role: 'Project Manager' },
|
||||||
|
{ id: 104, name: 'Sita Verma', role: 'Marketing Head' },
|
||||||
|
{ id: 105, name: 'Amit Patel', role: 'Senior Developer' },
|
||||||
|
{ id: 106, name: 'Priya Sharma', role: 'UX Designer' },
|
||||||
|
{ id: 107, name: 'John Doe', role: 'Financial Analyst' },
|
||||||
|
{ id: 108, name: 'Jane Smith', role: 'Compliance Officer' }
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function InitiationPage() {
|
||||||
|
const [activeTab, setActiveTab] = useState<'charter' | 'stakeholders' | 'risks'>('charter');
|
||||||
|
|
||||||
|
// Charter Edit State
|
||||||
|
const [isEditingCharter, setIsEditingCharter] = useState(false);
|
||||||
|
const [charterData, setCharterData] = useState(MOCK_CHARTER);
|
||||||
|
|
||||||
|
const handleCharterChange = (field: string, value: any) => {
|
||||||
|
setCharterData(prev => ({ ...prev, [field]: value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveCharter = () => {
|
||||||
|
setIsEditingCharter(false);
|
||||||
|
// Save logic here
|
||||||
|
};
|
||||||
|
|
||||||
|
// Stakeholder State
|
||||||
|
const [stakeholders, setStakeholders] = useState(MOCK_STAKEHOLDERS);
|
||||||
|
const [isAddingStakeholder, setIsAddingStakeholder] = useState(false);
|
||||||
|
const [newStakeholder, setNewStakeholder] = useState({
|
||||||
|
name: '',
|
||||||
|
role: '',
|
||||||
|
influence: 'Medium',
|
||||||
|
interest: 'Medium'
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleEmployeeSelect = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
|
const selectedName = e.target.value;
|
||||||
|
const employee = MOCK_EMPLOYEES.find(emp => emp.name === selectedName);
|
||||||
|
setNewStakeholder(prev => ({
|
||||||
|
...prev,
|
||||||
|
name: selectedName,
|
||||||
|
role: employee ? employee.role : ''
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddStakeholder = () => {
|
||||||
|
if (!newStakeholder.name || !newStakeholder.role) return;
|
||||||
|
const newId = stakeholders.length + 1;
|
||||||
|
setStakeholders([...stakeholders, { id: newId, ...newStakeholder }]);
|
||||||
|
setNewStakeholder({ name: '', role: '', influence: 'Medium', interest: 'Medium' });
|
||||||
|
setIsAddingStakeholder(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Risk State
|
||||||
|
const [risks, setRisks] = useState(MOCK_RISKS);
|
||||||
|
const [isAddingRisk, setIsAddingRisk] = useState(false);
|
||||||
|
const [newRisk, setNewRisk] = useState({
|
||||||
|
title: '',
|
||||||
|
owner: '',
|
||||||
|
probability: 'Low',
|
||||||
|
impact: 'Low'
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleAddRisk = () => {
|
||||||
|
if (!newRisk.title || !newRisk.owner) return;
|
||||||
|
const newId = risks.length + 1;
|
||||||
|
setRisks([...risks, { id: newId, ...newRisk }]);
|
||||||
|
setNewRisk({ title: '', owner: '', probability: 'Low', impact: 'Low' });
|
||||||
|
setIsAddingRisk(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ animation: 'fadeIn 0.5s ease-out' }}>
|
||||||
|
{/* Module Header */}
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '2rem' }}>
|
||||||
|
<div>
|
||||||
|
<h2 style={{ fontSize: '1.8rem', marginBottom: '0.5rem', color: '#fff' }}>Initiation Phase</h2>
|
||||||
|
<p style={{ color: '#888' }}>Formally authorize the project and identify key stakeholders.</p>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', gap: '1rem' }}>
|
||||||
|
<button className={styles.btnPrimary} style={{ background: 'rgba(255,255,255,0.1)', color: '#fff' }}>Export Report</button>
|
||||||
|
<button className={styles.btnPrimary}>Submit for Approval</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Sub-Tabs */}
|
||||||
|
<div style={{ display: 'flex', gap: '1rem', borderBottom: '1px solid rgba(255,255,255,0.1)', marginBottom: '2rem' }}>
|
||||||
|
{['Project Charter', 'Stakeholder Register', 'Initial Risks'].map((tab, index) => {
|
||||||
|
const key = ['charter', 'stakeholders', 'risks'][index] as typeof activeTab;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={key}
|
||||||
|
onClick={() => setActiveTab(key)}
|
||||||
|
style={{
|
||||||
|
background: 'transparent',
|
||||||
|
border: 'none',
|
||||||
|
padding: '10px 20px',
|
||||||
|
color: activeTab === key ? 'var(--color-gold)' : 'rgba(255,255,255,0.5)',
|
||||||
|
borderBottom: activeTab === key ? '2px solid var(--color-gold)' : '2px solid transparent',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: '1rem',
|
||||||
|
fontWeight: 500,
|
||||||
|
transition: 'all 0.3s'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tab}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tab Content */}
|
||||||
|
<div className={styles.section}>
|
||||||
|
|
||||||
|
{/* CHARTER TAB */}
|
||||||
|
{activeTab === 'charter' && (
|
||||||
|
<div style={{ display: 'grid', gap: '1.5rem' }}>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: '1rem' }}>
|
||||||
|
{isEditingCharter ? (
|
||||||
|
<div style={{ display: 'flex', gap: '1rem' }}>
|
||||||
|
<button className={styles.btnSecondary} onClick={() => setIsEditingCharter(false)}>Cancel</button>
|
||||||
|
<button className={styles.btnPrimary} onClick={handleSaveCharter}>Save Changes</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<button className={styles.btnPrimary} onClick={() => setIsEditingCharter(true)}>Edit Charter</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '2rem' }}>
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<label style={{ display: 'block', color: 'var(--color-gold)', marginBottom: '0.5rem' }}>Business Case</label>
|
||||||
|
{isEditingCharter ? (
|
||||||
|
<textarea
|
||||||
|
className={styles.readonlyField}
|
||||||
|
style={{ width: '100%', background: 'rgba(0,0,0,0.4)', color: '#fff', minHeight: '100px' }}
|
||||||
|
value={charterData.businessCase}
|
||||||
|
onChange={(e) => handleCharterChange('businessCase', e.target.value)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div style={{ background: 'rgba(0,0,0,0.2)', padding: '1rem', borderRadius: '8px', color: '#ddd', lineHeight: '1.6' }}>
|
||||||
|
{charterData.businessCase}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<label style={{ display: 'block', color: 'var(--color-gold)', marginBottom: '0.5rem' }}>Project Scope</label>
|
||||||
|
{isEditingCharter ? (
|
||||||
|
<textarea
|
||||||
|
className={styles.readonlyField}
|
||||||
|
style={{ width: '100%', background: 'rgba(0,0,0,0.4)', color: '#fff', minHeight: '100px' }}
|
||||||
|
value={charterData.scope}
|
||||||
|
onChange={(e) => handleCharterChange('scope', e.target.value)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div style={{ background: 'rgba(0,0,0,0.2)', padding: '1rem', borderRadius: '8px', color: '#ddd', lineHeight: '1.6' }}>
|
||||||
|
{charterData.scope}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<label style={{ display: 'block', color: 'var(--color-gold)', marginBottom: '0.5rem' }}>Objectives</label>
|
||||||
|
{isEditingCharter ? (
|
||||||
|
<textarea
|
||||||
|
className={styles.readonlyField}
|
||||||
|
style={{ width: '100%', background: 'rgba(0,0,0,0.4)', color: '#fff', minHeight: '120px' }}
|
||||||
|
value={charterData.objectives}
|
||||||
|
onChange={(e) => handleCharterChange('objectives', e.target.value)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div style={{ background: 'rgba(0,0,0,0.2)', padding: '1rem', borderRadius: '8px', color: '#ddd', lineHeight: '1.6', whiteSpace: 'pre-line' }}>
|
||||||
|
{charterData.objectives}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '2rem', marginTop: '1rem', paddingTop: '1rem', borderTop: '1px solid rgba(255,255,255,0.05)' }}>
|
||||||
|
<div>
|
||||||
|
<span style={{ color: '#888', display: 'block', marginBottom: '0.5rem' }}>Project Sponsor:</span>
|
||||||
|
{isEditingCharter ? (
|
||||||
|
<input
|
||||||
|
className={styles.readonlyField}
|
||||||
|
style={{ width: '100%', background: 'rgba(0,0,0,0.4)', color: '#fff', fontWeight: 600 }}
|
||||||
|
value={charterData.sponsor}
|
||||||
|
onChange={(e) => handleCharterChange('sponsor', e.target.value)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<span style={{ fontWeight: 600 }}>{charterData.sponsor}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span style={{ color: '#888', display: 'block', marginBottom: '0.5rem' }}>Initial Budget:</span>
|
||||||
|
{isEditingCharter ? (
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
className={styles.readonlyField}
|
||||||
|
style={{ width: '100%', background: 'rgba(0,0,0,0.4)', color: '#00ff9d', fontWeight: 600 }}
|
||||||
|
value={charterData.budget}
|
||||||
|
onChange={(e) => handleCharterChange('budget', Number(e.target.value))}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<span style={{ fontWeight: 600, color: '#00ff9d' }}>₹ {charterData.budget.toLocaleString()}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* STAKEHOLDERS TAB */}
|
||||||
|
{activeTab === 'stakeholders' && (
|
||||||
|
<div>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: '1rem' }}>
|
||||||
|
<button
|
||||||
|
className={styles.btnPrimary}
|
||||||
|
style={{ fontSize: '0.8rem', padding: '0.5rem 1rem' }}
|
||||||
|
onClick={() => setIsAddingStakeholder(true)}
|
||||||
|
>
|
||||||
|
+ Add Stakeholder
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Add Stakeholder Form */}
|
||||||
|
{isAddingStakeholder && (
|
||||||
|
<div style={{
|
||||||
|
background: 'rgba(255,255,255,0.05)',
|
||||||
|
padding: '1.5rem',
|
||||||
|
borderRadius: '8px',
|
||||||
|
marginBottom: '1.5rem',
|
||||||
|
border: '1px solid var(--color-gold)'
|
||||||
|
}}>
|
||||||
|
<h4 style={{ margin: '0 0 1rem 0', color: 'var(--color-gold)' }}>New Stakeholder Details</h4>
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr 1fr', gap: '1rem', marginBottom: '1rem' }}>
|
||||||
|
<select
|
||||||
|
className={styles.inputField}
|
||||||
|
value={newStakeholder.name}
|
||||||
|
onChange={handleEmployeeSelect}
|
||||||
|
>
|
||||||
|
<option value="">Select Employee</option>
|
||||||
|
{MOCK_EMPLOYEES.map(emp => (
|
||||||
|
<option key={emp.id} value={emp.name}>{emp.name}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<input
|
||||||
|
placeholder="Role"
|
||||||
|
className={styles.readonlyField}
|
||||||
|
style={{ background: 'rgba(255,255,255,0.05)', color: '#aaa', cursor: 'not-allowed' }}
|
||||||
|
value={newStakeholder.role}
|
||||||
|
readOnly
|
||||||
|
title="Role is auto-fetched from employee database"
|
||||||
|
/>
|
||||||
|
<select
|
||||||
|
className={styles.inputField}
|
||||||
|
value={newStakeholder.influence}
|
||||||
|
onChange={(e) => setNewStakeholder({ ...newStakeholder, influence: e.target.value })}
|
||||||
|
>
|
||||||
|
<option value="High">Influence: High</option>
|
||||||
|
<option value="Medium">Influence: Medium</option>
|
||||||
|
<option value="Low">Influence: Low</option>
|
||||||
|
</select>
|
||||||
|
<select
|
||||||
|
className={styles.inputField}
|
||||||
|
value={newStakeholder.interest}
|
||||||
|
onChange={(e) => setNewStakeholder({ ...newStakeholder, interest: e.target.value })}
|
||||||
|
>
|
||||||
|
<option value="High">Interest: High</option>
|
||||||
|
<option value="Medium">Interest: Medium</option>
|
||||||
|
<option value="Low">Interest: Low</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', gap: '1rem', justifyContent: 'flex-end' }}>
|
||||||
|
<button className={styles.btnSecondary} onClick={() => setIsAddingStakeholder(false)}>Cancel</button>
|
||||||
|
<button className={styles.btnPrimary} onClick={handleAddStakeholder}>Add Person</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<table className={styles.table} style={{ width: '100%' }}>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style={{ textAlign: 'left', padding: '1rem', color: '#888' }}>Name</th>
|
||||||
|
<th style={{ textAlign: 'left', padding: '1rem', color: '#888' }}>Role</th>
|
||||||
|
<th style={{ textAlign: 'left', padding: '1rem', color: '#888' }}>Influence</th>
|
||||||
|
<th style={{ textAlign: 'left', padding: '1rem', color: '#888' }}>Interest</th>
|
||||||
|
<th style={{ textAlign: 'right', padding: '1rem', color: '#888' }}>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{stakeholders.map(s => (
|
||||||
|
<tr key={s.id} style={{ borderBottom: '1px solid rgba(255,255,255,0.05)' }}>
|
||||||
|
<td style={{ padding: '1rem' }}>{s.name}</td>
|
||||||
|
<td style={{ padding: '1rem' }}>{s.role}</td>
|
||||||
|
<td style={{ padding: '1rem' }}>
|
||||||
|
<span style={{
|
||||||
|
padding: '0.2rem 0.6rem',
|
||||||
|
borderRadius: '4px',
|
||||||
|
background: s.influence === 'High' ? 'rgba(255,50,50,0.1)' : 'rgba(255,255,255,0.1)',
|
||||||
|
color: s.influence === 'High' ? '#ff8888' : '#ccc'
|
||||||
|
}}>{s.influence}</span>
|
||||||
|
</td>
|
||||||
|
<td style={{ padding: '1rem' }}>{s.interest}</td>
|
||||||
|
<td style={{ padding: '1rem', textAlign: 'right' }}>
|
||||||
|
<button style={{ background: 'none', border: 'none', cursor: 'pointer', marginRight: '0.5rem' }}>✏️</button>
|
||||||
|
<button style={{ background: 'none', border: 'none', cursor: 'pointer' }}>🗑️</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* RISKS TAB */}
|
||||||
|
{activeTab === 'risks' && (
|
||||||
|
<div>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: '1rem' }}>
|
||||||
|
<button
|
||||||
|
className={styles.btnPrimary}
|
||||||
|
style={{ fontSize: '0.8rem', padding: '0.5rem 1rem' }}
|
||||||
|
onClick={() => setIsAddingRisk(true)}
|
||||||
|
>
|
||||||
|
+ Add Risk
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Add Risk Form */}
|
||||||
|
{isAddingRisk && (
|
||||||
|
<div style={{
|
||||||
|
background: 'rgba(255,255,255,0.05)',
|
||||||
|
padding: '1.5rem',
|
||||||
|
borderRadius: '8px',
|
||||||
|
marginBottom: '1.5rem',
|
||||||
|
border: '1px solid var(--color-gold)'
|
||||||
|
}}>
|
||||||
|
<h4 style={{ margin: '0 0 1rem 0', color: 'var(--color-gold)' }}>New Risk Details</h4>
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: '2fr 1fr 1fr 1fr', gap: '1rem', marginBottom: '1rem' }}>
|
||||||
|
<input
|
||||||
|
placeholder="Risk Title"
|
||||||
|
className={styles.inputField}
|
||||||
|
value={newRisk.title}
|
||||||
|
onChange={(e) => setNewRisk({ ...newRisk, title: e.target.value })}
|
||||||
|
/>
|
||||||
|
<select
|
||||||
|
className={styles.inputField}
|
||||||
|
value={newRisk.owner}
|
||||||
|
onChange={(e) => setNewRisk({ ...newRisk, owner: e.target.value })}
|
||||||
|
>
|
||||||
|
<option value="">Select Owner</option>
|
||||||
|
{MOCK_EMPLOYEES.map(emp => (
|
||||||
|
<option key={emp.id} value={emp.name}>{emp.name}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<select
|
||||||
|
className={styles.inputField}
|
||||||
|
value={newRisk.probability}
|
||||||
|
onChange={(e) => setNewRisk({ ...newRisk, probability: e.target.value })}
|
||||||
|
>
|
||||||
|
<option value="High">Prob: High</option>
|
||||||
|
<option value="Medium">Prob: Medium</option>
|
||||||
|
<option value="Low">Prob: Low</option>
|
||||||
|
</select>
|
||||||
|
<select
|
||||||
|
className={styles.inputField}
|
||||||
|
value={newRisk.impact}
|
||||||
|
onChange={(e) => setNewRisk({ ...newRisk, impact: e.target.value })}
|
||||||
|
>
|
||||||
|
<option value="High">Impact: High</option>
|
||||||
|
<option value="Medium">Impact: Medium</option>
|
||||||
|
<option value="Low">Impact: Low</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', gap: '1rem', justifyContent: 'flex-end' }}>
|
||||||
|
<button className={styles.btnSecondary} onClick={() => setIsAddingRisk(false)}>Cancel</button>
|
||||||
|
<button className={styles.btnPrimary} onClick={handleAddRisk}>Add Risk</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div style={{ display: 'grid', gap: '1rem' }}>
|
||||||
|
{risks.map(risk => (
|
||||||
|
<div key={risk.id} style={{
|
||||||
|
background: 'rgba(255,255,255,0.03)',
|
||||||
|
padding: '1rem',
|
||||||
|
borderRadius: '8px',
|
||||||
|
borderLeft: `4px solid ${risk.impact === 'High' ? '#ff4444' : '#ffd700'}`,
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center'
|
||||||
|
}}>
|
||||||
|
<div>
|
||||||
|
<h3 style={{ margin: '0 0 0.5rem 0', fontSize: '1rem' }}>{risk.title}</h3>
|
||||||
|
<div style={{ fontSize: '0.85rem', color: '#888' }}>
|
||||||
|
Owned by: <span style={{ color: '#ccc' }}>{risk.owner}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ textAlign: 'right' }}>
|
||||||
|
<div style={{ fontSize: '0.85rem', color: '#888', marginBottom: '0.3rem' }}>Probability: {risk.probability}</div>
|
||||||
|
<div style={{ fontSize: '0.85rem', color: '#888' }}>Impact: <span style={{ color: risk.impact === 'High' ? '#ff4444' : '#ccc' }}>{risk.impact}</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
60
src/app/dashboard/projects/[id]/layout.tsx
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
'use client';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { usePathname } from 'next/navigation';
|
||||||
|
import styles from '../projects.module.css';
|
||||||
|
|
||||||
|
export default function ProjectLayout({ children }: { children: React.ReactNode }) {
|
||||||
|
const pathname = usePathname();
|
||||||
|
const basePath = pathname.split('/').slice(0, 4).join('/'); // /dashboard/projects/[id]
|
||||||
|
|
||||||
|
// Determine current phase from URL (mock logic)
|
||||||
|
// In real app, we would check sub-routes
|
||||||
|
const isActive = (seg: string) => {
|
||||||
|
if (seg === '') return pathname === basePath;
|
||||||
|
return pathname.includes(`/${seg}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
{/* Project Header */}
|
||||||
|
<div style={{ marginBottom: '2rem', borderBottom: '1px solid rgba(255,255,255,0.1)', paddingBottom: '1rem' }}>
|
||||||
|
<div style={{ display: 'flex', gap: '1rem', alignItems: 'center', marginBottom: '0.5rem' }}>
|
||||||
|
<Link href="/dashboard/projects" style={{ color: '#888', textDecoration: 'none' }}>← Projects</Link>
|
||||||
|
<span style={{ color: '#666' }}>/</span>
|
||||||
|
<span style={{ color: '#aaa' }}>PRJ-001</span>
|
||||||
|
</div>
|
||||||
|
<h1 style={{ fontSize: '2rem', margin: '0.5rem 0' }}>Alpha Redesign Campaign</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Phase Navigation Tabs */}
|
||||||
|
<div style={{ display: 'flex', gap: '2px', background: 'rgba(0,0,0,0.2)', padding: '5px', borderRadius: '8px', marginBottom: '2rem' }}>
|
||||||
|
{['Overview', 'Foundations', 'Initiation', 'Planning', 'Execution', 'Control', 'Closure'].map((phase) => {
|
||||||
|
const slug = phase === 'Overview' ? '' : phase.toLowerCase();
|
||||||
|
const active = isActive(slug);
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
key={phase}
|
||||||
|
href={`${basePath}/${slug}`}
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
textAlign: 'center',
|
||||||
|
padding: '10px',
|
||||||
|
background: active ? 'var(--color-gold)' : 'transparent',
|
||||||
|
color: active ? '#000' : '#888',
|
||||||
|
fontWeight: active ? 600 : 400,
|
||||||
|
borderRadius: '6px',
|
||||||
|
textDecoration: 'none',
|
||||||
|
fontSize: '0.9rem',
|
||||||
|
transition: 'all 0.2s'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{phase}
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
107
src/app/dashboard/projects/[id]/page.tsx
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
'use client';
|
||||||
|
import styles from '../projects.module.css';
|
||||||
|
|
||||||
|
export default function ProjectDashboardPage({ params }: { params: { id: string } }) {
|
||||||
|
return (
|
||||||
|
<div style={{ animation: 'fadeIn 0.5s ease-out' }}>
|
||||||
|
{/* 1.2 KPI Cards Section */}
|
||||||
|
<div className={styles.statsGrid}>
|
||||||
|
{/* 1.2.1 Schedule Variance Card */}
|
||||||
|
<div className={styles.statCard}>
|
||||||
|
<div className={styles.statLabel}>Schedule Variance</div>
|
||||||
|
<div className={styles.statValue} style={{ color: '#00ff9d' }}>0%</div>
|
||||||
|
<div style={{ fontSize: '0.8rem', color: '#888' }}>Status: On Time</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 1.2.2 Cost Variance Card */}
|
||||||
|
<div className={styles.statCard}>
|
||||||
|
<div className={styles.statLabel}>Cost Variance</div>
|
||||||
|
<div className={styles.statValue} style={{ color: '#ff4444' }}>+5.2%</div>
|
||||||
|
<div style={{ fontSize: '0.8rem', color: '#888' }}>Status: Over Budget</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 1.2.3 Risk Summary Card */}
|
||||||
|
<div className={styles.statCard}>
|
||||||
|
<div className={styles.statLabel}>Active Risks</div>
|
||||||
|
<div className={styles.statValue}>12</div>
|
||||||
|
<div style={{ fontSize: '0.8rem', color: '#ffd700' }}>3 High Priority</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: '2fr 1fr', gap: '2rem' }}>
|
||||||
|
{/* 1.3 Recent Activity Feed */}
|
||||||
|
<div className={styles.section}>
|
||||||
|
<h3 style={{ marginBottom: '1rem', color: 'var(--color-gold)', display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
|
||||||
|
<span>⚡</span> Recent Activity
|
||||||
|
</h3>
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
|
||||||
|
{[
|
||||||
|
{ title: 'Task "Design Mockups" assigned to Anjali', time: '2 hours ago', type: 'task' },
|
||||||
|
{ title: 'Risk "Vendor Delay" escalated to High', time: '4 hours ago', type: 'risk' },
|
||||||
|
{ title: 'Budget approval request submitted', time: 'Yesterday', type: 'approval' }
|
||||||
|
].map((item, i) => (
|
||||||
|
<div key={i} style={{
|
||||||
|
padding: '1rem',
|
||||||
|
background: 'rgba(255,255,255,0.03)',
|
||||||
|
borderRadius: '8px',
|
||||||
|
borderLeft: '3px solid var(--color-gold)',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center'
|
||||||
|
}}>
|
||||||
|
<div style={{ fontSize: '0.95rem', color: '#fff' }}>{item.title}</div>
|
||||||
|
<div style={{ fontSize: '0.8rem', color: '#666' }}>{item.time}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 1.4 Pending Approvals Widget */}
|
||||||
|
<div className={styles.section}>
|
||||||
|
<h3 style={{ marginBottom: '1rem', color: 'var(--color-gold)', display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
|
||||||
|
<span>Bn</span> Pending Approvals
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
{/* Approval Item 1 */}
|
||||||
|
<div style={{ padding: '1rem', background: 'rgba(255,50,50,0.1)', borderRadius: '8px', marginBottom: '1rem', border: '1px solid rgba(255,50,50,0.2)' }}>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '0.5rem' }}>
|
||||||
|
<span style={{ fontWeight: 600, color: '#ff8888' }}>Charter Sign-off</span>
|
||||||
|
<span style={{ fontSize: '0.7rem', background: '#ff4444', color: '#000', padding: '2px 6px', borderRadius: '4px' }}>URGENT</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: '0.85rem', color: '#ccc', marginBottom: '1rem' }}>Waiting for Sponsor approval to proceed to Planning.</div>
|
||||||
|
<div style={{ display: 'flex', gap: '0.5rem' }}>
|
||||||
|
<button className={styles.btnPrimary} style={{ fontSize: '0.8rem', padding: '0.4rem 0.8rem', flex: 1 }}>Approve</button>
|
||||||
|
<button style={{
|
||||||
|
fontSize: '0.8rem',
|
||||||
|
padding: '0.4rem 0.8rem',
|
||||||
|
flex: 1,
|
||||||
|
background: 'transparent',
|
||||||
|
border: '1px solid rgba(255,255,255,0.2)',
|
||||||
|
color: '#fff',
|
||||||
|
borderRadius: '6px',
|
||||||
|
cursor: 'pointer'
|
||||||
|
}}>Reject</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Approval Item 2 */}
|
||||||
|
<div style={{ padding: '1rem', background: 'rgba(255, 215, 0, 0.05)', borderRadius: '8px', border: '1px solid rgba(255, 215, 0, 0.1)' }}>
|
||||||
|
<div style={{ fontWeight: 600, color: 'var(--color-gold)', marginBottom: '0.5rem' }}>Budget Expansion</div>
|
||||||
|
<div style={{ fontSize: '0.85rem', color: '#ccc', marginBottom: '1rem' }}>Request for additional ₹50k for Q3 Ads.</div>
|
||||||
|
<button style={{
|
||||||
|
width: '100%',
|
||||||
|
fontSize: '0.8rem',
|
||||||
|
padding: '0.4rem 0.8rem',
|
||||||
|
background: 'transparent',
|
||||||
|
border: '1px solid var(--color-gold)',
|
||||||
|
color: 'var(--color-gold)',
|
||||||
|
borderRadius: '6px',
|
||||||
|
cursor: 'pointer'
|
||||||
|
}}>Review</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
338
src/app/dashboard/projects/[id]/planning/page.tsx
Normal file
@@ -0,0 +1,338 @@
|
|||||||
|
'use client';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import styles from '../../projects.module.css';
|
||||||
|
|
||||||
|
// Mock Data
|
||||||
|
const MOCK_SCOPE = {
|
||||||
|
inScope: '• High-fidelity designs for Web & Mobile\n• React Frontend Implementation\n• Node.js Backend API Refactor\n• User Acceptance Testing (UAT)',
|
||||||
|
outScope: '• Legacy Data Migration\n• Native Mobile Apps (iOS/Android)',
|
||||||
|
acceptanceCriteria: '1. All pages must load under 2 seconds.\n2. Design must be responsive on Tablet & Mobile.\n3. 100% Pass rate on critical user flows.\n4. Code coverage > 80%.'
|
||||||
|
};
|
||||||
|
|
||||||
|
const MOCK_TASKS = [
|
||||||
|
{ id: 1, name: '1.0 Design Phase', owner: '', start: '', end: '', duration: '', predecessor: '', isHeader: true },
|
||||||
|
{ id: 2, name: '1.1 Wireframing', owner: 'Anjali Gupta', start: '01 Jan', end: '07 Jan', duration: '7d', predecessor: '-' },
|
||||||
|
{ id: 3, name: '1.2 UI High-Fi', owner: 'Design Team', start: '08 Jan', end: '15 Jan', duration: '8d', predecessor: '1.1' },
|
||||||
|
{ id: 4, name: '2.0 Development Phase', owner: '', start: '', end: '', duration: '', predecessor: '', isHeader: true },
|
||||||
|
{ id: 5, name: '2.1 Frontend Setup', owner: 'Rohan Mehta', start: '16 Jan', end: '20 Jan', duration: '5d', predecessor: '1.2' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const MOCK_BUDGET = [
|
||||||
|
{ id: 1, category: 'Labor Cost', amount: 300000 },
|
||||||
|
{ id: 2, category: 'Software Licenses', amount: 100000 },
|
||||||
|
{ id: 3, category: 'Contingency (10%)', amount: 40000 }
|
||||||
|
];
|
||||||
|
|
||||||
|
const MOCK_RESOURCES = [
|
||||||
|
{ id: 1, name: 'Vikram Singh (PM)', utilization: 50, color: '#00ff9d' },
|
||||||
|
{ id: 2, name: 'Anjali Gupta (Design)', utilization: 110, color: '#ff4444' }
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function PlanningPage() {
|
||||||
|
const [activeTab, setActiveTab] = useState<'scope' | 'wbs' | 'budget' | 'risks'>('scope');
|
||||||
|
|
||||||
|
// Scope State
|
||||||
|
const [isEditingScope, setIsEditingScope] = useState(false);
|
||||||
|
const [scopeData, setScopeData] = useState(MOCK_SCOPE);
|
||||||
|
|
||||||
|
const handleScopeChange = (field: string, value: string) => {
|
||||||
|
setScopeData(prev => ({ ...prev, [field]: value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveScope = () => {
|
||||||
|
setIsEditingScope(false);
|
||||||
|
// Save logic
|
||||||
|
};
|
||||||
|
|
||||||
|
// WBS State
|
||||||
|
const [tasks, setTasks] = useState(MOCK_TASKS);
|
||||||
|
const [isAddingTask, setIsAddingTask] = useState(false);
|
||||||
|
const [newTask, setNewTask] = useState({ name: '', owner: '', start: '', end: '', isHeader: false });
|
||||||
|
|
||||||
|
// Budget State
|
||||||
|
const [budgetItems, setBudgetItems] = useState(MOCK_BUDGET);
|
||||||
|
const [isAddingBudget, setIsAddingBudget] = useState(false);
|
||||||
|
const [newBudget, setNewBudget] = useState({ category: '', amount: 0 });
|
||||||
|
|
||||||
|
const [resources, setResources] = useState(MOCK_RESOURCES);
|
||||||
|
|
||||||
|
|
||||||
|
const handleAddTask = () => {
|
||||||
|
if (!newTask.name) return;
|
||||||
|
const newId = tasks.length + 1;
|
||||||
|
// @ts-ignore
|
||||||
|
setTasks([...tasks, { id: newId, ...newTask, duration: '2d', predecessor: '-' }]);
|
||||||
|
setNewTask({ name: '', owner: '', start: '', end: '', isHeader: false });
|
||||||
|
setIsAddingTask(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddBudget = () => {
|
||||||
|
if (!newBudget.category || newBudget.amount <= 0) return;
|
||||||
|
setBudgetItems([...budgetItems, { id: budgetItems.length + 1, ...newBudget }]);
|
||||||
|
setNewBudget({ category: '', amount: 0 });
|
||||||
|
setIsAddingBudget(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ animation: 'fadeIn 0.5s ease-out' }}>
|
||||||
|
<div className={styles.sectionHeader}>
|
||||||
|
<div>
|
||||||
|
<h2 className={styles.pageTitle}>Planning Phase</h2>
|
||||||
|
<p className={styles.pageSubtitle}>Define scope, schedule, budget, and risk responses.</p>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', gap: '1rem' }}>
|
||||||
|
<button className={styles.btnSecondary}>Lock Baseline 🔒</button>
|
||||||
|
<button className={styles.btnPrimary}>Save Plan</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Sub-Tabs */}
|
||||||
|
<div style={{ display: 'flex', gap: '1rem', borderBottom: '1px solid rgba(255,255,255,0.1)', marginBottom: '2rem' }}>
|
||||||
|
{['Scope Definition', 'WBS & Schedule', 'Budget & Resources', 'Risk Response'].map((tab, index) => {
|
||||||
|
const key = ['scope', 'wbs', 'budget', 'risks'][index] as typeof activeTab;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={key}
|
||||||
|
onClick={() => setActiveTab(key)}
|
||||||
|
style={{
|
||||||
|
background: 'transparent',
|
||||||
|
border: 'none',
|
||||||
|
padding: '10px 20px',
|
||||||
|
color: activeTab === key ? 'var(--color-gold)' : '#888',
|
||||||
|
borderBottom: activeTab === key ? '2px solid var(--color-gold)' : '2px solid transparent',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: '1rem',
|
||||||
|
fontWeight: 500,
|
||||||
|
transition: 'all 0.3s'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tab}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.section}>
|
||||||
|
{/* 4.1 Scope Definition */}
|
||||||
|
{activeTab === 'scope' && (
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '2rem' }}>
|
||||||
|
<div>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1rem' }}>
|
||||||
|
<h3 className={styles.sectionTitle} style={{ marginBottom: 0 }}>✅ In Scope (Deliverables)</h3>
|
||||||
|
{isEditingScope ? (
|
||||||
|
<div style={{ display: 'flex', gap: '0.5rem' }}>
|
||||||
|
<button className={styles.btnSecondary} onClick={() => setIsEditingScope(false)} style={{ padding: '0.4rem 0.8rem', fontSize: '0.8rem' }}>Cancel</button>
|
||||||
|
<button className={styles.btnPrimary} onClick={handleSaveScope} style={{ padding: '0.4rem 0.8rem', fontSize: '0.8rem' }}>Save</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<button className={styles.btnSecondary} onClick={() => setIsEditingScope(true)} style={{ padding: '0.4rem 0.8rem', fontSize: '0.8rem' }}>✏️ Edit</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
{isEditingScope ? (
|
||||||
|
<textarea
|
||||||
|
className={styles.inputField}
|
||||||
|
style={{ minHeight: '150px', background: 'rgba(0,0,0,0.4)' }}
|
||||||
|
value={scopeData.inScope}
|
||||||
|
onChange={(e) => handleScopeChange('inScope', e.target.value)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className={styles.readonlyField}>
|
||||||
|
<ul className={styles.list}>
|
||||||
|
{scopeData.inScope.split('\n').map((item, i) => <li key={i}>{item}</li>)}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 className={styles.sectionTitle} style={{ color: '#ff8888', marginTop: '1.5rem' }}>❌ Out of Scope</h3>
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
{isEditingScope ? (
|
||||||
|
<textarea
|
||||||
|
className={styles.inputField}
|
||||||
|
style={{ minHeight: '100px', background: 'rgba(0,0,0,0.4)' }}
|
||||||
|
value={scopeData.outScope}
|
||||||
|
onChange={(e) => handleScopeChange('outScope', e.target.value)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className={styles.readonlyField}>
|
||||||
|
<ul className={styles.list}>
|
||||||
|
{scopeData.outScope.split('\n').map((item, i) => <li key={i}>{item}</li>)}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className={styles.sectionTitle}>📋 Acceptance Criteria</h3>
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
{isEditingScope ? (
|
||||||
|
<textarea
|
||||||
|
className={styles.inputField}
|
||||||
|
style={{ minHeight: '200px', background: 'rgba(0,0,0,0.4)' }}
|
||||||
|
value={scopeData.acceptanceCriteria}
|
||||||
|
onChange={(e) => handleScopeChange('acceptanceCriteria', e.target.value)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className={styles.readonlyField}>
|
||||||
|
<ul className={styles.list}>
|
||||||
|
{scopeData.acceptanceCriteria.split('\n').map((item, i) => <li key={i}>{item}</li>)}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 4.2 WBS & 4.3 Schedule */}
|
||||||
|
{activeTab === 'wbs' && (
|
||||||
|
<div>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '1rem' }}>
|
||||||
|
<h3 className={styles.sectionTitle}>📅 Work Breakdown & Schedule</h3>
|
||||||
|
<button className={styles.btnSecondary} onClick={() => setIsAddingTask(true)} style={{ fontSize: '0.8rem', padding: '0.4rem 0.8rem' }}>+ Add Task</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isAddingTask && (
|
||||||
|
<div style={{ background: 'rgba(255,255,255,0.05)', padding: '1rem', borderRadius: '8px', marginBottom: '1rem', border: '1px solid var(--color-gold)' }}>
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: '2fr 1fr 1fr 1fr', gap: '1rem' }}>
|
||||||
|
<input placeholder="Task Name" className={styles.inputField} value={newTask.name} onChange={(e) => setNewTask({ ...newTask, name: e.target.value })} />
|
||||||
|
<input placeholder="Owner" className={styles.inputField} value={newTask.owner} onChange={(e) => setNewTask({ ...newTask, owner: e.target.value })} />
|
||||||
|
<input placeholder="Start Date" className={styles.inputField} value={newTask.start} onChange={(e) => setNewTask({ ...newTask, start: e.target.value })} />
|
||||||
|
<input placeholder="End Date" className={styles.inputField} value={newTask.end} onChange={(e) => setNewTask({ ...newTask, end: e.target.value })} />
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '1rem', marginTop: '1rem' }}>
|
||||||
|
<button className={styles.btnSecondary} onClick={() => setIsAddingTask(false)}>Cancel</button>
|
||||||
|
<button className={styles.btnPrimary} onClick={handleAddTask}>Add Task</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div style={{ overflowX: 'auto' }}>
|
||||||
|
<table className={styles.table}>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Task Name</th>
|
||||||
|
<th>Owner</th>
|
||||||
|
<th>Start Date</th>
|
||||||
|
<th>End Date</th>
|
||||||
|
<th>Duration</th>
|
||||||
|
<th>Predecessors</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{tasks.map((task) => (
|
||||||
|
<tr key={task.id}>
|
||||||
|
{task.isHeader ? (
|
||||||
|
<td colSpan={6} style={{ background: 'rgba(255,255,255,0.05)', fontWeight: 600 }}>{task.name}</td>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<td style={{ paddingLeft: '2rem' }}>{task.name}</td>
|
||||||
|
<td>{task.owner}</td>
|
||||||
|
<td>{task.start}</td>
|
||||||
|
<td>{task.end}</td>
|
||||||
|
<td>{task.duration}</td>
|
||||||
|
<td>{task.predecessor}</td>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 4.4 Budget & Resources */}
|
||||||
|
{activeTab === 'budget' && (
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '2rem' }}>
|
||||||
|
<div>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1rem' }}>
|
||||||
|
<h3 className={styles.sectionTitle} style={{ marginBottom: 0 }}>💰 Budget Allocation</h3>
|
||||||
|
<button className={styles.btnSecondary} onClick={() => setIsAddingBudget(true)} style={{ fontSize: '0.8rem', padding: '0.4rem 0.8rem' }}>+ Add Line Item</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isAddingBudget && (
|
||||||
|
<div style={{ background: 'rgba(255,255,255,0.05)', padding: '1rem', borderRadius: '8px', marginBottom: '1rem', border: '1px solid var(--color-gold)' }}>
|
||||||
|
<input placeholder="Category" className={styles.inputField} style={{ marginBottom: '0.5rem' }} value={newBudget.category} onChange={(e) => setNewBudget({ ...newBudget, category: e.target.value })} />
|
||||||
|
<input type="number" placeholder="Amount" className={styles.inputField} value={newBudget.amount} onChange={(e) => setNewBudget({ ...newBudget, amount: Number(e.target.value) })} />
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '1rem', marginTop: '1rem' }}>
|
||||||
|
<button className={styles.btnSecondary} onClick={() => setIsAddingBudget(false)}>Cancel</button>
|
||||||
|
<button className={styles.btnPrimary} onClick={handleAddBudget}>Add Budget</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
|
||||||
|
{budgetItems.map(item => (
|
||||||
|
<div key={item.id} style={{ display: 'flex', justifyContent: 'space-between', padding: '1rem', background: 'rgba(255,255,255,0.03)', borderRadius: '8px' }}>
|
||||||
|
<span>{item.category}</span>
|
||||||
|
<span style={{ fontWeight: 600 }}>₹ {item.amount.toLocaleString()}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', padding: '1rem', background: 'rgba(255,215,0,0.1)', borderRadius: '8px', border: '1px solid var(--color-gold)' }}>
|
||||||
|
<span style={{ color: 'var(--color-gold)', fontWeight: 600 }}>Total Budget</span>
|
||||||
|
<span style={{ color: 'var(--color-gold)', fontWeight: 700, fontSize: '1.1rem' }}>
|
||||||
|
₹ {budgetItems.reduce((acc, curr) => acc + curr.amount, 0).toLocaleString()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className={styles.sectionTitle}>👥 Resource Utilization</h3>
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
|
||||||
|
{resources.map(res => (
|
||||||
|
<div key={res.id} style={{ padding: '1rem', background: 'rgba(255,255,255,0.03)', borderRadius: '8px' }}>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '0.5rem' }}>
|
||||||
|
<span>{res.name}</span>
|
||||||
|
<span style={{ color: res.utilization > 100 ? '#ff4444' : '#00ff9d' }}>{res.utilization}% {res.utilization > 100 ? '(Overallocated)' : ''}</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ height: '6px', background: '#333', borderRadius: '3px' }}>
|
||||||
|
<div style={{ width: `${Math.min(res.utilization, 100)}%`, background: res.color, height: '100%', borderRadius: '3px' }}></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 4.5 Risk Response */}
|
||||||
|
{activeTab === 'risks' && (
|
||||||
|
<div>
|
||||||
|
<h3 className={styles.sectionTitle}>🛡️ Risk Response Plan</h3>
|
||||||
|
<div style={{ display: 'grid', gap: '1rem' }}>
|
||||||
|
{/* Risk 1 */}
|
||||||
|
<div style={{ padding: '1rem', background: 'rgba(255,255,255,0.03)', borderRadius: '8px', borderLeft: '3px solid #ff4444' }}>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
|
||||||
|
<div>
|
||||||
|
<h4 style={{ margin: '0 0 0.5rem 0', color: '#ff8888' }}>Budget Overrun > 10%</h4>
|
||||||
|
<div style={{ fontSize: '0.9rem', color: '#ccc' }}>Strategy: <span style={{ fontWeight: 600, color: '#fff' }}>Mitigate</span></div>
|
||||||
|
</div>
|
||||||
|
<span style={{ background: '#ff4444', color: '#000', padding: '2px 8px', borderRadius: '4px', fontSize: '0.8rem', fontWeight: 600 }}>High Criticality</span>
|
||||||
|
</div>
|
||||||
|
<p style={{ margin: '0.5rem 0 0 0', fontSize: '0.9rem', color: '#888' }}>
|
||||||
|
Action: Monitor weekly spend. Reduce scope of "Nice to have" features if variance > 5% by Week 4.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Risk 2 */}
|
||||||
|
<div style={{ padding: '1rem', background: 'rgba(255,255,255,0.03)', borderRadius: '8px', borderLeft: '3px solid #ffd700' }}>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
|
||||||
|
<div>
|
||||||
|
<h4 style={{ margin: '0 0 0.5rem 0', color: 'var(--color-gold)' }}>Vendor Delay</h4>
|
||||||
|
<div style={{ fontSize: '0.9rem', color: '#ccc' }}>Strategy: <span style={{ fontWeight: 600, color: '#fff' }}>Transfer</span></div>
|
||||||
|
</div>
|
||||||
|
<span style={{ background: 'var(--color-gold)', color: '#000', padding: '2px 8px', borderRadius: '4px', fontSize: '0.8rem', fontWeight: 600 }}>Medium</span>
|
||||||
|
</div>
|
||||||
|
<p style={{ margin: '0.5rem 0 0 0', fontSize: '0.9rem', color: '#888' }}>
|
||||||
|
Action: Include penalty clause in contract for delays > 3 days.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
220
src/app/dashboard/projects/new/page.tsx
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
'use client';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import styles from '../projects.module.css';
|
||||||
|
import wizardStyles from '../wizard.module.css';
|
||||||
|
|
||||||
|
export default function NewProjectPage() {
|
||||||
|
const router = useRouter();
|
||||||
|
const [step, setStep] = useState(1);
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
projectName: '',
|
||||||
|
projectCode: '',
|
||||||
|
description: '',
|
||||||
|
businessGoal: '',
|
||||||
|
successCriteria: '',
|
||||||
|
orgAlignment: 'Strategic Growth', // Default
|
||||||
|
startDate: '',
|
||||||
|
endDate: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
|
||||||
|
const { name, value } = e.target;
|
||||||
|
setFormData(prev => ({ ...prev, [name]: value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNext = () => {
|
||||||
|
if (step < 3) setStep(step + 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBack = () => {
|
||||||
|
if (step > 1) setStep(step - 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
// Here you would make an API call to save the project
|
||||||
|
console.log('Creating Project:', formData);
|
||||||
|
alert('Project "Foundations" Created Successfully! Redirecting to workspace...');
|
||||||
|
router.push('/dashboard/projects/PRJ-NEW-01');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<div className={styles.header}>
|
||||||
|
<div>
|
||||||
|
<h1>New Project: Foundations</h1>
|
||||||
|
<p className={styles.subtitle}>Define the purpose and alignment of your new initiative</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Link href="/dashboard/projects" className={styles.link}>
|
||||||
|
Cancel
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Wizard Progress */}
|
||||||
|
<div style={{ display: 'flex', marginBottom: '2rem', gap: '1rem', alignItems: 'center' }}>
|
||||||
|
<div className={`${wizardStyles.stepIndicator} ${step >= 1 ? wizardStyles.activeStep : ''}`}>1. Basics</div>
|
||||||
|
<div className={wizardStyles.stepLine}></div>
|
||||||
|
<div className={`${wizardStyles.stepIndicator} ${step >= 2 ? wizardStyles.activeStep : ''}`}>2. Foundations</div>
|
||||||
|
<div className={wizardStyles.stepLine}></div>
|
||||||
|
<div className={`${wizardStyles.stepIndicator} ${step >= 3 ? wizardStyles.activeStep : ''}`}>3. Review</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={wizardStyles.wizardCard}>
|
||||||
|
|
||||||
|
{/* Step 1: Basic Info */}
|
||||||
|
{step === 1 && (
|
||||||
|
<div className={wizardStyles.wizardStep}>
|
||||||
|
<h2>Project Basics</h2>
|
||||||
|
<div className={wizardStyles.formGroup}>
|
||||||
|
<label>Project Name *</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="projectName"
|
||||||
|
value={formData.projectName}
|
||||||
|
onChange={handleChange}
|
||||||
|
className={wizardStyles.inputField}
|
||||||
|
placeholder="e.g. Q3 Marketing Campaign"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={wizardStyles.formGrid}>
|
||||||
|
<div className={wizardStyles.formGroup}>
|
||||||
|
<label>Project Code</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="projectCode"
|
||||||
|
value={formData.projectCode}
|
||||||
|
onChange={handleChange}
|
||||||
|
className={wizardStyles.inputField}
|
||||||
|
placeholder="e.g. MKT-2024-Q3"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={wizardStyles.formGroup}>
|
||||||
|
<label>Start Date</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
name="startDate"
|
||||||
|
value={formData.startDate}
|
||||||
|
onChange={handleChange}
|
||||||
|
className={wizardStyles.inputField}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={wizardStyles.formGroup}>
|
||||||
|
<label>Description</label>
|
||||||
|
<textarea
|
||||||
|
name="description"
|
||||||
|
value={formData.description}
|
||||||
|
onChange={handleChange}
|
||||||
|
className={wizardStyles.inputField}
|
||||||
|
rows={4}
|
||||||
|
placeholder="Brief overview of the project scope..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Step 2: Foundations */}
|
||||||
|
{step === 2 && (
|
||||||
|
<div className={wizardStyles.wizardStep}>
|
||||||
|
<h2>Strategic Foundations</h2>
|
||||||
|
<div className={wizardStyles.formGroup}>
|
||||||
|
<label>Business Goal *</label>
|
||||||
|
<textarea
|
||||||
|
name="businessGoal"
|
||||||
|
value={formData.businessGoal}
|
||||||
|
onChange={handleChange}
|
||||||
|
className={wizardStyles.inputField}
|
||||||
|
rows={3}
|
||||||
|
placeholder="What is the primary business objective?"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={wizardStyles.formGroup}>
|
||||||
|
<label>Success Criteria *</label>
|
||||||
|
<textarea
|
||||||
|
name="successCriteria"
|
||||||
|
value={formData.successCriteria}
|
||||||
|
onChange={handleChange}
|
||||||
|
className={wizardStyles.inputField}
|
||||||
|
rows={3}
|
||||||
|
placeholder="How will we measure success?"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={wizardStyles.formGroup}>
|
||||||
|
<label>Organizational Alignment</label>
|
||||||
|
<select
|
||||||
|
name="orgAlignment"
|
||||||
|
value={formData.orgAlignment}
|
||||||
|
onChange={handleChange}
|
||||||
|
className={wizardStyles.inputField}
|
||||||
|
>
|
||||||
|
<option value="Strategic Growth">Strategic Growth</option>
|
||||||
|
<option value="Operational Efficiency">Operational Efficiency</option>
|
||||||
|
<option value="Compliance & Risk">Compliance & Risk</option>
|
||||||
|
<option value="Innovation">Innovation</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Step 3: Review */}
|
||||||
|
{step === 3 && (
|
||||||
|
<div className={wizardStyles.wizardStep}>
|
||||||
|
<h2>Review & Initiate</h2>
|
||||||
|
<div className={wizardStyles.reviewGrid}>
|
||||||
|
<div className={wizardStyles.reviewItem}>
|
||||||
|
<label>Name:</label>
|
||||||
|
<span>{formData.projectName}</span>
|
||||||
|
</div>
|
||||||
|
<div className={wizardStyles.reviewItem}>
|
||||||
|
<label>Code:</label>
|
||||||
|
<span>{formData.projectCode || 'N/A'}</span>
|
||||||
|
</div>
|
||||||
|
<div className={wizardStyles.reviewItem}>
|
||||||
|
<label>Goal:</label>
|
||||||
|
<span>{formData.businessGoal}</span>
|
||||||
|
</div>
|
||||||
|
<div className={wizardStyles.reviewItem}>
|
||||||
|
<label>Alignment:</label>
|
||||||
|
<span>{formData.orgAlignment}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={wizardStyles.alertBox}>
|
||||||
|
⚠️ By clicking "Create Project", you are formally initializing the <b>Foundations Phase</b>.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Navigation Buttons */}
|
||||||
|
<div className={wizardStyles.wizardActions}>
|
||||||
|
<button
|
||||||
|
onClick={handleBack}
|
||||||
|
className={wizardStyles.btnSecondary}
|
||||||
|
disabled={step === 1}
|
||||||
|
>
|
||||||
|
Back
|
||||||
|
</button>
|
||||||
|
{step < 3 ? (
|
||||||
|
<button
|
||||||
|
onClick={handleNext}
|
||||||
|
className={styles.btnPrimary}
|
||||||
|
disabled={
|
||||||
|
(step === 1 && !formData.projectName) ||
|
||||||
|
(step === 2 && (!formData.businessGoal || !formData.successCriteria))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Next Step
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<button onClick={handleSubmit} className={styles.btnPrimary}>
|
||||||
|
Create Project
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
101
src/app/dashboard/projects/page.tsx
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
'use client';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import styles from './projects.module.css';
|
||||||
|
|
||||||
|
export default function ProjectsDashboard() {
|
||||||
|
// Mock Data for Projects
|
||||||
|
const projects = [
|
||||||
|
{ id: 'PRJ-001', name: 'Alpha Redesign', manager: 'Vikram Singh', status: 'In Progress', progress: 45, dueDate: '2024-12-30' },
|
||||||
|
{ id: 'PRJ-002', name: 'Cloud Migration', manager: 'Anjali Gupta', status: 'Planning', progress: 15, dueDate: '2025-02-15' },
|
||||||
|
{ id: 'PRJ-003', name: 'Mobile App Revamp', manager: 'Rohan Mehta', status: 'Completed', progress: 100, dueDate: '2024-11-01' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const getStatusClass = (status: string) => {
|
||||||
|
switch (status.toLowerCase()) {
|
||||||
|
case 'in progress': return styles.status_active;
|
||||||
|
case 'planning': return styles.status_planning;
|
||||||
|
case 'completed': return styles.status_completed;
|
||||||
|
default: return styles.status_delayed;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
{/* Header */}
|
||||||
|
<div className={styles.header}>
|
||||||
|
<div>
|
||||||
|
<h1>Projects</h1>
|
||||||
|
<p className={styles.subtitle}>Manage your portfolio and track progress</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Link href="/dashboard/projects/new" className={styles.btnPrimary}>
|
||||||
|
+ Start New Project
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Stats Overview */}
|
||||||
|
<div className={styles.statsGrid}>
|
||||||
|
<div className={styles.statCard}>
|
||||||
|
<div className={styles.statLabel}>Active Projects</div>
|
||||||
|
<div className={styles.statValue}>12</div>
|
||||||
|
<div style={{ color: '#00ff9d', fontSize: '0.8rem' }}>+2 this month</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.statCard}>
|
||||||
|
<div className={styles.statLabel}>Avg. Progress</div>
|
||||||
|
<div className={styles.statValue}>68%</div>
|
||||||
|
<div style={{ color: '#aaa', fontSize: '0.8rem' }}>On track</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.statCard}>
|
||||||
|
<div className={styles.statLabel}>Critical Risks</div>
|
||||||
|
<div className={styles.statValue}>3</div>
|
||||||
|
<div style={{ color: '#ff4444', fontSize: '0.8rem' }}>Action needed</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.statCard}>
|
||||||
|
<div className={styles.statLabel}>Resource Util.</div>
|
||||||
|
<div className={styles.statValue}>85%</div>
|
||||||
|
<div style={{ color: '#ffd700', fontSize: '0.8rem' }}>Optimal</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Project List */}
|
||||||
|
<div className={styles.projectList}>
|
||||||
|
<div className={styles.tableHeader}>
|
||||||
|
<div>PROJECT NAME</div>
|
||||||
|
<div>MANAGER</div>
|
||||||
|
<div>STATUS</div>
|
||||||
|
<div>PROGRESS</div>
|
||||||
|
<div>DUE DATE</div>
|
||||||
|
</div>
|
||||||
|
{projects.map(project => (
|
||||||
|
<Link href={`/dashboard/projects/${project.id}`} key={project.id} style={{ textDecoration: 'none', color: 'inherit' }}>
|
||||||
|
<div className={styles.projectRow}>
|
||||||
|
<div style={{ fontWeight: 600, color: '#fff' }}>
|
||||||
|
{project.name}
|
||||||
|
<div style={{ fontSize: '0.75rem', color: '#666' }}>{project.id}</div>
|
||||||
|
</div>
|
||||||
|
<div>{project.manager}</div>
|
||||||
|
<div>
|
||||||
|
<span className={`${styles.statusBadge} ${getStatusClass(project.status)}`}>
|
||||||
|
{project.status}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div style={{ background: 'rgba(255,255,255,0.1)', height: '6px', borderRadius: '3px', width: '100px', marginBottom: '5px' }}>
|
||||||
|
<div style={{
|
||||||
|
width: `${project.progress}%`,
|
||||||
|
height: '100%',
|
||||||
|
background: project.progress === 100 ? '#00ff9d' : 'var(--color-gold)',
|
||||||
|
borderRadius: '3px'
|
||||||
|
}}></div>
|
||||||
|
</div>
|
||||||
|
<span style={{ fontSize: '0.8rem' }}>{project.progress}%</span>
|
||||||
|
</div>
|
||||||
|
<div>{project.dueDate}</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
326
src/app/dashboard/projects/projects.module.css
Normal file
@@ -0,0 +1,326 @@
|
|||||||
|
.container {
|
||||||
|
padding: 2rem;
|
||||||
|
color: #fff;
|
||||||
|
animation: fadeIn 0.5s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Restored Header Styles */
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h1 {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
background: linear-gradient(135deg, #fff 0%, #ccc 100%);
|
||||||
|
background-clip: text;
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
margin: 0;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.pageTitle {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
color: #fff;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pageSubtitle {
|
||||||
|
color: #888;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionHeader {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionTitle {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
color: var(--color-gold);
|
||||||
|
font-size: 1.1rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
display: block;
|
||||||
|
color: #888;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.readonlyField {
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: #ddd;
|
||||||
|
line-height: 1.5;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.list {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list li {
|
||||||
|
padding: 0.5rem 0;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
color: #ccc;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list li:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statusBadge {
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status_active {
|
||||||
|
background: rgba(0, 255, 157, 0.1);
|
||||||
|
color: #00ff9d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status_planning {
|
||||||
|
background: rgba(255, 215, 0, 0.1);
|
||||||
|
color: var(--color-gold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status_completed {
|
||||||
|
background: rgba(0, 150, 255, 0.1);
|
||||||
|
color: #0096ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statsGrid {
|
||||||
|
display: grid;
|
||||||
|
/* User requested 4 cards so we force 4 columns, or auto-fit with smaller min-width */
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 1.5rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.statCard {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statCard:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
border-color: var(--color-gold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.statLabel {
|
||||||
|
color: #aaa;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statValue {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableContainer {
|
||||||
|
overflow-x: auto;
|
||||||
|
/* Custom scrollbar triggers */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Wizard Styles are imported via wizard.module.css, keeping minimal here if shared */
|
||||||
|
.btnPrimary {
|
||||||
|
background: var(--color-gold);
|
||||||
|
color: #000;
|
||||||
|
border: none;
|
||||||
|
padding: 0.8rem 1.5rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btnPrimary:hover {
|
||||||
|
box-shadow: 0 0 15px rgba(255, 215, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btnSecondary {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
color: #fff;
|
||||||
|
padding: 0.8rem 1.5rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
background: rgba(20, 30, 40, 0.6);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formGroup {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formGrid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table th {
|
||||||
|
text-align: left;
|
||||||
|
padding: 1rem;
|
||||||
|
color: #888;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table td {
|
||||||
|
padding: 1rem;
|
||||||
|
color: #ddd;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table tbody tr {
|
||||||
|
transition: background 0.2s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table tbody tr:hover {
|
||||||
|
background: rgba(255, 215, 0, 0.05);
|
||||||
|
/* Gold tint on hover */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Restored Project List Styles */
|
||||||
|
.projectList {
|
||||||
|
background: rgba(15, 25, 35, 0.6);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
border-radius: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableHeader {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 2fr 1fr 1fr 1fr 1fr;
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
color: #888;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.projectRow {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 2fr 1fr 1fr 1fr 1fr;
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
align-items: center;
|
||||||
|
transition: all 0.2s;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.projectRow:hover {
|
||||||
|
background: rgba(255, 215, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.projectRow:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status_delayed {
|
||||||
|
background: rgba(255, 50, 50, 0.1);
|
||||||
|
color: #ff4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom Scrollbar for the module */
|
||||||
|
.container ::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container ::-webkit-scrollbar-track {
|
||||||
|
background: rgba(0, 50, 20, 0.3);
|
||||||
|
/* Dark green tint background */
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container ::-webkit-scrollbar-thumb {
|
||||||
|
background: linear-gradient(180deg, var(--color-gold) 0%, #b8860b 100%);
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.container ::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: linear-gradient(180deg, #ffd700 0%, var(--color-gold) 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Update Input Field to match Green & Gold theme */
|
||||||
|
.inputField {
|
||||||
|
background: #051a10;
|
||||||
|
/* Solid dark green background fallback */
|
||||||
|
background: linear-gradient(145deg, rgba(0, 20, 10, 0.9) 0%, rgba(0, 0, 0, 0.95) 100%);
|
||||||
|
padding: 0.8rem 1rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: #e0e0e0;
|
||||||
|
border: 1px solid rgba(0, 255, 157, 0.2);
|
||||||
|
/* Subtle green border */
|
||||||
|
width: 100%;
|
||||||
|
outline: none;
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
font-size: 0.95rem;
|
||||||
|
appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputField:focus {
|
||||||
|
border-color: var(--color-gold);
|
||||||
|
box-shadow: 0 0 15px rgba(0, 255, 157, 0.15);
|
||||||
|
/* Green glow */
|
||||||
|
background: rgba(0, 30, 15, 0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Force dark green option background for native selects */
|
||||||
|
.inputField option {
|
||||||
|
background-color: #051a10 !important;
|
||||||
|
/* Deep Dark Green */
|
||||||
|
color: #ffd700 !important;
|
||||||
|
/* Gold text for options */
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
133
src/app/dashboard/projects/wizard.module.css
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
/* Wizard Styles */
|
||||||
|
.wizardCard {
|
||||||
|
background: rgba(15, 25, 35, 0.6);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 2rem;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wizardStep {
|
||||||
|
animation: fadeIn 0.4s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wizardStep h2 {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
color: var(--color-gold);
|
||||||
|
font-size: 1.5rem;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formGroup {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formGrid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 1.5rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formGroup label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
color: #ccc;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputField {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 8px;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-family: inherit;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputField:focus {
|
||||||
|
border-color: var(--color-gold);
|
||||||
|
outline: none;
|
||||||
|
box-shadow: 0 0 10px rgba(255, 215, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stepIndicator {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #666;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activeStep {
|
||||||
|
color: var(--color-gold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stepLine {
|
||||||
|
height: 1px;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
flex: 1;
|
||||||
|
max-width: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wizardActions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-top: 3rem;
|
||||||
|
padding-top: 1.5rem;
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btnSecondary {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
color: #fff;
|
||||||
|
padding: 0.8rem 1.5rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btnSecondary:hover:not(:disabled) {
|
||||||
|
border-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btnSecondary:disabled,
|
||||||
|
.btnPrimary:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reviewGrid {
|
||||||
|
display: grid;
|
||||||
|
gap: 1rem;
|
||||||
|
background: rgba(255, 255, 255, 0.03);
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reviewItem {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 150px 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reviewItem label {
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alertBox {
|
||||||
|
background: rgba(255, 215, 0, 0.1);
|
||||||
|
border: 1px solid rgba(255, 215, 0, 0.3);
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: #ddd;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
282
src/app/dashboard/responsive.module.css
Normal file
@@ -0,0 +1,282 @@
|
|||||||
|
/* Mobile Responsive Styles */
|
||||||
|
|
||||||
|
/* Hamburger Menu Button */
|
||||||
|
.mobileMenuBtn {
|
||||||
|
display: none;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0.5rem;
|
||||||
|
margin-right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobileMenuBtn span {
|
||||||
|
width: 24px;
|
||||||
|
height: 2px;
|
||||||
|
background: #ffd700;
|
||||||
|
border-radius: 2px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tablet Breakpoint (1024px and below) */
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.topNavbar {
|
||||||
|
padding: 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.companySwitcher {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
padding: 0.4rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leftSidebar {
|
||||||
|
width: 220px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contentArea {
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Grid adjustments */
|
||||||
|
.grid4 {
|
||||||
|
grid-template-columns: repeat(2, 1fr) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid3 {
|
||||||
|
grid-template-columns: repeat(2, 1fr) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile Breakpoint (768px and below) */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.mobileMenuBtn {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topNavbar {
|
||||||
|
padding: 0 1rem;
|
||||||
|
height: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navCenter {
|
||||||
|
position: static;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.companySwitcher {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.onboardingLink {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userProfile {
|
||||||
|
padding: 0.4rem 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userName {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userRole {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mainLayout {
|
||||||
|
margin-top: 60px;
|
||||||
|
height: calc(100vh - 60px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.leftSidebar {
|
||||||
|
position: fixed;
|
||||||
|
left: -260px;
|
||||||
|
top: 60px;
|
||||||
|
height: calc(100vh - 60px);
|
||||||
|
width: 260px;
|
||||||
|
z-index: 999;
|
||||||
|
transition: left 0.3s ease;
|
||||||
|
background: rgba(10, 20, 30, 0.98);
|
||||||
|
border-right: 1px solid rgba(255, 215, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.leftSidebar.mobileOpen {
|
||||||
|
left: 0;
|
||||||
|
box-shadow: 4px 0 20px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.leftSidebar.collapsed {
|
||||||
|
left: -260px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggleBtn {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contentArea {
|
||||||
|
padding: 1rem;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Grid adjustments */
|
||||||
|
.grid4,
|
||||||
|
.grid3,
|
||||||
|
.grid2 {
|
||||||
|
grid-template-columns: 1fr !important;
|
||||||
|
gap: 1rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Page titles */
|
||||||
|
.pageTitle {
|
||||||
|
font-size: 1.5rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pageSubtitle {
|
||||||
|
font-size: 0.85rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tables */
|
||||||
|
.table {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table th,
|
||||||
|
.table td {
|
||||||
|
padding: 0.75rem 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modals */
|
||||||
|
.modal {
|
||||||
|
max-width: 95%;
|
||||||
|
margin: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Filter bars */
|
||||||
|
.filterBar {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filterGroup {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Buttons */
|
||||||
|
.btnPrimary,
|
||||||
|
.btnSecondary {
|
||||||
|
padding: 0.6rem 1.2rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stats/Widgets */
|
||||||
|
.widgetValue,
|
||||||
|
.statValue {
|
||||||
|
font-size: 1.75rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.widgetLabel,
|
||||||
|
.statLabel {
|
||||||
|
font-size: 0.8rem !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Small Mobile (480px and below) */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.topNavbar {
|
||||||
|
padding: 0 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userAvatar {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contentArea {
|
||||||
|
padding: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pageTitle {
|
||||||
|
font-size: 1.3rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionHeader {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btnPrimary,
|
||||||
|
.btnSecondary {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table th,
|
||||||
|
.table td {
|
||||||
|
padding: 0.5rem 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide less important table columns on very small screens */
|
||||||
|
.table th:nth-child(n+5),
|
||||||
|
.table td:nth-child(n+5) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.widgetValue,
|
||||||
|
.statValue {
|
||||||
|
font-size: 1.5rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal {
|
||||||
|
max-width: 100%;
|
||||||
|
margin: 0.5rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modalHeader {
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modalBody {
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modalFooter {
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modalFooter button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Landscape Mobile */
|
||||||
|
@media (max-width: 768px) and (orientation: landscape) {
|
||||||
|
.leftSidebar {
|
||||||
|
top: 60px;
|
||||||
|
height: calc(100vh - 60px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mainLayout {
|
||||||
|
height: calc(100vh - 60px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tablet Landscape */
|
||||||
|
@media (min-width: 769px) and (max-width: 1024px) and (orientation: landscape) {
|
||||||
|
.leftSidebar {
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leftSidebar.collapsed {
|
||||||
|
width: 60px;
|
||||||
|
}
|
||||||
|
}
|
||||||
475
src/app/dashboard/shared/components.module.css
Normal file
@@ -0,0 +1,475 @@
|
|||||||
|
/* ============================================
|
||||||
|
PENNYZO DESIGN SYSTEM - SHARED COMPONENTS
|
||||||
|
Luxury Tech Aesthetic with Diamond Gold
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
/* ========== TYPOGRAPHY ========== */
|
||||||
|
.pageTitle {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0;
|
||||||
|
background: linear-gradient(135deg, #fff 0%, #ffd700 100%);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pageSubtitle {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
color: #888;
|
||||||
|
margin: 0.5rem 0 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionTitle {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
color: #ffd700;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0 0 1.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== CARDS ========== */
|
||||||
|
.card {
|
||||||
|
background: rgba(255, 255, 255, 0.03);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
-webkit-backdrop-filter: blur(10px);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border-color: rgba(255, 215, 0, 0.3);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardHeader {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardTitle {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardBody {
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== BUTTONS ========== */
|
||||||
|
.btnPrimary {
|
||||||
|
background: linear-gradient(135deg, #ffd700 0%, #b8860b 100%);
|
||||||
|
color: #000;
|
||||||
|
border: none;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btnPrimary:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 8px 20px rgba(255, 215, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btnPrimary:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btnSecondary {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
color: #fff;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btnSecondary:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border-color: rgba(255, 215, 0, 0.5);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btnDanger {
|
||||||
|
background: rgba(220, 38, 38, 0.2);
|
||||||
|
border: 1px solid rgba(220, 38, 38, 0.5);
|
||||||
|
color: #ff6b6b;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btnDanger:hover {
|
||||||
|
background: rgba(220, 38, 38, 0.3);
|
||||||
|
border-color: rgba(220, 38, 38, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== FORM INPUTS ========== */
|
||||||
|
.inputField {
|
||||||
|
width: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputField:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: rgba(255, 215, 0, 0.5);
|
||||||
|
box-shadow: 0 0 0 3px rgba(255, 215, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputField::placeholder {
|
||||||
|
color: rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.selectField {
|
||||||
|
width: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selectField:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: rgba(255, 215, 0, 0.5);
|
||||||
|
box-shadow: 0 0 0 3px rgba(255, 215, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.textareaField {
|
||||||
|
width: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
resize: vertical;
|
||||||
|
min-height: 100px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.textareaField:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: rgba(255, 215, 0, 0.5);
|
||||||
|
box-shadow: 0 0 0 3px rgba(255, 215, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #888;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== BADGES ========== */
|
||||||
|
.badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.35rem 0.75rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badgeSuccess {
|
||||||
|
background: rgba(34, 197, 94, 0.2);
|
||||||
|
color: #4ade80;
|
||||||
|
border: 1px solid rgba(34, 197, 94, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.badgeWarning {
|
||||||
|
background: rgba(251, 191, 36, 0.2);
|
||||||
|
color: #fbbf24;
|
||||||
|
border: 1px solid rgba(251, 191, 36, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.badgeDanger {
|
||||||
|
background: rgba(239, 68, 68, 0.2);
|
||||||
|
color: #f87171;
|
||||||
|
border: 1px solid rgba(239, 68, 68, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.badgeInfo {
|
||||||
|
background: rgba(59, 130, 246, 0.2);
|
||||||
|
color: #60a5fa;
|
||||||
|
border: 1px solid rgba(59, 130, 246, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.badgeGold {
|
||||||
|
background: rgba(255, 215, 0, 0.2);
|
||||||
|
color: #ffd700;
|
||||||
|
border: 1px solid rgba(255, 215, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== TABLES ========== */
|
||||||
|
.table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
background: rgba(255, 255, 255, 0.02);
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table thead {
|
||||||
|
background: rgba(255, 215, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table th {
|
||||||
|
color: #ffd700;
|
||||||
|
font-weight: 600;
|
||||||
|
text-align: left;
|
||||||
|
padding: 1rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table td {
|
||||||
|
padding: 1rem;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table tbody tr {
|
||||||
|
transition: background 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table tbody tr:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== MODALS ========== */
|
||||||
|
.modalOverlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.9);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 9999;
|
||||||
|
padding: 2rem;
|
||||||
|
backdrop-filter: blur(5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal {
|
||||||
|
background: linear-gradient(135deg, rgba(15, 20, 30, 0.98) 0%, rgba(25, 30, 40, 0.98) 100%);
|
||||||
|
border: 1px solid rgba(255, 215, 0, 0.2);
|
||||||
|
border-radius: 16px;
|
||||||
|
max-width: 600px;
|
||||||
|
width: 100%;
|
||||||
|
max-height: 90vh;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modalHeader {
|
||||||
|
padding: 1.5rem 2rem;
|
||||||
|
border-bottom: 1px solid rgba(255, 215, 0, 0.2);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modalTitle {
|
||||||
|
font-size: 1.3rem;
|
||||||
|
color: #ffd700;
|
||||||
|
margin: 0;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modalClose {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 2rem;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0.5rem;
|
||||||
|
line-height: 1;
|
||||||
|
transition: color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modalClose:hover {
|
||||||
|
color: #ffd700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modalBody {
|
||||||
|
padding: 2rem;
|
||||||
|
overflow-y: auto;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modalFooter {
|
||||||
|
padding: 1.5rem 2rem;
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== WIDGETS/STATS ========== */
|
||||||
|
.widget {
|
||||||
|
background: rgba(255, 255, 255, 0.03);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.widget:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border-color: rgba(255, 215, 0, 0.3);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.widgetIcon {
|
||||||
|
font-size: 2rem;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.widgetValue {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.widgetLabel {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #888;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== EMPTY STATES ========== */
|
||||||
|
.emptyState {
|
||||||
|
text-align: center;
|
||||||
|
padding: 4rem 2rem;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emptyState>div:first-child {
|
||||||
|
font-size: 4rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== LOADING STATES ========== */
|
||||||
|
.loading {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 3rem;
|
||||||
|
color: #ffd700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
border: 3px solid rgba(255, 215, 0, 0.1);
|
||||||
|
border-top-color: #ffd700;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== GRID LAYOUTS ========== */
|
||||||
|
.grid2 {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid3 {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid4 {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gridAuto {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== RESPONSIVE ========== */
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.grid4 {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid3 {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
|
||||||
|
.grid4,
|
||||||
|
.grid3,
|
||||||
|
.grid2 {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pageTitle {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
src/app/favicon.ico
Normal file
|
After Width: | Height: | Size: 25 KiB |
313
src/app/forgot-password/page.module.css
Normal file
@@ -0,0 +1,313 @@
|
|||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
min-height: 100vh;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: var(--bg-deep);
|
||||||
|
/* Custom Background Image */
|
||||||
|
background-image:
|
||||||
|
linear-gradient(135deg, rgba(5, 50, 25, 0.65), rgba(2, 20, 10, 0.9)),
|
||||||
|
url('/bg_pennyzo.png');
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes bg-drift {
|
||||||
|
0% {
|
||||||
|
background-position: 0% 0%;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
background-position: 100% 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 500px;
|
||||||
|
padding: 3rem;
|
||||||
|
background: rgba(10, 10, 12, 0.6);
|
||||||
|
backdrop-filter: blur(24px);
|
||||||
|
-webkit-backdrop-filter: blur(24px);
|
||||||
|
/* Shining Gold Border */
|
||||||
|
border: 1px solid rgba(255, 215, 0, 0.5);
|
||||||
|
border-top: 1px solid rgba(255, 215, 0, 0.8);
|
||||||
|
border-bottom: 1px solid rgba(255, 215, 0, 0.3);
|
||||||
|
border-radius: 32px;
|
||||||
|
/* Gold Glow */
|
||||||
|
box-shadow:
|
||||||
|
0 24px 64px rgba(0, 0, 0, 0.6),
|
||||||
|
0 0 30px rgba(255, 215, 0, 0.15),
|
||||||
|
inset 0 0 20px rgba(255, 215, 0, 0.05);
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
overflow: visible;
|
||||||
|
/* For dropdown */
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 2.5rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logoContainer {
|
||||||
|
filter: drop-shadow(0 0 15px rgba(255, 215, 0, 0.4));
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
color: #fff;
|
||||||
|
font-family: var(--font-geist-sans);
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formGroup {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.8rem 0;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.15);
|
||||||
|
color: #fff;
|
||||||
|
font-family: var(--font-geist-mono);
|
||||||
|
font-size: 1rem;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
outline: none;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input:focus {
|
||||||
|
border-bottom-color: var(--color-cyan);
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0.6rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
pointer-events: none;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
font-family: var(--font-geist-sans);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input:focus~.label,
|
||||||
|
.input:not(:placeholder-shown)~.label {
|
||||||
|
top: -1rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--color-cyan);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
width: 100%;
|
||||||
|
padding: 1.2rem;
|
||||||
|
border: none;
|
||||||
|
background: var(--gradient-gold);
|
||||||
|
color: #000;
|
||||||
|
font-family: var(--font-geist-sans);
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
margin-top: 1rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 8px 24px rgba(255, 215, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:hover:not(:disabled) {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 12px 32px rgba(255, 215, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:disabled {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
color: rgba(255, 255, 255, 0.2);
|
||||||
|
box-shadow: none;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.backLink {
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 2rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
transition: 0.3s;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.backLink:hover {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dropdown */
|
||||||
|
.mobileInputContainer {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.countrySelect {
|
||||||
|
position: relative;
|
||||||
|
min-width: 130px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selectedCode {
|
||||||
|
padding: 0.8rem 0;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.15);
|
||||||
|
color: #fff;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: var(--font-geist-mono);
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdownMenu {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 0;
|
||||||
|
width: 260px;
|
||||||
|
max-height: 300px;
|
||||||
|
background: rgba(10, 10, 15, 0.95);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 12px;
|
||||||
|
z-index: 100;
|
||||||
|
overflow-y: auto;
|
||||||
|
margin-top: 10px;
|
||||||
|
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom Scrollbar */
|
||||||
|
.dropdownMenu::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdownMenu::-webkit-scrollbar-track {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdownMenu::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--color-cyan);
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchInput {
|
||||||
|
width: 100%;
|
||||||
|
padding: 1rem;
|
||||||
|
background: #0a0a0f;
|
||||||
|
/* Opaque */
|
||||||
|
border: none;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
color: #fff;
|
||||||
|
outline: none;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdownItem {
|
||||||
|
padding: 1rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.02);
|
||||||
|
transition: 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdownItem:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* OTP */
|
||||||
|
.otpGroup {
|
||||||
|
margin-top: 1rem;
|
||||||
|
padding: 1.5rem;
|
||||||
|
background: rgba(255, 255, 255, 0.02);
|
||||||
|
border-radius: 16px;
|
||||||
|
border: 1px solid rgba(0, 255, 234, 0.2);
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Notifications */
|
||||||
|
.notificationContainer {
|
||||||
|
position: fixed;
|
||||||
|
top: 2rem;
|
||||||
|
right: 2rem;
|
||||||
|
z-index: 9999;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification {
|
||||||
|
background: rgba(5, 5, 5, 0.85);
|
||||||
|
/* Deep dark glass */
|
||||||
|
backdrop-filter: blur(16px);
|
||||||
|
border-left: 4px solid var(--color-cyan);
|
||||||
|
border-radius: 8px;
|
||||||
|
/* Clean luxury radius */
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
min-width: 320px;
|
||||||
|
color: #fff;
|
||||||
|
font-family: var(--font-geist-sans);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.4);
|
||||||
|
animation: slideIn 0.4s cubic-bezier(0.2, 0.8, 0.2, 1);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notifySuccess {
|
||||||
|
border-left-color: var(--color-green);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notifyError {
|
||||||
|
border-left-color: #ff4d4d;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideIn {
|
||||||
|
from {
|
||||||
|
transform: translateX(50px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
transform: translateX(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.frozenInput {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
265
src/app/forgot-password/page.tsx
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useMemo, useRef, useEffect } from 'react';
|
||||||
|
import Image from 'next/image';
|
||||||
|
import styles from "./page.module.css";
|
||||||
|
import { countryCodes } from "@/data/countryCodes";
|
||||||
|
|
||||||
|
type NotificationType = 'success' | 'error' | 'info';
|
||||||
|
|
||||||
|
interface Notification {
|
||||||
|
id: number;
|
||||||
|
message: string;
|
||||||
|
type: NotificationType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ForgotPassword() {
|
||||||
|
const [selectedCountry, setSelectedCountry] = useState(countryCodes.find(c => c.iso === 'IN') || countryCodes[0]);
|
||||||
|
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||||
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// Step: 0 = Enter Mobile, 1 = Verify OTP, 2 = Reset Password
|
||||||
|
const [step, setStep] = useState(0);
|
||||||
|
|
||||||
|
// Fields
|
||||||
|
const [mobileNumber, setMobileNumber] = useState("");
|
||||||
|
const [otpInput, setOtpInput] = useState("");
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
|
const [confirmPassword, setConfirmPassword] = useState("");
|
||||||
|
|
||||||
|
// Notifications
|
||||||
|
const [notifications, setNotifications] = useState<Notification[]>([]);
|
||||||
|
|
||||||
|
const showNotification = (message: string, type: NotificationType = 'info') => {
|
||||||
|
const id = Date.now();
|
||||||
|
setNotifications(prev => [...prev, { id, message, type }]);
|
||||||
|
setTimeout(() => {
|
||||||
|
setNotifications(prev => prev.filter(n => n.id !== id));
|
||||||
|
}, 4000);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Filter countries
|
||||||
|
const filteredCountries = useMemo(() => {
|
||||||
|
return countryCodes.filter(country =>
|
||||||
|
country.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
|
country.code.includes(searchQuery)
|
||||||
|
);
|
||||||
|
}, [searchQuery]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function handleClickOutside(event: MouseEvent) {
|
||||||
|
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
||||||
|
setDropdownOpen(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.addEventListener("mousedown", handleClickOutside);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("mousedown", handleClickOutside);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleMobileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const value = e.target.value.replace(/\D/g, '');
|
||||||
|
if (value.length <= 10) {
|
||||||
|
setMobileNumber(value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Step 1: Send OTP
|
||||||
|
const handleSendOtp = () => {
|
||||||
|
if (mobileNumber.length !== 10) {
|
||||||
|
showNotification("Please enter a valid 10-digit mobile number", 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Simulate API call
|
||||||
|
showNotification(`OTP sent to ${selectedCountry.code} ${mobileNumber}`, 'info');
|
||||||
|
setStep(1);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Step 2: Verify OTP
|
||||||
|
const handleVerifyOtp = () => {
|
||||||
|
if (otpInput === "123456") {
|
||||||
|
showNotification("Identity Verified", 'success');
|
||||||
|
setStep(2);
|
||||||
|
} else {
|
||||||
|
showNotification("Invalid OTP Code", 'error');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Step 3: Reset Password
|
||||||
|
const handleResetPassword = () => {
|
||||||
|
if (!password || !confirmPassword) {
|
||||||
|
showNotification("Both password fields are required", 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (password.length < 8) {
|
||||||
|
showNotification("Password must be at least 8 characters", 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (password !== confirmPassword) {
|
||||||
|
showNotification("Passwords do not match", 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showNotification("Password successfully updated. redirecting...", 'success');
|
||||||
|
// Mock redirect logic
|
||||||
|
setTimeout(() => window.location.href = "/", 2000);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
|
||||||
|
<div className={styles.notificationContainer}>
|
||||||
|
{notifications.map(n => (
|
||||||
|
<div key={n.id} className={`${styles.notification} ${n.type === 'success' ? styles.notifySuccess : n.type === 'error' ? styles.notifyError : ''}`}>
|
||||||
|
<div style={{ fontSize: '0.75rem', opacity: 0.7, textTransform: 'uppercase', marginBottom: '4px' }}>
|
||||||
|
{n.type === 'error' ? 'System Alert' : n.type === 'success' ? 'Success' : 'Indication'}
|
||||||
|
</div>
|
||||||
|
{n.message}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.card}>
|
||||||
|
<div className={styles.header}>
|
||||||
|
<div className={styles.logoContainer}>
|
||||||
|
<Image
|
||||||
|
src="/logo_symbol.png"
|
||||||
|
alt="Pennyzo"
|
||||||
|
width={120}
|
||||||
|
height={120}
|
||||||
|
style={{ width: 'auto', height: '90px', objectFit: 'contain' }}
|
||||||
|
priority
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<h1 className={styles.title} style={{ fontSize: '1.5rem', marginTop: '0.5rem', fontWeight: 800, letterSpacing: '2px' }}>PENNYZO</h1>
|
||||||
|
<p className={styles.subtitle} style={{ marginBottom: '1rem', color: 'var(--color-gold)' }}>Recovery Protocol</p>
|
||||||
|
<p className={styles.subtitle}>
|
||||||
|
{step === 0 && "Enter your registered mobile number to receive an authentication code."}
|
||||||
|
{step === 1 && `Enter the code sent to ${selectedCountry.code} ${mobileNumber}`}
|
||||||
|
{step === 2 && "Create a new strong password to secure your account."}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* STEP 0 & 1: Mobile & OTP */}
|
||||||
|
{(step === 0 || step === 1) && (
|
||||||
|
<>
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<div className={styles.mobileInputContainer}>
|
||||||
|
<div className={styles.countrySelect} ref={dropdownRef}>
|
||||||
|
<div
|
||||||
|
className={`${styles.selectedCode} ${step === 1 ? styles.frozenInput : ''}`}
|
||||||
|
onClick={() => step === 0 && setDropdownOpen(!dropdownOpen)}
|
||||||
|
>
|
||||||
|
<span>{selectedCountry.iso} {selectedCountry.code}</span>
|
||||||
|
{step === 0 && <span style={{ fontSize: '0.7rem', opacity: 0.5 }}>▼</span>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{dropdownOpen && (
|
||||||
|
<div className={styles.dropdownMenu}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className={styles.searchInput}
|
||||||
|
placeholder="Search..."
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
{filteredCountries.map((country, index) => (
|
||||||
|
<div
|
||||||
|
key={`${country.iso}-${index}`}
|
||||||
|
className={styles.dropdownItem}
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedCountry(country);
|
||||||
|
setDropdownOpen(false);
|
||||||
|
setSearchQuery("");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>{country.name}</span>
|
||||||
|
<span style={{ color: 'var(--color-cyan)' }}>{country.code}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ flex: 1, position: 'relative' }}>
|
||||||
|
<input
|
||||||
|
type="tel"
|
||||||
|
className={`${styles.input} ${step === 1 ? styles.frozenInput : ''}`}
|
||||||
|
placeholder=" "
|
||||||
|
value={mobileNumber}
|
||||||
|
onChange={handleMobileChange}
|
||||||
|
readOnly={step === 1}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<label className={styles.label}>Mobile Number</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{step === 0 && (
|
||||||
|
<button type="button" className={styles.button} onClick={handleSendOtp}>
|
||||||
|
Send OTP
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{step === 1 && (
|
||||||
|
<div className={styles.otpGroup}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className={styles.input}
|
||||||
|
maxLength={6}
|
||||||
|
placeholder="000000"
|
||||||
|
style={{ textAlign: 'center', letterSpacing: '8px', fontSize: '1.5rem', marginBottom: '1rem' }}
|
||||||
|
value={otpInput}
|
||||||
|
onChange={(e) => setOtpInput(e.target.value.replace(/\D/g, ''))}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
<button type="button" className={styles.button} onClick={handleVerifyOtp} style={{ background: 'var(--color-green)' }}>
|
||||||
|
Verify Code
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* STEP 2: Password Reset */}
|
||||||
|
{step === 2 && (
|
||||||
|
<div className="fade-in">
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
id="newPass"
|
||||||
|
className={styles.input}
|
||||||
|
placeholder=" "
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
/>
|
||||||
|
<label htmlFor="newPass" className={styles.label}>New Password</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
id="confirmPass"
|
||||||
|
className={styles.input}
|
||||||
|
placeholder=" "
|
||||||
|
value={confirmPassword}
|
||||||
|
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||||
|
/>
|
||||||
|
<label htmlFor="confirmPass" className={styles.label}>Confirm Password</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="button" className={styles.button} onClick={handleResetPassword}>
|
||||||
|
Update Password
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<a href="/" className={styles.backLink}>Back to Login</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
172
src/app/globals.css
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
:root {
|
||||||
|
/* Lux Business + FinTech Palette */
|
||||||
|
--bg-deep: #020404;
|
||||||
|
/* Ultra Deep Void */
|
||||||
|
|
||||||
|
/* Primary Accents (Existing Palette) */
|
||||||
|
--color-cyan: #00ffea;
|
||||||
|
--color-gold: #ffd700;
|
||||||
|
--color-green: #00ff9d;
|
||||||
|
--color-void: #000000;
|
||||||
|
|
||||||
|
/* Glass Variables - Liquid Transparency */
|
||||||
|
--glass-surface: rgba(255, 255, 255, 0.03);
|
||||||
|
--glass-border: rgba(255, 255, 255, 0.08);
|
||||||
|
--glass-highlight: rgba(255, 255, 255, 0.15);
|
||||||
|
--glass-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
|
||||||
|
|
||||||
|
/* Text Colors */
|
||||||
|
--text-primary: #ffffff;
|
||||||
|
--text-secondary: rgba(255, 255, 255, 0.7);
|
||||||
|
--text-muted: rgba(255, 255, 255, 0.4);
|
||||||
|
|
||||||
|
/* Fonts */
|
||||||
|
--font-geist-sans: 'Geist Sans', sans-serif;
|
||||||
|
--font-geist-mono: 'Geist Mono', monospace;
|
||||||
|
|
||||||
|
/* Gradients */
|
||||||
|
--gradient-gold: linear-gradient(135deg, #ffd700, #b8860b);
|
||||||
|
--gradient-cyan: linear-gradient(135deg, #00ffea, #008b8b);
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
max-width: 100vw;
|
||||||
|
overflow-x: hidden;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
color: var(--text-primary);
|
||||||
|
background: var(--bg-deep);
|
||||||
|
font-family: var(--font-geist-sans);
|
||||||
|
/* Switch to Sans for Business Luxury */
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
/* Smooth fonts for luxury feel */
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Liquid Glass Utility Class */
|
||||||
|
.liquid-glass {
|
||||||
|
background: var(--glass-surface);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
-webkit-backdrop-filter: blur(20px);
|
||||||
|
border: 1px solid var(--glass-border);
|
||||||
|
box-shadow: var(--glass-shadow);
|
||||||
|
border-radius: 24px;
|
||||||
|
/* Soft rounded corners */
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: all 0.4s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Subtle Shimmer Effect on Hover */
|
||||||
|
.liquid-glass::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: -100%;
|
||||||
|
width: 50%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg,
|
||||||
|
transparent,
|
||||||
|
rgba(255, 255, 255, 0.05),
|
||||||
|
transparent);
|
||||||
|
transform: skewX(-25deg);
|
||||||
|
transition: 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.liquid-glass:hover::before {
|
||||||
|
left: 100%;
|
||||||
|
transition: 0.7s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Glitch Animation (Kept minimal for logo) */
|
||||||
|
@keyframes glitch-skew {
|
||||||
|
0% {
|
||||||
|
transform: skew(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
20% {
|
||||||
|
transform: skew(-1deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
40% {
|
||||||
|
transform: skew(1deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
60% {
|
||||||
|
transform: skew(-0.5deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
80% {
|
||||||
|
transform: skew(0.5deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: skew(0deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.glitch-text {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
/* Reduced intensity for business class */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Input Auto-fill fix for dark theme */
|
||||||
|
input:-webkit-autofill,
|
||||||
|
input:-webkit-autofill:hover,
|
||||||
|
input:-webkit-autofill:focus,
|
||||||
|
input:-webkit-autofill:active {
|
||||||
|
-webkit-box-shadow: 0 0 0 30px #050505 inset !important;
|
||||||
|
background: rgba(5, 26, 21, 0.4);
|
||||||
|
/* Deep Green Tint */
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Luxury Scrollbar Theme (Universal) */
|
||||||
|
* {
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: rgba(0, 255, 157, 0.4) rgba(5, 26, 21, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
*::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
*::-webkit-scrollbar-track {
|
||||||
|
background: rgba(5, 26, 21, 0.4);
|
||||||
|
/* Deep Green Tint */
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
*::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(0, 255, 157, 0.3);
|
||||||
|
/* Muted Green */
|
||||||
|
border-radius: 3px;
|
||||||
|
border: 1px solid rgba(0, 255, 157, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
*::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: var(--color-gold);
|
||||||
|
/* Gold on Hover */
|
||||||
|
box-shadow: 0 0 8px rgba(255, 215, 0, 0.5);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Corner for dual scrollbars */
|
||||||
|
*::-webkit-scrollbar-corner {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
38
src/app/layout.tsx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
import { Geist, Geist_Mono } from "next/font/google";
|
||||||
|
import "./globals.css";
|
||||||
|
import { ToastProvider } from "@/context/ToastContext";
|
||||||
|
import { LanguageProvider } from "@/context/LanguageContext";
|
||||||
|
|
||||||
|
const geistSans = Geist({
|
||||||
|
variable: "--font-geist-sans",
|
||||||
|
subsets: ["latin"],
|
||||||
|
});
|
||||||
|
|
||||||
|
const geistMono = Geist_Mono({
|
||||||
|
variable: "--font-geist-mono",
|
||||||
|
subsets: ["latin"],
|
||||||
|
});
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Pennyzo - Business Management Suite",
|
||||||
|
description: "Complete business management solution",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function RootLayout({
|
||||||
|
children,
|
||||||
|
}: Readonly<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
}>) {
|
||||||
|
return (
|
||||||
|
<html lang="en">
|
||||||
|
<body className={`${geistSans.variable} ${geistMono.variable}`}>
|
||||||
|
<LanguageProvider>
|
||||||
|
<ToastProvider>
|
||||||
|
{children}
|
||||||
|
</ToastProvider>
|
||||||
|
</LanguageProvider>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
311
src/app/onboarding/page.module.css
Normal file
@@ -0,0 +1,311 @@
|
|||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
min-height: 100vh;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: var(--bg-deep);
|
||||||
|
background-image:
|
||||||
|
linear-gradient(135deg, rgba(5, 50, 25, 0.65), rgba(2, 20, 10, 0.9)),
|
||||||
|
url('/bg_pennyzo.png');
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.onboardingCard {
|
||||||
|
background: rgba(10, 20, 30, 0.3);
|
||||||
|
backdrop-filter: blur(24px);
|
||||||
|
-webkit-backdrop-filter: blur(24px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
border-radius: 24px;
|
||||||
|
padding: 3rem;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 900px;
|
||||||
|
box-shadow:
|
||||||
|
0 20px 50px rgba(0, 0, 0, 0.4),
|
||||||
|
inset 0 0 0 1px rgba(255, 255, 255, 0.05);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
animation: fade-in 0.6s cubic-bezier(0.2, 0.8, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.onboardingCard::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
border-radius: 24px;
|
||||||
|
padding: 1px;
|
||||||
|
background: linear-gradient(135deg, rgba(255, 215, 0, 0.3), transparent 40%, transparent 60%, rgba(0, 255, 255, 0.2));
|
||||||
|
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
|
||||||
|
mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
|
||||||
|
-webkit-mask-composite: xor;
|
||||||
|
mask-composite: exclude;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
background: linear-gradient(180deg, #fff, #bbb);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
font-size: 1rem;
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Selection State */
|
||||||
|
.selectionContainer {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.optionCard {
|
||||||
|
background: rgba(255, 255, 255, 0.03);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 2.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.optionCard:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.06);
|
||||||
|
transform: translateY(-5px);
|
||||||
|
border-color: var(--color-gold);
|
||||||
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.iconWrapper {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(255, 215, 0, 0.1);
|
||||||
|
/* Gold tint default */
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
font-size: 2rem;
|
||||||
|
border: 1px solid rgba(255, 215, 0, 0.2);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.optionCard:hover .iconWrapper {
|
||||||
|
background: var(--color-gold);
|
||||||
|
color: #000;
|
||||||
|
box-shadow: 0 0 20px rgba(255, 215, 0, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.optionTitle {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.optionDesc {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Blue tint for Join option */
|
||||||
|
.optionCard.join .iconWrapper {
|
||||||
|
background: rgba(0, 255, 255, 0.1);
|
||||||
|
border-color: rgba(0, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.optionCard.join:hover {
|
||||||
|
border-color: var(--color-cyan);
|
||||||
|
}
|
||||||
|
|
||||||
|
.optionCard.join:hover .iconWrapper {
|
||||||
|
background: var(--color-cyan);
|
||||||
|
box-shadow: 0 0 20px rgba(0, 255, 255, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form Styles (Shared) */
|
||||||
|
.formContainer {
|
||||||
|
animation: slide-up 0.4s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formGrid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 1.5rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formGroup {
|
||||||
|
position: relative;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input,
|
||||||
|
.select {
|
||||||
|
width: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1rem 1rem 0.5rem;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 1rem;
|
||||||
|
outline: none;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
height: 56px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
appearance: none;
|
||||||
|
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23ffd700' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e");
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: right 1rem center;
|
||||||
|
background-size: 1em;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select option {
|
||||||
|
background-color: #0a141e;
|
||||||
|
/* Deep dark background */
|
||||||
|
color: #fff;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input:focus,
|
||||||
|
.select:focus {
|
||||||
|
border-color: var(--color-gold);
|
||||||
|
background: rgba(10, 20, 30, 0.6);
|
||||||
|
box-shadow: 0 0 20px rgba(255, 215, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
position: absolute;
|
||||||
|
left: 1rem;
|
||||||
|
top: 1rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
pointer-events: none;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input:focus~.label,
|
||||||
|
.input:not(:placeholder-shown)~.label,
|
||||||
|
.select:focus~.label,
|
||||||
|
.select:not([value=""])~.label {
|
||||||
|
top: 4px;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: var(--color-gold);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Actions */
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.backBtn {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
transition: color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.backBtn:hover {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submitBtn {
|
||||||
|
background: linear-gradient(135deg, var(--color-gold), #b8860b);
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0.8rem 2rem;
|
||||||
|
color: #000;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submitBtn:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 5px 15px rgba(255, 215, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.submitBtn:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fade-in {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slide-up {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.selectionContainer {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formGrid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.onboardingCard {
|
||||||
|
padding: 1.5rem;
|
||||||
|
margin: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
360
src/app/onboarding/page.tsx
Normal file
@@ -0,0 +1,360 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import Image from 'next/image';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import styles from "./page.module.css";
|
||||||
|
import { indianLocations } from "@/data/indianLocations";
|
||||||
|
import { businessTypes } from "@/data/businessTypes";
|
||||||
|
|
||||||
|
type NotificationType = 'success' | 'error' | 'info';
|
||||||
|
interface Notification {
|
||||||
|
id: number;
|
||||||
|
message: string;
|
||||||
|
type: NotificationType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Onboarding() {
|
||||||
|
const router = useRouter();
|
||||||
|
const [view, setView] = useState<'selection' | 'create' | 'join'>('selection');
|
||||||
|
|
||||||
|
// Create Company State
|
||||||
|
const [companyName, setCompanyName] = useState("");
|
||||||
|
const [businessType, setBusinessType] = useState("");
|
||||||
|
const [industry, setIndustry] = useState("");
|
||||||
|
const [size, setSize] = useState("");
|
||||||
|
const [city, setCity] = useState("");
|
||||||
|
const [state, setState] = useState("");
|
||||||
|
|
||||||
|
// Compliance State
|
||||||
|
const [complianceType, setComplianceType] = useState("");
|
||||||
|
const [complianceId, setComplianceId] = useState("");
|
||||||
|
|
||||||
|
// Derived Data
|
||||||
|
// Filter cities based on selected state if available, else show all or empty
|
||||||
|
// const availableCities = indianLocations.find(s => s.name === state)?.cities || [];
|
||||||
|
|
||||||
|
// Join Company State
|
||||||
|
const [inviteCode, setInviteCode] = useState("");
|
||||||
|
|
||||||
|
// Notifications
|
||||||
|
const [notifications, setNotifications] = useState<Notification[]>([]);
|
||||||
|
|
||||||
|
const showNotification = (message: string, type: NotificationType = 'info') => {
|
||||||
|
const id = Date.now();
|
||||||
|
setNotifications(prev => [...prev, { id, message, type }]);
|
||||||
|
setTimeout(() => {
|
||||||
|
setNotifications(prev => prev.filter(n => n.id !== id));
|
||||||
|
}, 4000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreateSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
// Simulate API call
|
||||||
|
if (!companyName || !businessType || !industry || !size || !state || !city) {
|
||||||
|
showNotification("Please fill in all required fields.", 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showNotification("Creating your workspace...", 'success');
|
||||||
|
|
||||||
|
// Mock success redirect
|
||||||
|
setTimeout(() => {
|
||||||
|
// Ideally redirect to dashboard, for now just show success
|
||||||
|
console.log("Company Created:", {
|
||||||
|
companyName,
|
||||||
|
businessType,
|
||||||
|
industry,
|
||||||
|
size,
|
||||||
|
city,
|
||||||
|
state,
|
||||||
|
compliance: complianceType ? { type: complianceType, id: complianceId } : null,
|
||||||
|
country: "India",
|
||||||
|
currency: "INR"
|
||||||
|
});
|
||||||
|
// Redirect to dashboard
|
||||||
|
router.push('/dashboard');
|
||||||
|
}, 1500);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleJoinSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!inviteCode) {
|
||||||
|
showNotification("Please enter an invite code.", 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showNotification("Verifying invitation...", 'info');
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
showNotification("Successfully joined company!", 'success');
|
||||||
|
}, 1500);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
|
||||||
|
{/* Notification Toast (Reused logic) */}
|
||||||
|
<div style={{ position: 'fixed', top: '20px', right: '20px', zIndex: 1000, display: 'flex', flexDirection: 'column', gap: '10px' }}>
|
||||||
|
{notifications.map(n => (
|
||||||
|
<div key={n.id} style={{
|
||||||
|
background: n.type === 'success' ? 'rgba(0, 200, 100, 0.9)' : n.type === 'error' ? 'rgba(255, 80, 80, 0.9)' : 'rgba(0, 150, 255, 0.9)',
|
||||||
|
backdropFilter: 'blur(10px)',
|
||||||
|
padding: '12px 20px',
|
||||||
|
borderRadius: '8px',
|
||||||
|
color: 'white',
|
||||||
|
boxShadow: '0 5px 15px rgba(0,0,0,0.2)',
|
||||||
|
minWidth: '250px',
|
||||||
|
fontSize: '0.9rem',
|
||||||
|
animation: 'fadeIn 0.3s ease'
|
||||||
|
}}>
|
||||||
|
{n.message}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.onboardingCard}>
|
||||||
|
|
||||||
|
{/* Header with Logo */}
|
||||||
|
<div className={styles.header}>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'center', marginBottom: '1rem' }}>
|
||||||
|
<Image
|
||||||
|
src="/logo_symbol.png"
|
||||||
|
alt="Pennyzo"
|
||||||
|
width={80}
|
||||||
|
height={80}
|
||||||
|
style={{ width: 'auto', height: '64px', objectFit: 'contain' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<h1 className={styles.title}>
|
||||||
|
{view === 'selection' && "Welcome, Founder"}
|
||||||
|
{view === 'create' && "Establish Entity"}
|
||||||
|
{view === 'join' && "Join Structure"}
|
||||||
|
</h1>
|
||||||
|
<p className={styles.subtitle}>
|
||||||
|
{view === 'selection' && "Choose your path to begin using the Business 360 Platform."}
|
||||||
|
{view === 'create' && "Register a new business entity to manage operations, finance, and teams."}
|
||||||
|
{view === 'join' && "Enter your invitation code provided by your organization administrator."}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* VIEW 1: SELECTION */}
|
||||||
|
{view === 'selection' && (
|
||||||
|
<div className={styles.selectionContainer}>
|
||||||
|
|
||||||
|
{/* Option: Create Company */}
|
||||||
|
<div className={styles.optionCard} onClick={() => setView('create')}>
|
||||||
|
<div className={styles.iconWrapper}>
|
||||||
|
<span>+</span>
|
||||||
|
</div>
|
||||||
|
<h3 className={styles.optionTitle}>Create Company</h3>
|
||||||
|
<p className={styles.optionDesc}>
|
||||||
|
Register a new business organization. You will be assigned the <strong>Founder</strong> role with full administrative controls.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Option: Join Company */}
|
||||||
|
<div className={` ${styles.optionCard} ${styles.join} `} onClick={() => setView('join')}>
|
||||||
|
<div className={styles.iconWrapper}>
|
||||||
|
<span>→</span>
|
||||||
|
</div>
|
||||||
|
<h3 className={styles.optionTitle}>Join Existing</h3>
|
||||||
|
<p className={styles.optionDesc}>
|
||||||
|
Join a team using an invitation code or request access. You will be assigned a role based on the invite.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* VIEW 2: CREATE COMPANY FORM */}
|
||||||
|
{view === 'create' && (
|
||||||
|
<div className={styles.formContainer}>
|
||||||
|
<form onSubmit={handleCreateSubmit}>
|
||||||
|
<div className={styles.formGrid}>
|
||||||
|
{/* Company Name */}
|
||||||
|
<div className={styles.formGroup} style={{ gridColumn: 'span 2' }}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className={styles.input}
|
||||||
|
placeholder=" "
|
||||||
|
value={companyName}
|
||||||
|
onChange={e => setCompanyName(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<label className={styles.label}>Company/Trade Name</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Business Type */}
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<select
|
||||||
|
className={styles.select}
|
||||||
|
value={businessType}
|
||||||
|
onChange={e => setBusinessType(e.target.value)}
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<option value="" disabled>Select Business Type</option>
|
||||||
|
{businessTypes.map(bt => (
|
||||||
|
<option key={bt.id} value={bt.id}>{bt.label}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<label className={styles.label} style={{ top: '4px', fontSize: '0.7rem', color: 'var(--color-gold)' }}>Legal Structure</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Industry */}
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<select
|
||||||
|
className={styles.select}
|
||||||
|
value={industry}
|
||||||
|
onChange={e => setIndustry(e.target.value)}
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<option value="" disabled>Select Industry</option>
|
||||||
|
<option value="tech">Technology & SaaS</option>
|
||||||
|
<option value="finance">Finance & Banking</option>
|
||||||
|
<option value="retail">Retail & E-commerce</option>
|
||||||
|
<option value="healthcare">Healthcare</option>
|
||||||
|
<option value="manufacturing">Manufacturing</option>
|
||||||
|
<option value="consulting">Consulting & Services</option>
|
||||||
|
<option value="education">Education & EdTech</option>
|
||||||
|
<option value="construction">Real Estate & Construction</option>
|
||||||
|
</select>
|
||||||
|
<label className={styles.label} style={{ top: '4px', fontSize: '0.7rem', color: 'var(--color-gold)' }}>Industry Sector</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Company Size */}
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<select
|
||||||
|
className={styles.select}
|
||||||
|
value={size}
|
||||||
|
onChange={e => setSize(e.target.value)}
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<option value="" disabled>Employee Count</option>
|
||||||
|
<option value="1-10">1 - 10 Employees</option>
|
||||||
|
<option value="11-50">11 - 50 Employees</option>
|
||||||
|
<option value="51-200">51 - 200 Employees</option>
|
||||||
|
<option value="201-500">201 - 500 Employees</option>
|
||||||
|
<option value="500+">500+ Employees</option>
|
||||||
|
</select>
|
||||||
|
<label className={styles.label} style={{ top: '4px', fontSize: '0.7rem', color: 'var(--color-gold)' }}>Business Size</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* State Selection */}
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<select
|
||||||
|
className={styles.select}
|
||||||
|
value={state}
|
||||||
|
onChange={e => {
|
||||||
|
setState(e.target.value);
|
||||||
|
setCity(""); // Reset city on state change
|
||||||
|
}}
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<option value="" disabled>Select State</option>
|
||||||
|
{indianLocations.map((s, i) => (
|
||||||
|
<option key={i} value={s.name}>{s.name}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<label className={styles.label} style={{ top: '4px', fontSize: '0.7rem', color: 'var(--color-gold)' }}>State</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* City Selection (Dependent) */}
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<select
|
||||||
|
className={styles.select}
|
||||||
|
value={city}
|
||||||
|
onChange={e => setCity(e.target.value)}
|
||||||
|
required
|
||||||
|
disabled={!state}
|
||||||
|
>
|
||||||
|
<option value="" disabled>Select City</option>
|
||||||
|
{indianLocations.find(s => s.name === state)?.cities.map((c: string, i: number) => (
|
||||||
|
<option key={i} value={c}>{c}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<label className={styles.label} style={{ top: '4px', fontSize: '0.7rem', color: 'var(--color-gold)' }}>City</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Country (Locked) */}
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className={styles.input}
|
||||||
|
value="India"
|
||||||
|
readOnly
|
||||||
|
style={{ opacity: 0.7, cursor: 'not-allowed', textAlign: 'center' }}
|
||||||
|
/>
|
||||||
|
<label className={styles.label} style={{ top: '4px', fontSize: '0.7rem', color: 'var(--color-gold)' }}>Country (Locked)</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Compliance Section (Optional) */}
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<select
|
||||||
|
className={styles.select}
|
||||||
|
value={complianceType}
|
||||||
|
onChange={e => {
|
||||||
|
setComplianceType(e.target.value);
|
||||||
|
setComplianceId(""); // Reset ID on type change
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value="">No Compliance (Optional)</option>
|
||||||
|
<option value="msme">MSME Udyam</option>
|
||||||
|
<option value="gstin">GSTIN</option>
|
||||||
|
<option value="cin">CIN</option>
|
||||||
|
</select>
|
||||||
|
<label className={styles.label} style={{ top: '4px', fontSize: '0.7rem', color: 'var(--color-gold)' }}>Compliance Type</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className={styles.input}
|
||||||
|
placeholder=" "
|
||||||
|
value={complianceId}
|
||||||
|
onChange={e => setComplianceId(e.target.value)}
|
||||||
|
disabled={!complianceType}
|
||||||
|
style={{ opacity: !complianceType ? 0.5 : 1, cursor: !complianceType ? 'not-allowed' : 'text' }}
|
||||||
|
/>
|
||||||
|
<label className={styles.label}>
|
||||||
|
{complianceType ? 'Registration Number' : 'ID Number'}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.actions}>
|
||||||
|
<button type="button" className={styles.backBtn} onClick={() => setView('selection')}>Back</button>
|
||||||
|
<button type="submit" className={styles.submitBtn}>Register Entity</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* VIEW 3: JOIN COMPANY */}
|
||||||
|
{view === 'join' && (
|
||||||
|
<div className={styles.formContainer}>
|
||||||
|
<form onSubmit={handleJoinSubmit}>
|
||||||
|
<div className={styles.formGroup} style={{ marginBottom: '2rem' }}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className={styles.input}
|
||||||
|
placeholder=" "
|
||||||
|
value={inviteCode}
|
||||||
|
onChange={e => setInviteCode(e.target.value)}
|
||||||
|
style={{ letterSpacing: '2px', textAlign: 'center', fontSize: '1.2rem' }}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<label className={styles.label} style={{ left: '50%', transform: 'translateX(-50%)' }}>Enter Invitation Code</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.actions} style={{ justifyContent: 'center' }}>
|
||||||
|
<button type="button" className={styles.backBtn} onClick={() => setView('selection')}>Back</button>
|
||||||
|
<button type="submit" className={styles.submitBtn}>Verify & Join</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
240
src/app/page.module.css
Normal file
@@ -0,0 +1,240 @@
|
|||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
min-height: 100vh;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: var(--bg-deep);
|
||||||
|
/* Custom Background Image */
|
||||||
|
background-image:
|
||||||
|
linear-gradient(135deg, rgba(5, 50, 25, 0.65), rgba(2, 20, 10, 0.9)),
|
||||||
|
url('/bg_pennyzo.png');
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes bg-drift {
|
||||||
|
0% {
|
||||||
|
background-position: 0% 0%;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
background-position: 100% 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.loginCard {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 480px;
|
||||||
|
padding: 4rem 3rem;
|
||||||
|
/* Inherits .liquid-glass properties from global utility if applied,
|
||||||
|
but we define specific overrides here */
|
||||||
|
background: rgba(10, 10, 12, 0.6);
|
||||||
|
/* Darker glass for contrast */
|
||||||
|
backdrop-filter: blur(24px);
|
||||||
|
-webkit-backdrop-filter: blur(24px);
|
||||||
|
/* Shining Gold Border */
|
||||||
|
border: 1px solid rgba(255, 215, 0, 0.5);
|
||||||
|
border-top: 1px solid rgba(255, 215, 0, 0.8);
|
||||||
|
border-bottom: 1px solid rgba(255, 215, 0, 0.3);
|
||||||
|
border-radius: 32px;
|
||||||
|
/* Gold Glow */
|
||||||
|
box-shadow:
|
||||||
|
0 24px 64px rgba(0, 0, 0, 0.6),
|
||||||
|
0 0 30px rgba(255, 215, 0, 0.15),
|
||||||
|
inset 0 0 20px rgba(255, 215, 0, 0.05);
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Decorative Top Shine */
|
||||||
|
.loginCard::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 1px;
|
||||||
|
background: linear-gradient(90deg, transparent, var(--color-gold), transparent);
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 3.5rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logoContainer {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
filter: drop-shadow(0 0 15px rgba(255, 215, 0, 0.4));
|
||||||
|
/* Gold glow for logo */
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logoContainer:hover {
|
||||||
|
transform: scale(1.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
.logo {
|
||||||
|
... removed text styles ...
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
.logoText {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 4px;
|
||||||
|
color: #fff;
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
font-family: var(--font-geist-sans);
|
||||||
|
background: linear-gradient(180deg, #fff, #bbb);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
text-shadow: 0 5px 15px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tagline {
|
||||||
|
color: var(--color-gold);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 3px;
|
||||||
|
font-weight: 500;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tagline {
|
||||||
|
color: var(--color-gold);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 3px;
|
||||||
|
font-weight: 500;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formGroup {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 1.2rem 1rem 0.5rem 1rem;
|
||||||
|
/* Space for floating label */
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
color: #fff;
|
||||||
|
font-family: var(--font-geist-mono);
|
||||||
|
font-size: 1rem;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
outline: none;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input:focus {
|
||||||
|
border-bottom-color: var(--color-gold);
|
||||||
|
background: linear-gradient(to top, rgba(255, 215, 0, 0.05), transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0.8rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
pointer-events: none;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
font-family: var(--font-geist-sans);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input:focus~.label,
|
||||||
|
.input:not(:placeholder-shown)~.label {
|
||||||
|
top: -0.5rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--color-gold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
width: 100%;
|
||||||
|
padding: 1.2rem;
|
||||||
|
border: none;
|
||||||
|
background: var(--color-gold);
|
||||||
|
color: #000;
|
||||||
|
font-family: var(--font-geist-sans);
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 1rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
margin-top: 2rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
/* Soft button */
|
||||||
|
box-shadow: 0 10px 30px rgba(255, 215, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 15px 40px rgba(255, 215, 0, 0.3);
|
||||||
|
background: #ffe033;
|
||||||
|
}
|
||||||
|
|
||||||
|
.forgotPassword {
|
||||||
|
display: block;
|
||||||
|
text-align: right;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
transition: 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.forgotPassword:hover {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.registerContainer {
|
||||||
|
margin-top: 3rem;
|
||||||
|
text-align: center;
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
padding-top: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.registerText {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.registerLink {
|
||||||
|
color: var(--color-green);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.registerLink:hover {
|
||||||
|
color: #4affc5;
|
||||||
|
text-shadow: 0 0 15px rgba(0, 255, 157, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.statusLine {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 0.65rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin-top: 2.5rem;
|
||||||
|
font-family: var(--font-geist-mono);
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
66
src/app/page.tsx
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import Image from 'next/image';
|
||||||
|
import styles from "./page.module.css";
|
||||||
|
|
||||||
|
export default function Login() {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const handleLogin = () => {
|
||||||
|
// Mock login success - redirecting to Dashboard directly for demo
|
||||||
|
router.push('/dashboard');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
|
||||||
|
{/* Clean Glass Card */}
|
||||||
|
<div className={styles.loginCard}>
|
||||||
|
|
||||||
|
<div className={styles.header}>
|
||||||
|
<div className={styles.logoContainer}>
|
||||||
|
<Image
|
||||||
|
src="/logo_symbol.png"
|
||||||
|
alt="Pennyzo"
|
||||||
|
width={120}
|
||||||
|
height={120}
|
||||||
|
style={{ width: 'auto', height: '100px', objectFit: 'contain' }}
|
||||||
|
priority
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<h1 className={styles.logoText}>PENNYZO</h1>
|
||||||
|
<p className={styles.tagline}>Business Intelligence Platform</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form>
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<input type="email" id="email" className={styles.input} placeholder=" " required />
|
||||||
|
<label htmlFor="email" className={styles.label}>Email Address</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<input type="password" id="password" className={styles.input} placeholder=" " required />
|
||||||
|
<label htmlFor="password" className={styles.label}>Password</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a href="/forgot-password" className={styles.forgotPassword}>Forgot Password?</a>
|
||||||
|
|
||||||
|
<button type="button" className={styles.button} onClick={handleLogin}>
|
||||||
|
Login to Dashboard
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className={styles.registerContainer}>
|
||||||
|
<span className={styles.registerText}>Data-driven decisions start here.</span>
|
||||||
|
<a href="/register" className={styles.registerLink}>Join Pennyzo</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div className={styles.statusLine}>
|
||||||
|
<span>SECURE ENCLAVE</span>
|
||||||
|
<span>ENTERPRISE GRADE</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
408
src/app/register/page.module.css
Normal file
@@ -0,0 +1,408 @@
|
|||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
min-height: 100vh;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: var(--bg-deep);
|
||||||
|
/* Custom Background Image */
|
||||||
|
background-image:
|
||||||
|
linear-gradient(135deg, rgba(5, 50, 25, 0.65), rgba(2, 20, 10, 0.9)),
|
||||||
|
url('/bg_pennyzo.png');
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes bg-drift {
|
||||||
|
0% {
|
||||||
|
background-position: 0% 0%;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
background-position: 100% 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.registerCard {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 960px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
background: rgba(10, 10, 12, 0.6);
|
||||||
|
backdrop-filter: blur(24px);
|
||||||
|
-webkit-backdrop-filter: blur(24px);
|
||||||
|
/* Shining Gold Border */
|
||||||
|
border: 1px solid rgba(255, 215, 0, 0.5);
|
||||||
|
border-top: 1px solid rgba(255, 215, 0, 0.8);
|
||||||
|
border-bottom: 1px solid rgba(255, 215, 0, 0.3);
|
||||||
|
border-radius: 32px;
|
||||||
|
/* Gold Glow */
|
||||||
|
box-shadow:
|
||||||
|
0 24px 64px rgba(0, 0, 0, 0.6),
|
||||||
|
0 0 30px rgba(255, 215, 0, 0.15),
|
||||||
|
inset 0 0 20px rgba(255, 215, 0, 0.05);
|
||||||
|
position: relative;
|
||||||
|
overflow: visible;
|
||||||
|
/* For dropdowns */
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leftColumn {
|
||||||
|
flex: 1;
|
||||||
|
padding: 4rem 3rem;
|
||||||
|
background: rgba(255, 255, 255, 0.02);
|
||||||
|
/* Slight tint diff */
|
||||||
|
border-right: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rightColumn {
|
||||||
|
flex: 1;
|
||||||
|
padding: 4rem 3rem;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
margin-bottom: 2.5rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
/* Align left for split panel */
|
||||||
|
}
|
||||||
|
|
||||||
|
.logoContainer {
|
||||||
|
margin-bottom: 0.2rem;
|
||||||
|
filter: drop-shadow(0 0 12px rgba(255, 215, 0, 0.3));
|
||||||
|
}
|
||||||
|
|
||||||
|
/*.logo { removed }*/
|
||||||
|
|
||||||
|
.tagline {
|
||||||
|
color: var(--color-cyan);
|
||||||
|
font-size: 0.7rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionTitle {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
font-family: var(--font-geist-sans);
|
||||||
|
}
|
||||||
|
|
||||||
|
.formGroup {
|
||||||
|
margin-bottom: 2.5rem;
|
||||||
|
/* More airy spacing */
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.8rem 0;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.15);
|
||||||
|
color: #fff;
|
||||||
|
font-family: var(--font-geist-mono);
|
||||||
|
font-size: 1rem;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
outline: none;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input:focus {
|
||||||
|
border-bottom-color: var(--color-cyan);
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0.6rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
pointer-events: none;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
font-family: var(--font-geist-sans);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input:focus~.label,
|
||||||
|
.input:not(:placeholder-shown)~.label {
|
||||||
|
top: -1rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--color-cyan);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
width: 100%;
|
||||||
|
padding: 1.2rem;
|
||||||
|
border: none;
|
||||||
|
background: var(--gradient-gold);
|
||||||
|
color: #000;
|
||||||
|
font-family: var(--font-geist-sans);
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
margin-top: 2rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 8px 24px rgba(255, 215, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:hover:not(:disabled) {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 12px 32px rgba(255, 215, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:disabled {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
color: rgba(255, 255, 255, 0.2);
|
||||||
|
box-shadow: none;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loginLink {
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
transition: 0.3s;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loginLink:hover {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dropdown */
|
||||||
|
.mobileInputContainer {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
align-items: flex-end;
|
||||||
|
/* Align baselines */
|
||||||
|
}
|
||||||
|
|
||||||
|
.countrySelect {
|
||||||
|
position: relative;
|
||||||
|
min-width: 130px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selectedCode {
|
||||||
|
padding: 0.8rem 0;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.15);
|
||||||
|
color: #fff;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: var(--font-geist-mono);
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdownMenu {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 0;
|
||||||
|
width: 260px;
|
||||||
|
max-height: 300px;
|
||||||
|
background: rgba(10, 10, 15, 0.95);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 12px;
|
||||||
|
z-index: 100;
|
||||||
|
overflow-y: auto;
|
||||||
|
margin-top: 10px;
|
||||||
|
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom Scrollbar */
|
||||||
|
.dropdownMenu::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdownMenu::-webkit-scrollbar-track {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
/* Subtle dark track */
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdownMenu::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--color-cyan);
|
||||||
|
/* Cyan accent thumb */
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdownMenu::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #00ffea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchInput {
|
||||||
|
width: 100%;
|
||||||
|
padding: 1rem;
|
||||||
|
background: #0a0a0f;
|
||||||
|
/* Opaque background matching dropdown theme */
|
||||||
|
border: none;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
color: #fff;
|
||||||
|
outline: none;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 10;
|
||||||
|
/* Ensure it stays on top */
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdownItem {
|
||||||
|
padding: 1rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.02);
|
||||||
|
transition: 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdownItem:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Validation States */
|
||||||
|
.errorText {
|
||||||
|
color: #ff3333;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputError {
|
||||||
|
border-bottom-color: #ff4d4d !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* OTP Styles */
|
||||||
|
.frozenInput {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.otpGroup {
|
||||||
|
margin-top: 1rem;
|
||||||
|
padding: 1.5rem;
|
||||||
|
background: rgba(255, 255, 255, 0.02);
|
||||||
|
border-radius: 16px;
|
||||||
|
border: 1px solid rgba(0, 255, 234, 0.2);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sendOtpBtn {
|
||||||
|
padding: 0.6rem 1rem;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--color-cyan);
|
||||||
|
border: 1px solid rgba(0, 255, 234, 0.3);
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sendOtpBtn:hover:not(:disabled) {
|
||||||
|
background: rgba(0, 255, 234, 0.1);
|
||||||
|
border-color: var(--color-cyan);
|
||||||
|
}
|
||||||
|
|
||||||
|
.verifyBtn {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.8rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
background: var(--color-green);
|
||||||
|
color: #000;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.disabledSection {
|
||||||
|
opacity: 0.4;
|
||||||
|
pointer-events: none;
|
||||||
|
filter: blur(2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Notifications - Luxury Glass */
|
||||||
|
.notificationContainer {
|
||||||
|
position: fixed;
|
||||||
|
top: 2rem;
|
||||||
|
right: 2rem;
|
||||||
|
z-index: 9999;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification {
|
||||||
|
background: rgba(5, 5, 5, 0.85);
|
||||||
|
/* Deep dark glass */
|
||||||
|
backdrop-filter: blur(16px);
|
||||||
|
border-left: 4px solid var(--color-cyan);
|
||||||
|
border-radius: 8px;
|
||||||
|
/* Clean luxury radius */
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
min-width: 320px;
|
||||||
|
color: #fff;
|
||||||
|
font-family: var(--font-geist-sans);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.4);
|
||||||
|
animation: slideIn 0.4s cubic-bezier(0.2, 0.8, 0.2, 1);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notifySuccess {
|
||||||
|
border-left-color: var(--color-green);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notifyError {
|
||||||
|
border-left-color: #ff4d4d;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideIn {
|
||||||
|
from {
|
||||||
|
transform: translateX(50px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
transform: translateX(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.registerCard {
|
||||||
|
flex-direction: column;
|
||||||
|
max-width: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leftColumn {
|
||||||
|
border-right: none;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
}
|
||||||
330
src/app/register/page.tsx
Normal file
@@ -0,0 +1,330 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useMemo, useRef, useEffect } from 'react';
|
||||||
|
import Image from 'next/image';
|
||||||
|
import styles from "./page.module.css";
|
||||||
|
import { countryCodes } from "@/data/countryCodes";
|
||||||
|
|
||||||
|
type NotificationType = 'success' | 'error' | 'info';
|
||||||
|
|
||||||
|
interface Notification {
|
||||||
|
id: number;
|
||||||
|
message: string;
|
||||||
|
type: NotificationType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Register() {
|
||||||
|
const [selectedCountry, setSelectedCountry] = useState(countryCodes.find(c => c.iso === 'IN') || countryCodes[0]);
|
||||||
|
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||||
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
|
||||||
|
// Fields
|
||||||
|
const [fullName, setFullName] = useState("");
|
||||||
|
const [email, setEmail] = useState("");
|
||||||
|
const [mobileNumber, setMobileNumber] = useState("");
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
|
const [confirmPassword, setConfirmPassword] = useState("");
|
||||||
|
|
||||||
|
// States
|
||||||
|
const [emailError, setEmailError] = useState("");
|
||||||
|
const [isOtpSent, setIsOtpSent] = useState(false);
|
||||||
|
const [isOtpVerified, setIsOtpVerified] = useState(false);
|
||||||
|
const [otpInput, setOtpInput] = useState("");
|
||||||
|
|
||||||
|
// Notifications
|
||||||
|
const [notifications, setNotifications] = useState<Notification[]>([]);
|
||||||
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const showNotification = (message: string, type: NotificationType = 'info') => {
|
||||||
|
const id = Date.now();
|
||||||
|
setNotifications(prev => [...prev, { id, message, type }]);
|
||||||
|
setTimeout(() => {
|
||||||
|
setNotifications(prev => prev.filter(n => n.id !== id));
|
||||||
|
}, 4000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const filteredCountries = useMemo(() => {
|
||||||
|
return countryCodes.filter(country =>
|
||||||
|
country.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
|
country.code.includes(searchQuery)
|
||||||
|
);
|
||||||
|
}, [searchQuery]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function handleClickOutside(event: MouseEvent) {
|
||||||
|
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
||||||
|
setDropdownOpen(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.addEventListener("mousedown", handleClickOutside);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("mousedown", handleClickOutside);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleMobileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const value = e.target.value.replace(/\D/g, '');
|
||||||
|
if (value.length <= 10) {
|
||||||
|
setMobileNumber(value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateEmail = (value: string) => {
|
||||||
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||||
|
if (!emailRegex.test(value) && value.length > 0) {
|
||||||
|
setEmailError("Invalid Email Format");
|
||||||
|
} else {
|
||||||
|
setEmailError("");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setEmail(e.target.value);
|
||||||
|
if (emailError) validateEmail(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
// OTP Logic
|
||||||
|
const handleSendOtp = () => {
|
||||||
|
if (fullName && email && mobileNumber.length === 10 && !emailError) {
|
||||||
|
setIsOtpSent(true);
|
||||||
|
showNotification(`OTP Sent to ${selectedCountry.code} ${mobileNumber}`, 'info');
|
||||||
|
} else {
|
||||||
|
showNotification("Please complete all profile details first.", 'error');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleVerifyOtp = () => {
|
||||||
|
if (otpInput === "123456") {
|
||||||
|
setIsOtpVerified(true);
|
||||||
|
setIsOtpSent(false);
|
||||||
|
showNotification("Identity Verified Successfully", 'success');
|
||||||
|
} else {
|
||||||
|
showNotification("Invalid OTP Code", 'error');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRegister = () => {
|
||||||
|
if (!password || !confirmPassword) {
|
||||||
|
showNotification("Password fields are required", 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password.length < 8) {
|
||||||
|
showNotification("Password must be at least 8 characters", 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password !== confirmPassword) {
|
||||||
|
showNotification("Passwords do not match", 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Success
|
||||||
|
showNotification("Account creation started...", 'success');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
|
||||||
|
{/* Notification Container */}
|
||||||
|
<div className={styles.notificationContainer}>
|
||||||
|
{notifications.map(n => (
|
||||||
|
<div key={n.id} className={`${styles.notification} ${n.type === 'success' ? styles.notifySuccess : n.type === 'error' ? styles.notifyError : ''}`}>
|
||||||
|
<div style={{ fontSize: '0.75rem', opacity: 0.7, textTransform: 'uppercase', marginBottom: '4px' }}>
|
||||||
|
{n.type === 'error' ? 'System Alert' : n.type === 'success' ? 'Success' : 'Indication'}
|
||||||
|
</div>
|
||||||
|
{n.message}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Clean Glass Card - Removed Robotic Brackets */}
|
||||||
|
<div className={styles.registerCard}>
|
||||||
|
|
||||||
|
{/* Column 1: Personal Details */}
|
||||||
|
<div className={styles.leftColumn}>
|
||||||
|
<div className={styles.header}>
|
||||||
|
<div className={styles.logoContainer}>
|
||||||
|
<Image
|
||||||
|
src="/logo_symbol.png"
|
||||||
|
alt="Pennyzo"
|
||||||
|
width={100}
|
||||||
|
height={100}
|
||||||
|
style={{ width: 'auto', height: '80px', objectFit: 'contain' }}
|
||||||
|
priority
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<h1 className={styles.logo} style={{ fontSize: '1.8rem', marginTop: '0.5rem' }}>PENNYZO</h1>
|
||||||
|
<p className={styles.tagline}>Future of Business Intelligence</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 className={styles.sectionTitle}>Profile Details</h2>
|
||||||
|
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="fullname"
|
||||||
|
className={`${styles.input} ${(isOtpSent || isOtpVerified) ? styles.frozenInput : ''}`}
|
||||||
|
placeholder=" "
|
||||||
|
value={fullName}
|
||||||
|
onChange={(e) => setFullName(e.target.value)}
|
||||||
|
readOnly={isOtpSent || isOtpVerified}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<label htmlFor="fullname" className={styles.label}>Full Name</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
id="email"
|
||||||
|
className={`${styles.input} ${emailError ? styles.inputError : ''} ${(isOtpSent || isOtpVerified) ? styles.frozenInput : ''}`}
|
||||||
|
placeholder=" "
|
||||||
|
value={email}
|
||||||
|
onChange={handleEmailChange}
|
||||||
|
onBlur={(e) => validateEmail(e.target.value)}
|
||||||
|
readOnly={isOtpSent || isOtpVerified}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<label htmlFor="email" className={styles.label}>Email Address</label>
|
||||||
|
{emailError && <span style={{ color: '#ff4d4d', fontSize: '0.7rem', marginTop: '5px', display: 'block' }}>{emailError}</span>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Column 2: Account Security */}
|
||||||
|
<div className={styles.rightColumn}>
|
||||||
|
<h2 className={styles.sectionTitle}>Security & Access</h2>
|
||||||
|
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
{/* Dropdown & Input */}
|
||||||
|
<div className={styles.mobileInputContainer}>
|
||||||
|
<div className={styles.countrySelect} ref={dropdownRef}>
|
||||||
|
<div
|
||||||
|
className={`${styles.selectedCode} ${(isOtpSent || isOtpVerified) ? styles.frozenInput : ''}`}
|
||||||
|
onClick={() => !(isOtpSent || isOtpVerified) && setDropdownOpen(!dropdownOpen)}
|
||||||
|
>
|
||||||
|
<span>{selectedCountry.iso} {selectedCountry.code}</span>
|
||||||
|
{!isOtpVerified && <span style={{ fontSize: '0.7rem', opacity: 0.5 }}>▼</span>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{dropdownOpen && (
|
||||||
|
<div className={styles.dropdownMenu}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className={styles.searchInput}
|
||||||
|
placeholder="Search..."
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
{filteredCountries.map((country, index) => (
|
||||||
|
<div
|
||||||
|
key={`${country.iso}-${index}`}
|
||||||
|
className={styles.dropdownItem}
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedCountry(country);
|
||||||
|
setDropdownOpen(false);
|
||||||
|
setSearchQuery("");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>{country.name}</span>
|
||||||
|
<span style={{ color: 'var(--color-cyan)' }}>{country.code}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ flex: 1, position: 'relative' }}>
|
||||||
|
<input
|
||||||
|
type="tel"
|
||||||
|
className={`${styles.input} ${(isOtpSent || isOtpVerified) ? styles.frozenInput : ''}`}
|
||||||
|
placeholder=" "
|
||||||
|
value={mobileNumber}
|
||||||
|
onChange={handleMobileChange}
|
||||||
|
readOnly={isOtpSent || isOtpVerified}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<label className={styles.label}>Mobile Number</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Send OTP Button (Utility) */}
|
||||||
|
{!isOtpVerified && !isOtpSent && (
|
||||||
|
<div style={{ textAlign: 'right', marginTop: '0.5rem' }}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={styles.sendOtpBtn}
|
||||||
|
onClick={handleSendOtp}
|
||||||
|
disabled={!fullName || !email || mobileNumber.length < 10 || !!emailError}
|
||||||
|
>
|
||||||
|
VERIFY MOBILE
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* OTP Verification Section - Glass Panel Inside */}
|
||||||
|
{isOtpSent && !isOtpVerified && (
|
||||||
|
<div className={styles.otpGroup}>
|
||||||
|
<p style={{ fontSize: '0.8rem', color: '#fff', marginBottom: '1rem' }}>
|
||||||
|
Enter verification code sent to {mobileNumber}
|
||||||
|
</p>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className={styles.input}
|
||||||
|
maxLength={6}
|
||||||
|
placeholder="000000"
|
||||||
|
style={{ textAlign: 'center', letterSpacing: '8px', fontSize: '1.5rem' }}
|
||||||
|
value={otpInput}
|
||||||
|
onChange={(e) => setOtpInput(e.target.value.replace(/\D/g, ''))}
|
||||||
|
/>
|
||||||
|
<button type="button" className={styles.verifyBtn} onClick={handleVerifyOtp}>
|
||||||
|
Verify Identity
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Password Section */}
|
||||||
|
<div className={!isOtpVerified ? styles.disabledSection : ''}>
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
id="password"
|
||||||
|
className={styles.input}
|
||||||
|
placeholder=" "
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
required
|
||||||
|
disabled={!isOtpVerified}
|
||||||
|
/>
|
||||||
|
<label htmlFor="password" className={styles.label}>Password</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
id="confirmPassword"
|
||||||
|
className={styles.input}
|
||||||
|
placeholder=" "
|
||||||
|
value={confirmPassword}
|
||||||
|
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||||
|
required
|
||||||
|
disabled={!isOtpVerified}
|
||||||
|
/>
|
||||||
|
<label htmlFor="confirmPassword" className={styles.label}>Confirm Password</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="button" className={styles.button} disabled={!isOtpVerified} onClick={handleRegister}>
|
||||||
|
Create Account
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a href="/" className={styles.loginLink}>
|
||||||
|
Have an account? <span style={{ color: '#fff' }}>Login</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
46
src/components/dms/ConfirmationModal.tsx
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
'use client';
|
||||||
|
import styles from './dmsComponents.module.css';
|
||||||
|
|
||||||
|
interface ConfirmationModalProps {
|
||||||
|
title: string;
|
||||||
|
message: string;
|
||||||
|
onConfirm: () => void;
|
||||||
|
onCancel: () => void;
|
||||||
|
confirmText?: string;
|
||||||
|
cancelText?: string;
|
||||||
|
isDestructive?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ConfirmationModal({
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
onConfirm,
|
||||||
|
onCancel,
|
||||||
|
confirmText = 'Confirm',
|
||||||
|
cancelText = 'Cancel',
|
||||||
|
isDestructive = false
|
||||||
|
}: ConfirmationModalProps) {
|
||||||
|
return (
|
||||||
|
<div className={styles.modalOverlay} onClick={onCancel}>
|
||||||
|
<div className={styles.confirmModal} onClick={e => e.stopPropagation()}>
|
||||||
|
<div className={styles.modalHeader}>
|
||||||
|
<h3>{title}</h3>
|
||||||
|
</div>
|
||||||
|
<div className={styles.modalBody}>
|
||||||
|
<p style={{ color: '#ccc', lineHeight: '1.5' }}>{message}</p>
|
||||||
|
</div>
|
||||||
|
<div className={styles.modalFooter}>
|
||||||
|
<button onClick={onCancel} className={styles.cancelBtn}>
|
||||||
|
{cancelText}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={onConfirm}
|
||||||
|
className={isDestructive ? styles.deleteBtn : styles.shareBtn}
|
||||||
|
>
|
||||||
|
{confirmText}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
67
src/components/dms/FileRow.tsx
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import Link from 'next/link';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import styles from './dmsComponents.module.css';
|
||||||
|
|
||||||
|
interface FileRowProps {
|
||||||
|
file: {
|
||||||
|
id: number | string;
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
size: string;
|
||||||
|
date: string;
|
||||||
|
tags?: string[];
|
||||||
|
};
|
||||||
|
onDelete?: (id: number | string) => void;
|
||||||
|
onShare?: (file: FileRowProps['file']) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function FileRow({ file, onDelete, onShare }: FileRowProps) {
|
||||||
|
const router = useRouter();
|
||||||
|
return (
|
||||||
|
<tr className={styles.fileRow}>
|
||||||
|
<td>
|
||||||
|
<div className={styles.fileName}>
|
||||||
|
<span className={styles.fileIcon}>
|
||||||
|
{file.type === 'PDF' ? '📄' : file.type === 'IMG' ? '🖼️' : '📁'}
|
||||||
|
</span>
|
||||||
|
<Link href={`/dashboard/documents/${file.id}`} className={styles.fileLink}>
|
||||||
|
{file.name}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{file.tags && file.tags.length > 0 ? (
|
||||||
|
<div className={styles.tags}>
|
||||||
|
{file.tags.map((tag, i) => (
|
||||||
|
<span key={i} className={styles.tag}>{tag}</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<span className={styles.noTags}>-</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td>{file.size}</td>
|
||||||
|
<td>{file.date}</td>
|
||||||
|
<td>
|
||||||
|
<div className={styles.actions}>
|
||||||
|
<button
|
||||||
|
className={styles.actionBtn}
|
||||||
|
title="Preview"
|
||||||
|
onClick={() => router.push(`/dashboard/documents/${file.id}`)}
|
||||||
|
>
|
||||||
|
👁️
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={styles.actionBtn}
|
||||||
|
title="Share with Team"
|
||||||
|
onClick={() => onShare?.(file)}
|
||||||
|
>
|
||||||
|
👥
|
||||||
|
</button>
|
||||||
|
<button className={styles.actionBtn} title="Download">⬇️</button>
|
||||||
|
<button className={styles.actionBtn} title="Delete" onClick={() => onDelete?.(file.id)}>🗑️</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
105
src/components/dms/ShareModal.tsx
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
'use client';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import styles from './dmsComponents.module.css';
|
||||||
|
|
||||||
|
interface User {
|
||||||
|
id: number | string;
|
||||||
|
name: string;
|
||||||
|
role: string;
|
||||||
|
avatar: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ShareModalProps {
|
||||||
|
fileName: string;
|
||||||
|
users: User[];
|
||||||
|
onClose: () => void;
|
||||||
|
onShare: (selectedUserIds: (number | string)[], permissions: string[]) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ShareModal({ fileName, users, onClose, onShare }: ShareModalProps) {
|
||||||
|
const [selectedUsers, setSelectedUsers] = useState<(number | string)[]>([]);
|
||||||
|
const [permissions, setPermissions] = useState<string[]>(['View']); // 'View' is default
|
||||||
|
|
||||||
|
const toggleUser = (id: number | string) => {
|
||||||
|
if (selectedUsers.includes(id)) {
|
||||||
|
setSelectedUsers(selectedUsers.filter(uId => uId !== id));
|
||||||
|
} else {
|
||||||
|
setSelectedUsers([...selectedUsers, id]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const togglePermission = (perm: string) => {
|
||||||
|
if (permissions.includes(perm)) {
|
||||||
|
setPermissions(permissions.filter(p => p !== perm));
|
||||||
|
} else {
|
||||||
|
setPermissions([...permissions, perm]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleShare = () => {
|
||||||
|
onShare(selectedUsers, permissions);
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.modalOverlay} onClick={onClose}>
|
||||||
|
<div className={styles.shareModal} onClick={e => e.stopPropagation()}>
|
||||||
|
<div className={styles.modalHeader}>
|
||||||
|
<h3>Share "{fileName}"</h3>
|
||||||
|
<button onClick={onClose} className={styles.closeBtn}>×</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.modalBody}>
|
||||||
|
<div className={styles.sectionTitle}>Select Team Members</div>
|
||||||
|
<div className={styles.userList}>
|
||||||
|
{users.map(user => (
|
||||||
|
<div
|
||||||
|
key={user.id}
|
||||||
|
className={`${styles.userRow} ${selectedUsers.includes(user.id) ? styles.selected : ''}`}
|
||||||
|
onClick={() => toggleUser(user.id)}
|
||||||
|
>
|
||||||
|
<div className={styles.userAvatar}>{user.avatar}</div>
|
||||||
|
<div className={styles.userInfo}>
|
||||||
|
<div className={styles.userName}>{user.name}</div>
|
||||||
|
<div className={styles.userRole}>{user.role}</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.checkbox}>
|
||||||
|
{selectedUsers.includes(user.id) && '✓'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.sectionTitle}>Permissions</div>
|
||||||
|
<div className={styles.permissionOptions}>
|
||||||
|
<div
|
||||||
|
className={`${styles.permissionOption} ${permissions.includes('View') ? styles.active : ''}`}
|
||||||
|
onClick={() => togglePermission('View')}
|
||||||
|
>
|
||||||
|
<span className={styles.checkbox}>{permissions.includes('View') ? '✓' : ''}</span>
|
||||||
|
<span>Can View</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`${styles.permissionOption} ${permissions.includes('Download') ? styles.active : ''}`}
|
||||||
|
onClick={() => togglePermission('Download')}
|
||||||
|
>
|
||||||
|
<span className={styles.checkbox}>{permissions.includes('Download') ? '✓' : ''}</span>
|
||||||
|
<span>Can Download</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.modalFooter}>
|
||||||
|
<button onClick={onClose} className={styles.cancelBtn}>Cancel</button>
|
||||||
|
<button
|
||||||
|
onClick={handleShare}
|
||||||
|
className={styles.shareBtn}
|
||||||
|
disabled={selectedUsers.length === 0}
|
||||||
|
>
|
||||||
|
Share Access
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
129
src/components/dms/SmartSearchModal.tsx
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
'use client';
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import styles from './dmsComponents.module.css';
|
||||||
|
|
||||||
|
interface SmartSearchModalProps {
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SmartSearchModal({ onClose }: SmartSearchModalProps) {
|
||||||
|
const [query, setQuery] = useState('');
|
||||||
|
const [results, setResults] = useState<any[]>([]);
|
||||||
|
const [isSearching, setIsSearching] = useState(false);
|
||||||
|
|
||||||
|
// Mock global data
|
||||||
|
const allFiles = [
|
||||||
|
{ id: 1, name: 'Project_Alpha_proposal.pdf', type: 'PDF', path: '/Projects/Alpha' },
|
||||||
|
{ id: 2, name: 'Invoice_Nov_2024.pdf', type: 'PDF', path: '/Finance/Invoices' },
|
||||||
|
{ id: 3, name: 'Site_Images_Nov.zip', type: 'ZIP', path: '/Site Data' },
|
||||||
|
{ id: 4, name: 'Meeting_Notes_Q3.docx', type: 'DOCX', path: '/Internal/Meetings' },
|
||||||
|
{ id: 5, name: 'Company_Policy_v2.pdf', type: 'PDF', path: '/HR' },
|
||||||
|
{ id: 6, name: 'Logo_Pack.zip', type: 'ZIP', path: '/Branding' },
|
||||||
|
{ id: 7, name: 'Marketing_Plan_2025.pptx', type: 'PPTX', path: '/Marketing' },
|
||||||
|
{ id: 8, name: 'Tax_Returns_FY23.pdf', type: 'PDF', path: '/Finance/Tax' },
|
||||||
|
];
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleEsc = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'Escape') onClose();
|
||||||
|
};
|
||||||
|
window.addEventListener('keydown', handleEsc);
|
||||||
|
return () => window.removeEventListener('keydown', handleEsc);
|
||||||
|
}, [onClose]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (query.trim() === '') {
|
||||||
|
setResults([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsSearching(true);
|
||||||
|
// Simulate network delay
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
const lowerQuery = query.toLowerCase();
|
||||||
|
const filtered = allFiles.filter(f =>
|
||||||
|
f.name.toLowerCase().includes(lowerQuery) ||
|
||||||
|
f.type.toLowerCase().includes(lowerQuery) ||
|
||||||
|
f.path.toLowerCase().includes(lowerQuery)
|
||||||
|
);
|
||||||
|
setResults(filtered);
|
||||||
|
setIsSearching(false);
|
||||||
|
}, 300);
|
||||||
|
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [query]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.modalOverlay} onClick={onClose}>
|
||||||
|
<div className={styles.searchModal} onClick={e => e.stopPropagation()}>
|
||||||
|
<div className={styles.searchHeader}>
|
||||||
|
<span className={styles.searchIcon}>🔍</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search for files, folders, or tags..."
|
||||||
|
value={query}
|
||||||
|
onChange={(e) => setQuery(e.target.value)}
|
||||||
|
autoFocus
|
||||||
|
className={styles.searchInput}
|
||||||
|
/>
|
||||||
|
<button onClick={onClose} className={styles.closeBtn}>×</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.resultsContainer}>
|
||||||
|
{query === '' && (
|
||||||
|
<div className={styles.emptyState}>
|
||||||
|
<p>Type to start searching your entire document library.</p>
|
||||||
|
<div className={styles.suggestions}>
|
||||||
|
<span>Try: "Invoice"</span>
|
||||||
|
<span>"Project Alpha"</span>
|
||||||
|
<span>"PDF"</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isSearching && (
|
||||||
|
<div className={styles.loadingState}>
|
||||||
|
Searching...
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isSearching && query !== '' && results.length === 0 && (
|
||||||
|
<div className={styles.emptyState}>
|
||||||
|
<p>No results found for "{query}"</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isSearching && results.length > 0 && (
|
||||||
|
<ul className={styles.resultsList}>
|
||||||
|
{results.map(file => (
|
||||||
|
<li key={file.id}>
|
||||||
|
<Link href={`/dashboard/documents/${file.id}`} onClick={onClose} className={styles.resultItem}>
|
||||||
|
<div className={styles.resultIcon}>
|
||||||
|
{file.type === 'PDF' ? '📄' : file.type === 'IMG' ? '🖼️' : '📁'}
|
||||||
|
</div>
|
||||||
|
<div className={styles.resultInfo}>
|
||||||
|
<div className={styles.resultName}>
|
||||||
|
{file.name.replace(new RegExp(query, 'gi'), (match: string) => match)}
|
||||||
|
{/* Note: React won't render HTML string directly safely without dangerouslySetInnerHTML, simpler to just text for now */}
|
||||||
|
{/* Just rendering name plainly to act as MVP */}
|
||||||
|
{file.name}
|
||||||
|
</div>
|
||||||
|
<div className={styles.resultPath}>{file.path}</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.resultType}>{file.type}</div>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.modalFooter}>
|
||||||
|
<span>High-Performance Search</span>
|
||||||
|
<span>Press <b>ESC</b> to close</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
33
src/components/dms/Toast.tsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
'use client';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import styles from './dmsComponents.module.css';
|
||||||
|
|
||||||
|
interface ToastProps {
|
||||||
|
message: string;
|
||||||
|
type?: 'success' | 'error' | 'info';
|
||||||
|
onClose: () => void;
|
||||||
|
duration?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Toast({ message, type = 'success', onClose, duration = 3000 }: ToastProps) {
|
||||||
|
const [isVisible, setIsVisible] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setIsVisible(false);
|
||||||
|
setTimeout(onClose, 300); // Allow animation to finish
|
||||||
|
}, duration);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [duration, onClose]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`${styles.toast} ${styles[type]} ${isVisible ? styles.show : styles.hide}`}>
|
||||||
|
<span className={styles.toastIcon}>
|
||||||
|
{type === 'success' && '✅'}
|
||||||
|
{type === 'error' && '❌'}
|
||||||
|
{type === 'info' && 'ℹ️'}
|
||||||
|
</span>
|
||||||
|
<span className={styles.toastMessage}>{message}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
549
src/components/dms/dmsComponents.module.css
Normal file
@@ -0,0 +1,549 @@
|
|||||||
|
.fileRow:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fileName {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.8rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fileIcon {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fileLink {
|
||||||
|
color: #fff;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fileLink:hover {
|
||||||
|
color: var(--color-gold, #FFD700);
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tags {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag {
|
||||||
|
background: rgba(255, 215, 0, 0.1);
|
||||||
|
color: var(--color-gold, #FFD700);
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
border: 1px solid rgba(255, 215, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.noTags {
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
opacity: 0.6;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fileRow:hover .actions {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actionBtn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #aaa;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1rem;
|
||||||
|
padding: 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actionBtn:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Smart Search Modal */
|
||||||
|
.modalOverlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
backdrop-filter: blur(5px);
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: center;
|
||||||
|
padding-top: 100px;
|
||||||
|
z-index: 1000;
|
||||||
|
animation: fadeIn 0.2s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchModal {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 650px;
|
||||||
|
background: rgba(10, 20, 30, 0.85);
|
||||||
|
/* Deep glass */
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
-webkit-backdrop-filter: blur(20px);
|
||||||
|
border: 1px solid var(--color-gold);
|
||||||
|
border-radius: 24px;
|
||||||
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.6), 0 0 15px rgba(255, 215, 0, 0.1);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
animation: slideUp 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideUp {
|
||||||
|
from {
|
||||||
|
transform: translateY(20px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
transform: translateY(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchHeader {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchIcon {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
margin-right: 1rem;
|
||||||
|
color: var(--color-gold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchInput {
|
||||||
|
flex: 1;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
color: #fff;
|
||||||
|
outline: none;
|
||||||
|
font-family: var(--font-geist-sans);
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchInput::placeholder {
|
||||||
|
color: rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.closeBtn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: rgba(255, 255, 255, 0.4);
|
||||||
|
font-size: 1.8rem;
|
||||||
|
cursor: pointer;
|
||||||
|
line-height: 1;
|
||||||
|
transition: color 0.2s;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.closeBtn:hover {
|
||||||
|
color: var(--color-gold);
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.resultsContainer {
|
||||||
|
max-height: 450px;
|
||||||
|
overflow-y: auto;
|
||||||
|
min-height: 300px;
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom Scrollbar for Results */
|
||||||
|
.resultsContainer::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resultsContainer::-webkit-scrollbar-track {
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.resultsContainer::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(255, 215, 0, 0.3);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resultsContainer::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: var(--color-gold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.emptyState,
|
||||||
|
.loadingState {
|
||||||
|
padding: 3rem;
|
||||||
|
text-align: center;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestions {
|
||||||
|
margin-top: 1rem;
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestions span {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resultsList {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resultItem {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
text-decoration: none;
|
||||||
|
color: #fff;
|
||||||
|
border-left: 2px solid transparent;
|
||||||
|
transition: background 0.1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resultItem:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border-left-color: var(--color-gold, #FFD700);
|
||||||
|
}
|
||||||
|
|
||||||
|
.resultIcon {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
margin-right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resultInfo {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resultName {
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 1rem;
|
||||||
|
margin-bottom: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resultPath {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resultType {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: #aaa;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modalFooter {
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
background: rgba(255, 255, 255, 0.02);
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Share Modal Specifics */
|
||||||
|
.shareModal {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 500px;
|
||||||
|
background: rgba(15, 25, 35, 0.95);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
border: 1px solid rgba(255, 215, 0, 0.3);
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.6);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
animation: slideUp 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modalHeader {
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modalHeader h3 {
|
||||||
|
margin: 0;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
max-width: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modalBody {
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionTitle {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: rgba(255, 215, 0, 0.8);
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userList {
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.userRow {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.8rem 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.userRow:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userRow:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.userRow.selected {
|
||||||
|
background: rgba(255, 215, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.userAvatar {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: linear-gradient(135deg, #444, #222);
|
||||||
|
color: #fff;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-right: 1rem;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.userRow.selected .userAvatar {
|
||||||
|
background: var(--color-gold);
|
||||||
|
color: #000;
|
||||||
|
border-color: var(--color-gold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.userInfo {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userName {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userRole {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--color-gold);
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userRow.selected .checkbox,
|
||||||
|
.permissionOption.active .checkbox {
|
||||||
|
border-color: var(--color-gold);
|
||||||
|
background: rgba(255, 215, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.permissionOptions {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.permissionOption {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.8rem 1rem;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
flex: 1;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
transition: all 0.2s;
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.permissionOption:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.permissionOption.active {
|
||||||
|
border-color: var(--color-gold);
|
||||||
|
color: var(--color-gold);
|
||||||
|
background: rgba(255, 215, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cancelBtn {
|
||||||
|
padding: 0.8rem 1.5rem;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cancelBtn:hover {
|
||||||
|
border-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shareBtn {
|
||||||
|
padding: 0.8rem 2rem;
|
||||||
|
background: var(--color-gold, #FFD700);
|
||||||
|
color: #000;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shareBtn:hover {
|
||||||
|
box-shadow: 0 0 15px rgba(255, 215, 0, 0.4);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shareBtn:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
box-shadow: none;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Confirmation Modal */
|
||||||
|
.confirmModal {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
background: rgba(15, 25, 35, 0.95);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
border: 1px solid rgba(255, 215, 0, 0.3);
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.6);
|
||||||
|
animation: slideUp 0.3s ease-out;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deleteBtn {
|
||||||
|
padding: 0.8rem 1.5rem;
|
||||||
|
background: rgba(255, 50, 50, 0.1);
|
||||||
|
color: #ff4444;
|
||||||
|
border: 1px solid rgba(255, 50, 50, 0.3);
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deleteBtn:hover {
|
||||||
|
background: rgba(255, 50, 50, 0.2);
|
||||||
|
box-shadow: 0 0 10px rgba(255, 50, 50, 0.2);
|
||||||
|
border-color: #ff4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Toast Notification */
|
||||||
|
.toast {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 30px;
|
||||||
|
right: 30px;
|
||||||
|
background: rgba(10, 20, 30, 0.9);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-left: 4px solid var(--color-gold);
|
||||||
|
color: #fff;
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
z-index: 2000;
|
||||||
|
transition: all 0.3s cubic-bezier(0.68, -0.55, 0.27, 1.55);
|
||||||
|
transform: translateX(100%);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.show {
|
||||||
|
transform: translateX(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.hide {
|
||||||
|
transform: translateX(20px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.success {
|
||||||
|
border-left-color: #00ff9d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.error {
|
||||||
|
border-left-color: #ff4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.info {
|
||||||
|
border-left-color: var(--color-gold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toastIcon {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toastMessage {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
105
src/components/ui/Toast.tsx
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { Toast, ToastType } from '../../context/ToastContext';
|
||||||
|
|
||||||
|
interface ToastContainerProps {
|
||||||
|
toasts: Toast[];
|
||||||
|
removeToast: (id: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ToastContainer = ({ toasts, removeToast }: ToastContainerProps) => {
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
position: 'fixed',
|
||||||
|
top: '20px',
|
||||||
|
right: '20px',
|
||||||
|
zIndex: 9999,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: '10px',
|
||||||
|
pointerEvents: 'none' // Allow clicks to pass through container
|
||||||
|
}}>
|
||||||
|
{toasts.map(toast => (
|
||||||
|
<ToastItem key={toast.id} toast={toast} onClose={() => removeToast(toast.id)} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ToastItem = ({ toast, onClose }: { toast: Toast; onClose: () => void }) => {
|
||||||
|
const getColors = (type: ToastType) => {
|
||||||
|
switch (type) {
|
||||||
|
case 'success': return { border: '#4caf50', bg: 'rgba(76, 175, 80, 0.15)', icon: '✓', shadow: 'rgba(76, 175, 80, 0.4)' };
|
||||||
|
case 'error': return { border: '#f44336', bg: 'rgba(244, 67, 54, 0.15)', icon: '✕', shadow: 'rgba(244, 67, 54, 0.4)' };
|
||||||
|
case 'warning': return { border: '#ff9800', bg: 'rgba(255, 152, 0, 0.15)', icon: '!', shadow: 'rgba(255, 152, 0, 0.4)' };
|
||||||
|
default: return { border: '#2196f3', bg: 'rgba(33, 150, 243, 0.15)', icon: 'i', shadow: 'rgba(33, 150, 243, 0.4)' };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const colors = getColors(toast.type);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
pointerEvents: 'auto',
|
||||||
|
minWidth: '300px',
|
||||||
|
background: 'rgba(5, 26, 21, 0.95)', // Deep luxury green base
|
||||||
|
backdropFilter: 'blur(12px)',
|
||||||
|
border: `1px solid ${colors.border}`,
|
||||||
|
borderLeft: `4px solid ${colors.border}`,
|
||||||
|
borderRadius: '8px',
|
||||||
|
padding: '16px 20px',
|
||||||
|
color: '#fff',
|
||||||
|
boxShadow: `0 4px 20px rgba(0,0,0,0.5), 0 0 15px ${colors.shadow}`,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
animation: 'slideIn 0.3s ease-out forwards',
|
||||||
|
transformOrigin: 'right center',
|
||||||
|
}}>
|
||||||
|
<style jsx>{`
|
||||||
|
@keyframes slideIn {
|
||||||
|
0% { transform: translateX(100%) scale(0.9); opacity: 0; }
|
||||||
|
100% { transform: translateX(0) scale(1); opacity: 1; }
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||||||
|
<div style={{
|
||||||
|
width: '24px',
|
||||||
|
height: '24px',
|
||||||
|
borderRadius: '50%',
|
||||||
|
background: colors.bg,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
color: colors.border,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
fontSize: '14px',
|
||||||
|
border: `1px solid ${colors.border}`
|
||||||
|
}}>
|
||||||
|
{colors.icon}
|
||||||
|
</div>
|
||||||
|
<span style={{ fontSize: '0.95rem', fontWeight: 500, letterSpacing: '0.3px' }}>{toast.message}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
style={{
|
||||||
|
background: 'transparent',
|
||||||
|
border: 'none',
|
||||||
|
color: 'rgba(255,255,255,0.5)',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: '18px',
|
||||||
|
marginLeft: '15px',
|
||||||
|
padding: '0 4px',
|
||||||
|
transition: 'color 0.2s'
|
||||||
|
}}
|
||||||
|
onMouseEnter={e => e.currentTarget.style.color = '#fff'}
|
||||||
|
onMouseLeave={e => e.currentTarget.style.color = 'rgba(255,255,255,0.5)'}
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
83
src/context/LanguageContext.tsx
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
'use client';
|
||||||
|
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
|
||||||
|
|
||||||
|
// Import translation files
|
||||||
|
import en from '@/translations/en.json';
|
||||||
|
import hi from '@/translations/hi.json';
|
||||||
|
import bn from '@/translations/bn.json';
|
||||||
|
import te from '@/translations/te.json';
|
||||||
|
import mr from '@/translations/mr.json';
|
||||||
|
import ta from '@/translations/ta.json';
|
||||||
|
import gu from '@/translations/gu.json';
|
||||||
|
import kn from '@/translations/kn.json';
|
||||||
|
import ml from '@/translations/ml.json';
|
||||||
|
import pa from '@/translations/pa.json';
|
||||||
|
import or from '@/translations/or.json';
|
||||||
|
import as from '@/translations/as.json';
|
||||||
|
|
||||||
|
const translations: Record<string, any> = {
|
||||||
|
en, hi, bn, te, mr, ta, gu, kn, ml, pa, or, as
|
||||||
|
};
|
||||||
|
|
||||||
|
interface LanguageContextType {
|
||||||
|
currentLanguage: string;
|
||||||
|
setLanguage: (lang: string) => void;
|
||||||
|
t: (key: string) => string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LanguageContext = createContext<LanguageContextType | undefined>(undefined);
|
||||||
|
|
||||||
|
export function LanguageProvider({ children }: { children: ReactNode }) {
|
||||||
|
const [currentLanguage, setCurrentLanguage] = useState('en');
|
||||||
|
|
||||||
|
// Load saved language from localStorage
|
||||||
|
useEffect(() => {
|
||||||
|
const savedLang = localStorage.getItem('preferredLanguage');
|
||||||
|
if (savedLang && translations[savedLang]) {
|
||||||
|
setCurrentLanguage(savedLang);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const setLanguage = (lang: string) => {
|
||||||
|
setCurrentLanguage(lang);
|
||||||
|
localStorage.setItem('preferredLanguage', lang);
|
||||||
|
};
|
||||||
|
|
||||||
|
const t = (key: string): string => {
|
||||||
|
const keys = key.split('.');
|
||||||
|
let value: any = translations[currentLanguage];
|
||||||
|
|
||||||
|
for (const k of keys) {
|
||||||
|
if (value && typeof value === 'object') {
|
||||||
|
value = value[k];
|
||||||
|
} else {
|
||||||
|
// Fallback to English if translation not found
|
||||||
|
value = translations.en;
|
||||||
|
for (const fallbackKey of keys) {
|
||||||
|
if (value && typeof value === 'object') {
|
||||||
|
value = value[fallbackKey];
|
||||||
|
} else {
|
||||||
|
return key; // Return key if not found
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return value || key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return value || key;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LanguageContext.Provider value={{ currentLanguage, setLanguage, t }}>
|
||||||
|
{children}
|
||||||
|
</LanguageContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useLanguage() {
|
||||||
|
const context = useContext(LanguageContext);
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error('useLanguage must be used within a LanguageProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
53
src/context/ToastContext.tsx
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { createContext, useContext, useState, useCallback, ReactNode } from 'react';
|
||||||
|
import { ToastContainer } from '../components/ui/Toast';
|
||||||
|
|
||||||
|
export type ToastType = 'success' | 'error' | 'info' | 'warning';
|
||||||
|
|
||||||
|
export interface Toast {
|
||||||
|
id: string;
|
||||||
|
message: string;
|
||||||
|
type: ToastType;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ToastContextType {
|
||||||
|
showToast: (message: string, type?: ToastType) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ToastContext = createContext<ToastContextType | undefined>(undefined);
|
||||||
|
|
||||||
|
export const useToast = () => {
|
||||||
|
const context = useContext(ToastContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useToast must be used within a ToastProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ToastProvider = ({ children }: { children: ReactNode }) => {
|
||||||
|
const [toasts, setToasts] = useState<Toast[]>([]);
|
||||||
|
|
||||||
|
const showToast = useCallback((message: string, type: ToastType = 'info') => {
|
||||||
|
const id = Math.random().toString(36).substring(2, 9);
|
||||||
|
const newToast = { id, message, type };
|
||||||
|
|
||||||
|
setToasts(prev => [...prev, newToast]);
|
||||||
|
|
||||||
|
// Auto remove after 3 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
setToasts(prev => prev.filter(t => t.id !== id));
|
||||||
|
}, 3000);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const removeToast = useCallback((id: string) => {
|
||||||
|
setToasts(prev => prev.filter(t => t.id !== id));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ToastContext.Provider value={{ showToast }}>
|
||||||
|
{children}
|
||||||
|
<ToastContainer toasts={toasts} removeToast={removeToast} />
|
||||||
|
</ToastContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
12
src/data/businessTypes.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
export const businessTypes = [
|
||||||
|
{ id: 'proprietorship', label: 'Sole Proprietorship' },
|
||||||
|
{ id: 'partnership', label: 'Partnership Firm' },
|
||||||
|
{ id: 'llp', label: 'Limited Liability Partnership (LLP)' },
|
||||||
|
{ id: 'pvt_ltd', label: 'Private Limited Company' },
|
||||||
|
{ id: 'pub_ltd', label: 'Public Limited Company' },
|
||||||
|
{ id: 'opc', label: 'One Person Company (OPC)' },
|
||||||
|
{ id: 'huf', label: 'Hindu Undivided Family (HUF)' },
|
||||||
|
{ id: 'trust', label: 'Trust / NGO / Society' },
|
||||||
|
{ id: 'freelancer', label: 'Freelancer / Individual Consultant' },
|
||||||
|
{ id: 'other', label: 'Other' }
|
||||||
|
];
|
||||||
242
src/data/countryCodes.ts
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
export const countryCodes = [
|
||||||
|
{ name: "Afghanistan", code: "+93", iso: "AF" },
|
||||||
|
{ name: "Albania", code: "+355", iso: "AL" },
|
||||||
|
{ name: "Algeria", code: "+213", iso: "DZ" },
|
||||||
|
{ name: "American Samoa", code: "+1-684", iso: "AS" },
|
||||||
|
{ name: "Andorra", code: "+376", iso: "AD" },
|
||||||
|
{ name: "Angola", code: "+244", iso: "AO" },
|
||||||
|
{ name: "Anguilla", code: "+1-264", iso: "AI" },
|
||||||
|
{ name: "Antarctica", code: "+672", iso: "AQ" },
|
||||||
|
{ name: "Antigua and Barbuda", code: "+1-268", iso: "AG" },
|
||||||
|
{ name: "Argentina", code: "+54", iso: "AR" },
|
||||||
|
{ name: "Armenia", code: "+374", iso: "AM" },
|
||||||
|
{ name: "Aruba", code: "+297", iso: "AW" },
|
||||||
|
{ name: "Australia", code: "+61", iso: "AU" },
|
||||||
|
{ name: "Austria", code: "+43", iso: "AT" },
|
||||||
|
{ name: "Azerbaijan", code: "+994", iso: "AZ" },
|
||||||
|
{ name: "Bahamas", code: "+1-242", iso: "BS" },
|
||||||
|
{ name: "Bahrain", code: "+973", iso: "BH" },
|
||||||
|
{ name: "Bangladesh", code: "+880", iso: "BD" },
|
||||||
|
{ name: "Barbados", code: "+1-246", iso: "BB" },
|
||||||
|
{ name: "Belarus", code: "+375", iso: "BY" },
|
||||||
|
{ name: "Belgium", code: "+32", iso: "BE" },
|
||||||
|
{ name: "Belize", code: "+501", iso: "BZ" },
|
||||||
|
{ name: "Benin", code: "+229", iso: "BJ" },
|
||||||
|
{ name: "Bermuda", code: "+1-441", iso: "BM" },
|
||||||
|
{ name: "Bhutan", code: "+975", iso: "BT" },
|
||||||
|
{ name: "Bolivia", code: "+591", iso: "BO" },
|
||||||
|
{ name: "Bosnia and Herzegovina", code: "+387", iso: "BA" },
|
||||||
|
{ name: "Botswana", code: "+267", iso: "BW" },
|
||||||
|
{ name: "Brazil", code: "+55", iso: "BR" },
|
||||||
|
{ name: "British Indian Ocean Territory", code: "+246", iso: "IO" },
|
||||||
|
{ name: "British Virgin Islands", code: "+1-284", iso: "VG" },
|
||||||
|
{ name: "Brunei", code: "+673", iso: "BN" },
|
||||||
|
{ name: "Bulgaria", code: "+359", iso: "BG" },
|
||||||
|
{ name: "Burkina Faso", code: "+226", iso: "BF" },
|
||||||
|
{ name: "Burundi", code: "+257", iso: "BI" },
|
||||||
|
{ name: "Cambodia", code: "+855", iso: "KH" },
|
||||||
|
{ name: "Cameroon", code: "+237", iso: "CM" },
|
||||||
|
{ name: "Canada", code: "+1", iso: "CA" },
|
||||||
|
{ name: "Cape Verde", code: "+238", iso: "CV" },
|
||||||
|
{ name: "Cayman Islands", code: "+1-345", iso: "KY" },
|
||||||
|
{ name: "Central African Republic", code: "+236", iso: "CF" },
|
||||||
|
{ name: "Chad", code: "+235", iso: "TD" },
|
||||||
|
{ name: "Chile", code: "+56", iso: "CL" },
|
||||||
|
{ name: "China", code: "+86", iso: "CN" },
|
||||||
|
{ name: "Christmas Island", code: "+61", iso: "CX" },
|
||||||
|
{ name: "Cocos Islands", code: "+61", iso: "CC" },
|
||||||
|
{ name: "Colombia", code: "+57", iso: "CO" },
|
||||||
|
{ name: "Comoros", code: "+269", iso: "KM" },
|
||||||
|
{ name: "Cook Islands", code: "+682", iso: "CK" },
|
||||||
|
{ name: "Costa Rica", code: "+506", iso: "CR" },
|
||||||
|
{ name: "Croatia", code: "+385", iso: "HR" },
|
||||||
|
{ name: "Cuba", code: "+53", iso: "CU" },
|
||||||
|
{ name: "Curacao", code: "+599", iso: "CW" },
|
||||||
|
{ name: "Cyprus", code: "+357", iso: "CY" },
|
||||||
|
{ name: "Czech Republic", code: "+420", iso: "CZ" },
|
||||||
|
{ name: "Democratic Republic of the Congo", code: "+243", iso: "CD" },
|
||||||
|
{ name: "Denmark", code: "+45", iso: "DK" },
|
||||||
|
{ name: "Djibouti", code: "+253", iso: "DJ" },
|
||||||
|
{ name: "Dominica", code: "+1-767", iso: "DM" },
|
||||||
|
{ name: "Dominican Republic", code: "+1-809", iso: "DO" },
|
||||||
|
{ name: "East Timor", code: "+670", iso: "TL" },
|
||||||
|
{ name: "Ecuador", code: "+593", iso: "EC" },
|
||||||
|
{ name: "Egypt", code: "+20", iso: "EG" },
|
||||||
|
{ name: "El Salvador", code: "+503", iso: "SV" },
|
||||||
|
{ name: "Equatorial Guinea", code: "+240", iso: "GQ" },
|
||||||
|
{ name: "Eritrea", code: "+291", iso: "ER" },
|
||||||
|
{ name: "Estonia", code: "+372", iso: "EE" },
|
||||||
|
{ name: "Ethiopia", code: "+251", iso: "ET" },
|
||||||
|
{ name: "Falkland Islands", code: "+500", iso: "FK" },
|
||||||
|
{ name: "Faroe Islands", code: "+298", iso: "FO" },
|
||||||
|
{ name: "Fiji", code: "+679", iso: "FJ" },
|
||||||
|
{ name: "Finland", code: "+358", iso: "FI" },
|
||||||
|
{ name: "France", code: "+33", iso: "FR" },
|
||||||
|
{ name: "French Polynesia", code: "+689", iso: "PF" },
|
||||||
|
{ name: "Gabon", code: "+241", iso: "GA" },
|
||||||
|
{ name: "Gambia", code: "+220", iso: "GM" },
|
||||||
|
{ name: "Georgia", code: "+995", iso: "GE" },
|
||||||
|
{ name: "Germany", code: "+49", iso: "DE" },
|
||||||
|
{ name: "Ghana", code: "+233", iso: "GH" },
|
||||||
|
{ name: "Gibraltar", code: "+350", iso: "GI" },
|
||||||
|
{ name: "Greece", code: "+30", iso: "GR" },
|
||||||
|
{ name: "Greenland", code: "+299", iso: "GL" },
|
||||||
|
{ name: "Grenada", code: "+1-473", iso: "GD" },
|
||||||
|
{ name: "Guam", code: "+1-671", iso: "GU" },
|
||||||
|
{ name: "Guatemala", code: "+502", iso: "GT" },
|
||||||
|
{ name: "Guernsey", code: "+44-1481", iso: "GG" },
|
||||||
|
{ name: "Guinea", code: "+224", iso: "GN" },
|
||||||
|
{ name: "Guinea-Bissau", code: "+245", iso: "GW" },
|
||||||
|
{ name: "Guyana", code: "+592", iso: "GY" },
|
||||||
|
{ name: "Haiti", code: "+509", iso: "HT" },
|
||||||
|
{ name: "Honduras", code: "+504", iso: "HN" },
|
||||||
|
{ name: "Hong Kong", code: "+852", iso: "HK" },
|
||||||
|
{ name: "Hungary", code: "+36", iso: "HU" },
|
||||||
|
{ name: "Iceland", code: "+354", iso: "IS" },
|
||||||
|
{ name: "India", code: "+91", iso: "IN" },
|
||||||
|
{ name: "Indonesia", code: "+62", iso: "ID" },
|
||||||
|
{ name: "Iran", code: "+98", iso: "IR" },
|
||||||
|
{ name: "Iraq", code: "+964", iso: "IQ" },
|
||||||
|
{ name: "Ireland", code: "+353", iso: "IE" },
|
||||||
|
{ name: "Isle of Man", code: "+44-1624", iso: "IM" },
|
||||||
|
{ name: "Israel", code: "+972", iso: "IL" },
|
||||||
|
{ name: "Italy", code: "+39", iso: "IT" },
|
||||||
|
{ name: "Ivory Coast", code: "+225", iso: "CI" },
|
||||||
|
{ name: "Jamaica", code: "+1-876", iso: "JM" },
|
||||||
|
{ name: "Japan", code: "+81", iso: "JP" },
|
||||||
|
{ name: "Jersey", code: "+44-1534", iso: "JE" },
|
||||||
|
{ name: "Jordan", code: "+962", iso: "JO" },
|
||||||
|
{ name: "Kazakhstan", code: "+7", iso: "KZ" },
|
||||||
|
{ name: "Kenya", code: "+254", iso: "KE" },
|
||||||
|
{ name: "Kiribati", code: "+686", iso: "KI" },
|
||||||
|
{ name: "Kosovo", code: "+383", iso: "XK" },
|
||||||
|
{ name: "Kuwait", code: "+965", iso: "KW" },
|
||||||
|
{ name: "Kyrgyzstan", code: "+996", iso: "KG" },
|
||||||
|
{ name: "Laos", code: "+856", iso: "LA" },
|
||||||
|
{ name: "Latvia", code: "+371", iso: "LV" },
|
||||||
|
{ name: "Lebanon", code: "+961", iso: "LB" },
|
||||||
|
{ name: "Lesotho", code: "+266", iso: "LS" },
|
||||||
|
{ name: "Liberia", code: "+231", iso: "LR" },
|
||||||
|
{ name: "Libya", code: "+218", iso: "LY" },
|
||||||
|
{ name: "Liechtenstein", code: "+423", iso: "LI" },
|
||||||
|
{ name: "Lithuania", code: "+370", iso: "LT" },
|
||||||
|
{ name: "Luxembourg", code: "+352", iso: "LU" },
|
||||||
|
{ name: "Macau", code: "+853", iso: "MO" },
|
||||||
|
{ name: "Macedonia", code: "+389", iso: "MK" },
|
||||||
|
{ name: "Madagascar", code: "+261", iso: "MG" },
|
||||||
|
{ name: "Malawi", code: "+265", iso: "MW" },
|
||||||
|
{ name: "Malaysia", code: "+60", iso: "MY" },
|
||||||
|
{ name: "Maldives", code: "+960", iso: "MV" },
|
||||||
|
{ name: "Mali", code: "+223", iso: "ML" },
|
||||||
|
{ name: "Malta", code: "+356", iso: "MT" },
|
||||||
|
{ name: "Marshall Islands", code: "+692", iso: "MH" },
|
||||||
|
{ name: "Mauritania", code: "+222", iso: "MR" },
|
||||||
|
{ name: "Mauritius", code: "+230", iso: "MU" },
|
||||||
|
{ name: "Mayotte", code: "+262", iso: "YT" },
|
||||||
|
{ name: "Mexico", code: "+52", iso: "MX" },
|
||||||
|
{ name: "Micronesia", code: "+691", iso: "FM" },
|
||||||
|
{ name: "Moldova", code: "+373", iso: "MD" },
|
||||||
|
{ name: "Monaco", code: "+377", iso: "MC" },
|
||||||
|
{ name: "Mongolia", code: "+976", iso: "MN" },
|
||||||
|
{ name: "Montenegro", code: "+382", iso: "ME" },
|
||||||
|
{ name: "Montserrat", code: "+1-664", iso: "MS" },
|
||||||
|
{ name: "Morocco", code: "+212", iso: "MA" },
|
||||||
|
{ name: "Mozambique", code: "+258", iso: "MZ" },
|
||||||
|
{ name: "Myanmar", code: "+95", iso: "MM" },
|
||||||
|
{ name: "Namibia", code: "+264", iso: "NA" },
|
||||||
|
{ name: "Nauru", code: "+674", iso: "NR" },
|
||||||
|
{ name: "Nepal", code: "+977", iso: "NP" },
|
||||||
|
{ name: "Netherlands", code: "+31", iso: "NL" },
|
||||||
|
{ name: "Netherlands Antilles", code: "+599", iso: "AN" },
|
||||||
|
{ name: "New Caledonia", code: "+687", iso: "NC" },
|
||||||
|
{ name: "New Zealand", code: "+64", iso: "NZ" },
|
||||||
|
{ name: "Nicaragua", code: "+505", iso: "NI" },
|
||||||
|
{ name: "Niger", code: "+227", iso: "NE" },
|
||||||
|
{ name: "Nigeria", code: "+234", iso: "NG" },
|
||||||
|
{ name: "Niue", code: "+683", iso: "NU" },
|
||||||
|
{ name: "North Korea", code: "+850", iso: "KP" },
|
||||||
|
{ name: "Northern Mariana Islands", code: "+1-670", iso: "MP" },
|
||||||
|
{ name: "Norway", code: "+47", iso: "NO" },
|
||||||
|
{ name: "Oman", code: "+968", iso: "OM" },
|
||||||
|
{ name: "Pakistan", code: "+92", iso: "PK" },
|
||||||
|
{ name: "Palau", code: "+680", iso: "PW" },
|
||||||
|
{ name: "Palestine", code: "+970", iso: "PS" },
|
||||||
|
{ name: "Panama", code: "+507", iso: "PA" },
|
||||||
|
{ name: "Papua New Guinea", code: "+675", iso: "PG" },
|
||||||
|
{ name: "Paraguay", code: "+595", iso: "PY" },
|
||||||
|
{ name: "Peru", code: "+51", iso: "PE" },
|
||||||
|
{ name: "Philippines", code: "+63", iso: "PH" },
|
||||||
|
{ name: "Pitcairn", code: "+64", iso: "PN" },
|
||||||
|
{ name: "Poland", code: "+48", iso: "PL" },
|
||||||
|
{ name: "Portugal", code: "+351", iso: "PT" },
|
||||||
|
{ name: "Puerto Rico", code: "+1-787", iso: "PR" },
|
||||||
|
{ name: "Qatar", code: "+974", iso: "QA" },
|
||||||
|
{ name: "Republic of the Congo", code: "+242", iso: "CG" },
|
||||||
|
{ name: "Reunion", code: "+262", iso: "RE" },
|
||||||
|
{ name: "Romania", code: "+40", iso: "RO" },
|
||||||
|
{ name: "Russia", code: "+7", iso: "RU" },
|
||||||
|
{ name: "Rwanda", code: "+250", iso: "RW" },
|
||||||
|
{ name: "Saint Barthelemy", code: "+590", iso: "BL" },
|
||||||
|
{ name: "Saint Helena", code: "+290", iso: "SH" },
|
||||||
|
{ name: "Saint Kitts and Nevis", code: "+1-869", iso: "KN" },
|
||||||
|
{ name: "Saint Lucia", code: "+1-758", iso: "LC" },
|
||||||
|
{ name: "Saint Martin", code: "+590", iso: "MF" },
|
||||||
|
{ name: "Saint Pierre and Miquelon", code: "+508", iso: "PM" },
|
||||||
|
{ name: "Saint Vincent and the Grenadines", code: "+1-784", iso: "VC" },
|
||||||
|
{ name: "Samoa", code: "+685", iso: "WS" },
|
||||||
|
{ name: "San Marino", code: "+378", iso: "SM" },
|
||||||
|
{ name: "Sao Tome and Principe", code: "+239", iso: "ST" },
|
||||||
|
{ name: "Saudi Arabia", code: "+966", iso: "SA" },
|
||||||
|
{ name: "Senegal", code: "+221", iso: "SN" },
|
||||||
|
{ name: "Serbia", code: "+381", iso: "RS" },
|
||||||
|
{ name: "Seychelles", code: "+248", iso: "SC" },
|
||||||
|
{ name: "Sierra Leone", code: "+232", iso: "SL" },
|
||||||
|
{ name: "Singapore", code: "+65", iso: "SG" },
|
||||||
|
{ name: "Sint Maarten", code: "+1-721", iso: "SX" },
|
||||||
|
{ name: "Slovakia", code: "+421", iso: "SK" },
|
||||||
|
{ name: "Slovenia", code: "+386", iso: "SI" },
|
||||||
|
{ name: "Solomon Islands", code: "+677", iso: "SB" },
|
||||||
|
{ name: "Somalia", code: "+252", iso: "SO" },
|
||||||
|
{ name: "South Africa", code: "+27", iso: "ZA" },
|
||||||
|
{ name: "South Korea", code: "+82", iso: "KR" },
|
||||||
|
{ name: "South Sudan", code: "+211", iso: "SS" },
|
||||||
|
{ name: "Spain", code: "+34", iso: "ES" },
|
||||||
|
{ name: "Sri Lanka", code: "+94", iso: "LK" },
|
||||||
|
{ name: "Sudan", code: "+249", iso: "SD" },
|
||||||
|
{ name: "Suriname", code: "+597", iso: "SR" },
|
||||||
|
{ name: "Svalbard and Jan Mayen", code: "+47", iso: "SJ" },
|
||||||
|
{ name: "Swaziland", code: "+268", iso: "SZ" },
|
||||||
|
{ name: "Sweden", code: "+46", iso: "SE" },
|
||||||
|
{ name: "Switzerland", code: "+41", iso: "CH" },
|
||||||
|
{ name: "Syria", code: "+963", iso: "SY" },
|
||||||
|
{ name: "Taiwan", code: "+886", iso: "TW" },
|
||||||
|
{ name: "Tajikistan", code: "+992", iso: "TJ" },
|
||||||
|
{ name: "Tanzania", code: "+255", iso: "TZ" },
|
||||||
|
{ name: "Thailand", code: "+66", iso: "TH" },
|
||||||
|
{ name: "Togo", code: "+228", iso: "TG" },
|
||||||
|
{ name: "Tokelau", code: "+690", iso: "TK" },
|
||||||
|
{ name: "Tonga", code: "+676", iso: "TO" },
|
||||||
|
{ name: "Trinidad and Tobago", code: "+1-868", iso: "TT" },
|
||||||
|
{ name: "Tunisia", code: "+216", iso: "TN" },
|
||||||
|
{ name: "Turkey", code: "+90", iso: "TR" },
|
||||||
|
{ name: "Turkmenistan", code: "+993", iso: "TM" },
|
||||||
|
{ name: "Turks and Caicos Islands", code: "+1-649", iso: "TC" },
|
||||||
|
{ name: "Tuvalu", code: "+688", iso: "TV" },
|
||||||
|
{ name: "U.S. Virgin Islands", code: "+1-340", iso: "VI" },
|
||||||
|
{ name: "Uganda", code: "+256", iso: "UG" },
|
||||||
|
{ name: "Ukraine", code: "+380", iso: "UA" },
|
||||||
|
{ name: "United Arab Emirates", code: "+971", iso: "AE" },
|
||||||
|
{ name: "United Kingdom", code: "+44", iso: "GB" },
|
||||||
|
{ name: "United States", code: "+1", iso: "US" },
|
||||||
|
{ name: "Uruguay", code: "+598", iso: "UY" },
|
||||||
|
{ name: "Uzbekistan", code: "+998", iso: "UZ" },
|
||||||
|
{ name: "Vanuatu", code: "+678", iso: "VU" },
|
||||||
|
{ name: "Vatican", code: "+379", iso: "VA" },
|
||||||
|
{ name: "Venezuela", code: "+58", iso: "VE" },
|
||||||
|
{ name: "Vietnam", code: "+84", iso: "VN" },
|
||||||
|
{ name: "Wallis and Futuna", code: "+681", iso: "WF" },
|
||||||
|
{ name: "Western Sahara", code: "+212", iso: "EH" },
|
||||||
|
{ name: "Yemen", code: "+967", iso: "YE" },
|
||||||
|
{ name: "Zambia", code: "+260", iso: "ZM" },
|
||||||
|
{ name: "Zimbabwe", code: "+263", iso: "ZW" },
|
||||||
|
];
|
||||||
151
src/data/indianLocations.ts
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
export interface StateData {
|
||||||
|
name: string;
|
||||||
|
cities: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const indianLocations: StateData[] = [
|
||||||
|
{
|
||||||
|
name: "Andhra Pradesh",
|
||||||
|
cities: ["Visakhapatnam", "Vijayawada", "Guntur", "Nellore", "Kurnool", "Rajahmundry", "Tirupati", "Kakinada"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Arunachal Pradesh",
|
||||||
|
cities: ["Itanagar", "Tawang", "Pasighat", "Ziro", "Bomdila"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Assam",
|
||||||
|
cities: ["Guwahati", "Silchar", "Dibrugarh", "Jorhat", "Nagaon", "Tinsukia", "Tezpur"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Bihar",
|
||||||
|
cities: ["Patna", "Gaya", "Bhagalpur", "Muzaffarpur", "Purnia", "Darbhanga", "Bihar Sharif"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Chhattisgarh",
|
||||||
|
cities: ["Raipur", "Bhilai", "Bilaspur", "Korba", "Durg", "Rajnandgaon", "Raigarh"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Goa",
|
||||||
|
cities: ["Panaji", "Vasco da Gama", "Margao", "Mapusa", "Ponda"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Gujarat",
|
||||||
|
cities: ["Ahmedabad", "Surat", "Vadodara", "Rajkot", "Bhavnagar", "Jamnagar", "Gandhinagar", "Junagadh"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Haryana",
|
||||||
|
cities: ["Gurugram", "Faridabad", "Panipat", "Ambala", "Yamunanagar", "Rohtak", "Hisar", "Karnal"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Himachal Pradesh",
|
||||||
|
cities: ["Shimla", "Dharamshala", "Manali", "Mandi", "Solan", "Baddi", "Kullu"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Jharkhand",
|
||||||
|
cities: ["Ranchi", "Jamshedpur", "Dhanbad", "Bokaro", "Hazaribagh", "Deoghar"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Karnataka",
|
||||||
|
cities: ["Bengaluru", "Mysuru", "Hubballi", "Mangaluru", "Belagavi", "Davangere", "Ballari", "Shivamogga"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Kerala",
|
||||||
|
cities: ["Thiruvananthapuram", "Kochi", "Kozhikode", "Thrissur", "Kollam", "Kannur", "Alappuzha"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Madhya Pradesh",
|
||||||
|
cities: ["Indore", "Bhopal", "Jabalpur", "Gwalior", "Ujjain", "Sagar", "Dewas", "Satna"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Maharashtra",
|
||||||
|
cities: ["Mumbai", "Pune", "Nagpur", "Thane", "Nashik", "Aurangabad", "Solapur", "Navi Mumbai", "Amravati"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Manipur",
|
||||||
|
cities: ["Imphal", "Churachandpur", "Thoubal"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Meghalaya",
|
||||||
|
cities: ["Shillong", "Tura", "Jowai"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Mizoram",
|
||||||
|
cities: ["Aizawl", "Lunglei", "Champhai"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Nagaland",
|
||||||
|
cities: ["Kohima", "Dimapur", "Mokokchung"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Odisha",
|
||||||
|
cities: ["Bhubaneswar", "Cuttack", "Rourkela", "Berhampur", "Sambalpur", "Puri", "Balasore"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Punjab",
|
||||||
|
cities: ["Ludhiana", "Amritsar", "Jalandhar", "Patiala", "Bathinda", "Mohali", "Pathankot"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Rajasthan",
|
||||||
|
cities: ["Jaipur", "Jodhpur", "Kota", "Bikaner", "Ajmer", "Udaipur", "Bhilwara", "Alwar"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Sikkim",
|
||||||
|
cities: ["Gangtok", "Namchi", "Gyalshing"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Tamil Nadu",
|
||||||
|
cities: ["Chennai", "Coimbatore", "Madurai", "Tiruchirappalli", "Salem", "Tirunelveli", "Erode", "Vellore"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Telangana",
|
||||||
|
cities: ["Hyderabad", "Warangal", "Nizamabad", "Karimnagar", "Khammam", "Ramagundam"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Tripura",
|
||||||
|
cities: ["Agartala", "Udaipur", "Dharmanagar"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Uttar Pradesh",
|
||||||
|
cities: ["Lucknow", "Kanpur", "Ghaziabad", "Agra", "Varanasi", "Meerut", "Prayagraj", "Noida", "Gorakhpur"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Uttarakhand",
|
||||||
|
cities: ["Dehradun", "Haridwar", "Roorkee", "Haldwani", "Rudrapur", "Rishikesh"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "West Bengal",
|
||||||
|
cities: ["Kolkata", "Asansol", "Siliguri", "Durgapur", "Bardhaman", "Malda", "Kharagpur"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Andaman and Nicobar Islands",
|
||||||
|
cities: ["Port Blair"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Chandigarh",
|
||||||
|
cities: ["Chandigarh"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Dadra and Nagar Haveli and Daman and Diu",
|
||||||
|
cities: ["Daman", "Diu", "Silvassa"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Delhi",
|
||||||
|
cities: ["New Delhi", "North Delhi", "South Delhi", "East Delhi", "West Delhi"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Jammu and Kashmir",
|
||||||
|
cities: ["Srinagar", "Jammu", "Anantnag", "Baramulla"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Ladakh",
|
||||||
|
cities: ["Leh", "Kargil"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Lakshadweep",
|
||||||
|
cities: ["Kavaratti"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Puducherry",
|
||||||
|
cities: ["Puducherry", "Karaikal", "Mahe", "Yanam"]
|
||||||
|
}
|
||||||
|
];
|
||||||
81
src/translations/as.json
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
{
|
||||||
|
"nav": {
|
||||||
|
"dashboard": "डैशबोर्ड",
|
||||||
|
"accounts": "खाते",
|
||||||
|
"crm": "सीआरएम",
|
||||||
|
"ham": "मानव संसाधन और उपस्थिति",
|
||||||
|
"appointments": "नियुक्तियाँ",
|
||||||
|
"projects": "परियोजनाएं",
|
||||||
|
"documents": "दस्तावेज़",
|
||||||
|
"settings": "सेटिंग्स",
|
||||||
|
"logout": "लॉग आउट"
|
||||||
|
},
|
||||||
|
"accounts": {
|
||||||
|
"overview": "अवलोकन",
|
||||||
|
"invoices": "चालान",
|
||||||
|
"journal": "जर्नल",
|
||||||
|
"ledger": "खाता बही",
|
||||||
|
"reports": "रिपोर्ट"
|
||||||
|
},
|
||||||
|
"crm": {
|
||||||
|
"dashboard": "डैशबोर्ड",
|
||||||
|
"leads": "लीड्स",
|
||||||
|
"opportunities": "अवसर",
|
||||||
|
"quotes": "उद्धरण",
|
||||||
|
"analytics": "विश्लेषण"
|
||||||
|
},
|
||||||
|
"ham": {
|
||||||
|
"dashboard": "डैशबोर्ड",
|
||||||
|
"employees": "कर्मचारी",
|
||||||
|
"attendance": "उपस्थिति",
|
||||||
|
"leave": "छुट्टी",
|
||||||
|
"performance": "प्रदर्शन",
|
||||||
|
"payroll": "वेतन",
|
||||||
|
"analytics": "विश्लेषण"
|
||||||
|
},
|
||||||
|
"appointments": {
|
||||||
|
"dashboard": "डैशबोर्ड",
|
||||||
|
"book": "नियुक्ति बुक करें",
|
||||||
|
"myAppointments": "मेरी नियुक्तियाँ",
|
||||||
|
"calendar": "कैलेंडर",
|
||||||
|
"types": "नियुक्ति प्रकार",
|
||||||
|
"settings": "सेटिंग्स"
|
||||||
|
},
|
||||||
|
"documents": {
|
||||||
|
"dashboard": "डैशबोर्ड",
|
||||||
|
"allFiles": "सभी फाइलें",
|
||||||
|
"upload": "अपलोड करें",
|
||||||
|
"sign": "दस्तावेज़ पर हस्ताक्षर करें",
|
||||||
|
"templates": "टेम्पलेट",
|
||||||
|
"activity": "गतिविधि लॉग"
|
||||||
|
},
|
||||||
|
"common": {
|
||||||
|
"save": "सहेजें",
|
||||||
|
"cancel": "रद्द करें",
|
||||||
|
"delete": "हटाएं",
|
||||||
|
"edit": "संपादित करें",
|
||||||
|
"add": "जोड़ें",
|
||||||
|
"create": "बनाएं",
|
||||||
|
"update": "अपडेट करें",
|
||||||
|
"search": "खोजें",
|
||||||
|
"filter": "फ़िल्टर",
|
||||||
|
"export": "निर्यात",
|
||||||
|
"import": "आयात",
|
||||||
|
"close": "बंद करें",
|
||||||
|
"submit": "जमा करें",
|
||||||
|
"reset": "रीसेट करें",
|
||||||
|
"confirm": "पुष्टि करें",
|
||||||
|
"yes": "हाँ",
|
||||||
|
"no": "नहीं",
|
||||||
|
"loading": "लोड हो रहा है...",
|
||||||
|
"success": "सफलता",
|
||||||
|
"error": "त्रुटि",
|
||||||
|
"warning": "चेतावनी",
|
||||||
|
"info": "जानकारी"
|
||||||
|
},
|
||||||
|
"dashboard": {
|
||||||
|
"title": "डैशबोर्ड",
|
||||||
|
"subtitle": "आपके व्यवसाय का अवलोकन",
|
||||||
|
"welcome": "वापसी पर स्वागत है"
|
||||||
|
}
|
||||||
|
}
|
||||||
81
src/translations/bn.json
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
{
|
||||||
|
"nav": {
|
||||||
|
"dashboard": "डैशबोर्ड",
|
||||||
|
"accounts": "खाते",
|
||||||
|
"crm": "सीआरएम",
|
||||||
|
"ham": "मानव संसाधन और उपस्थिति",
|
||||||
|
"appointments": "नियुक्तियाँ",
|
||||||
|
"projects": "परियोजनाएं",
|
||||||
|
"documents": "दस्तावेज़",
|
||||||
|
"settings": "सेटिंग्स",
|
||||||
|
"logout": "लॉग आउट"
|
||||||
|
},
|
||||||
|
"accounts": {
|
||||||
|
"overview": "अवलोकन",
|
||||||
|
"invoices": "चालान",
|
||||||
|
"journal": "जर्नल",
|
||||||
|
"ledger": "खाता बही",
|
||||||
|
"reports": "रिपोर्ट"
|
||||||
|
},
|
||||||
|
"crm": {
|
||||||
|
"dashboard": "डैशबोर्ड",
|
||||||
|
"leads": "लीड्स",
|
||||||
|
"opportunities": "अवसर",
|
||||||
|
"quotes": "उद्धरण",
|
||||||
|
"analytics": "विश्लेषण"
|
||||||
|
},
|
||||||
|
"ham": {
|
||||||
|
"dashboard": "डैशबोर्ड",
|
||||||
|
"employees": "कर्मचारी",
|
||||||
|
"attendance": "उपस्थिति",
|
||||||
|
"leave": "छुट्टी",
|
||||||
|
"performance": "प्रदर्शन",
|
||||||
|
"payroll": "वेतन",
|
||||||
|
"analytics": "विश्लेषण"
|
||||||
|
},
|
||||||
|
"appointments": {
|
||||||
|
"dashboard": "डैशबोर्ड",
|
||||||
|
"book": "नियुक्ति बुक करें",
|
||||||
|
"myAppointments": "मेरी नियुक्तियाँ",
|
||||||
|
"calendar": "कैलेंडर",
|
||||||
|
"types": "नियुक्ति प्रकार",
|
||||||
|
"settings": "सेटिंग्स"
|
||||||
|
},
|
||||||
|
"documents": {
|
||||||
|
"dashboard": "डैशबोर्ड",
|
||||||
|
"allFiles": "सभी फाइलें",
|
||||||
|
"upload": "अपलोड करें",
|
||||||
|
"sign": "दस्तावेज़ पर हस्ताक्षर करें",
|
||||||
|
"templates": "टेम्पलेट",
|
||||||
|
"activity": "गतिविधि लॉग"
|
||||||
|
},
|
||||||
|
"common": {
|
||||||
|
"save": "सहेजें",
|
||||||
|
"cancel": "रद्द करें",
|
||||||
|
"delete": "हटाएं",
|
||||||
|
"edit": "संपादित करें",
|
||||||
|
"add": "जोड़ें",
|
||||||
|
"create": "बनाएं",
|
||||||
|
"update": "अपडेट करें",
|
||||||
|
"search": "खोजें",
|
||||||
|
"filter": "फ़िल्टर",
|
||||||
|
"export": "निर्यात",
|
||||||
|
"import": "आयात",
|
||||||
|
"close": "बंद करें",
|
||||||
|
"submit": "जमा करें",
|
||||||
|
"reset": "रीसेट करें",
|
||||||
|
"confirm": "पुष्टि करें",
|
||||||
|
"yes": "हाँ",
|
||||||
|
"no": "नहीं",
|
||||||
|
"loading": "लोड हो रहा है...",
|
||||||
|
"success": "सफलता",
|
||||||
|
"error": "त्रुटि",
|
||||||
|
"warning": "चेतावनी",
|
||||||
|
"info": "जानकारी"
|
||||||
|
},
|
||||||
|
"dashboard": {
|
||||||
|
"title": "डैशबोर्ड",
|
||||||
|
"subtitle": "आपके व्यवसाय का अवलोकन",
|
||||||
|
"welcome": "वापसी पर स्वागत है"
|
||||||
|
}
|
||||||
|
}
|
||||||
101
src/translations/en.json
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
{
|
||||||
|
"nav": {
|
||||||
|
"dashboard": "Dashboard",
|
||||||
|
"accounts": "Accounts",
|
||||||
|
"crm": "CRM",
|
||||||
|
"ham": "HR & Attendance",
|
||||||
|
"appointments": "Appointments",
|
||||||
|
"projects": "Projects",
|
||||||
|
"documents": "Documents",
|
||||||
|
"settings": "Settings",
|
||||||
|
"logout": "Logout"
|
||||||
|
},
|
||||||
|
"accounts": {
|
||||||
|
"overview": "Overview",
|
||||||
|
"invoices": "Invoices",
|
||||||
|
"journal": "Journal",
|
||||||
|
"ledger": "Ledger",
|
||||||
|
"reports": "Reports"
|
||||||
|
},
|
||||||
|
"crm": {
|
||||||
|
"dashboard": "Dashboard",
|
||||||
|
"leads": "Leads",
|
||||||
|
"opportunities": "Opportunities",
|
||||||
|
"quotes": "Quotes",
|
||||||
|
"analytics": "Analytics"
|
||||||
|
},
|
||||||
|
"ham": {
|
||||||
|
"dashboard": "Dashboard",
|
||||||
|
"employees": "Employees",
|
||||||
|
"attendance": "Attendance",
|
||||||
|
"leave": "Leave",
|
||||||
|
"performance": "Performance",
|
||||||
|
"payroll": "Payroll",
|
||||||
|
"analytics": "Analytics"
|
||||||
|
},
|
||||||
|
"appointments": {
|
||||||
|
"dashboard": "Dashboard",
|
||||||
|
"book": "Book Appointment",
|
||||||
|
"myAppointments": "My Appointments",
|
||||||
|
"calendar": "Calendar",
|
||||||
|
"types": "Appointment Types",
|
||||||
|
"settings": "Settings"
|
||||||
|
},
|
||||||
|
"documents": {
|
||||||
|
"dashboard": "Dashboard",
|
||||||
|
"allFiles": "All Files",
|
||||||
|
"upload": "Upload",
|
||||||
|
"sign": "Sign Documents",
|
||||||
|
"templates": "Templates",
|
||||||
|
"activity": "Activity Log"
|
||||||
|
},
|
||||||
|
"common": {
|
||||||
|
"save": "Save",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"delete": "Delete",
|
||||||
|
"edit": "Edit",
|
||||||
|
"add": "Add",
|
||||||
|
"create": "Create",
|
||||||
|
"update": "Update",
|
||||||
|
"search": "Search",
|
||||||
|
"filter": "Filter",
|
||||||
|
"export": "Export",
|
||||||
|
"import": "Import",
|
||||||
|
"close": "Close",
|
||||||
|
"submit": "Submit",
|
||||||
|
"reset": "Reset",
|
||||||
|
"confirm": "Confirm",
|
||||||
|
"yes": "Yes",
|
||||||
|
"no": "No",
|
||||||
|
"loading": "Loading...",
|
||||||
|
"success": "Success",
|
||||||
|
"error": "Error",
|
||||||
|
"warning": "Warning",
|
||||||
|
"info": "Information"
|
||||||
|
},
|
||||||
|
"dashboard": {
|
||||||
|
"title": "Dashboard",
|
||||||
|
"subtitle": "Overview of your business",
|
||||||
|
"welcome": "Welcome back",
|
||||||
|
"stats": {
|
||||||
|
"totalRevenue": "Total Revenue",
|
||||||
|
"activeProjects": "Active Projects",
|
||||||
|
"teamSize": "Team Size",
|
||||||
|
"pendingTasks": "Pending Tasks",
|
||||||
|
"growth": "Growth",
|
||||||
|
"sinceLastMonth": "since last month"
|
||||||
|
},
|
||||||
|
"sections": {
|
||||||
|
"recentActivity": "Recent Activity",
|
||||||
|
"upcomingDeadlines": "Upcoming Deadlines",
|
||||||
|
"performance": "Performance Overview",
|
||||||
|
"quickActions": "Quick Actions"
|
||||||
|
},
|
||||||
|
"actions": {
|
||||||
|
"createInvoice": "Create Invoice",
|
||||||
|
"addLead": "Add Lead",
|
||||||
|
"addEmployee": "Add Employee",
|
||||||
|
"bookAppointment": "Book Appointment"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
81
src/translations/gu.json
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
{
|
||||||
|
"nav": {
|
||||||
|
"dashboard": "डैशबोर्ड",
|
||||||
|
"accounts": "खाते",
|
||||||
|
"crm": "सीआरएम",
|
||||||
|
"ham": "मानव संसाधन और उपस्थिति",
|
||||||
|
"appointments": "नियुक्तियाँ",
|
||||||
|
"projects": "परियोजनाएं",
|
||||||
|
"documents": "दस्तावेज़",
|
||||||
|
"settings": "सेटिंग्स",
|
||||||
|
"logout": "लॉग आउट"
|
||||||
|
},
|
||||||
|
"accounts": {
|
||||||
|
"overview": "अवलोकन",
|
||||||
|
"invoices": "चालान",
|
||||||
|
"journal": "जर्नल",
|
||||||
|
"ledger": "खाता बही",
|
||||||
|
"reports": "रिपोर्ट"
|
||||||
|
},
|
||||||
|
"crm": {
|
||||||
|
"dashboard": "डैशबोर्ड",
|
||||||
|
"leads": "लीड्स",
|
||||||
|
"opportunities": "अवसर",
|
||||||
|
"quotes": "उद्धरण",
|
||||||
|
"analytics": "विश्लेषण"
|
||||||
|
},
|
||||||
|
"ham": {
|
||||||
|
"dashboard": "डैशबोर्ड",
|
||||||
|
"employees": "कर्मचारी",
|
||||||
|
"attendance": "उपस्थिति",
|
||||||
|
"leave": "छुट्टी",
|
||||||
|
"performance": "प्रदर्शन",
|
||||||
|
"payroll": "वेतन",
|
||||||
|
"analytics": "विश्लेषण"
|
||||||
|
},
|
||||||
|
"appointments": {
|
||||||
|
"dashboard": "डैशबोर्ड",
|
||||||
|
"book": "नियुक्ति बुक करें",
|
||||||
|
"myAppointments": "मेरी नियुक्तियाँ",
|
||||||
|
"calendar": "कैलेंडर",
|
||||||
|
"types": "नियुक्ति प्रकार",
|
||||||
|
"settings": "सेटिंग्स"
|
||||||
|
},
|
||||||
|
"documents": {
|
||||||
|
"dashboard": "डैशबोर्ड",
|
||||||
|
"allFiles": "सभी फाइलें",
|
||||||
|
"upload": "अपलोड करें",
|
||||||
|
"sign": "दस्तावेज़ पर हस्ताक्षर करें",
|
||||||
|
"templates": "टेम्पलेट",
|
||||||
|
"activity": "गतिविधि लॉग"
|
||||||
|
},
|
||||||
|
"common": {
|
||||||
|
"save": "सहेजें",
|
||||||
|
"cancel": "रद्द करें",
|
||||||
|
"delete": "हटाएं",
|
||||||
|
"edit": "संपादित करें",
|
||||||
|
"add": "जोड़ें",
|
||||||
|
"create": "बनाएं",
|
||||||
|
"update": "अपडेट करें",
|
||||||
|
"search": "खोजें",
|
||||||
|
"filter": "फ़िल्टर",
|
||||||
|
"export": "निर्यात",
|
||||||
|
"import": "आयात",
|
||||||
|
"close": "बंद करें",
|
||||||
|
"submit": "जमा करें",
|
||||||
|
"reset": "रीसेट करें",
|
||||||
|
"confirm": "पुष्टि करें",
|
||||||
|
"yes": "हाँ",
|
||||||
|
"no": "नहीं",
|
||||||
|
"loading": "लोड हो रहा है...",
|
||||||
|
"success": "सफलता",
|
||||||
|
"error": "त्रुटि",
|
||||||
|
"warning": "चेतावनी",
|
||||||
|
"info": "जानकारी"
|
||||||
|
},
|
||||||
|
"dashboard": {
|
||||||
|
"title": "डैशबोर्ड",
|
||||||
|
"subtitle": "आपके व्यवसाय का अवलोकन",
|
||||||
|
"welcome": "वापसी पर स्वागत है"
|
||||||
|
}
|
||||||
|
}
|
||||||