first commit

This commit is contained in:
Kapil Panwar
2025-12-22 07:29:45 +05:30
commit e38835f7cb
110 changed files with 28526 additions and 0 deletions

41
.gitignore vendored Normal file
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

26
package.json Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

1
public/file.svg Normal file
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 733 KiB

BIN
public/logo_full.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

BIN
public/logo_symbol.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 673 KiB

BIN
public/startup-bg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 568 KiB

1
public/vercel.svg Normal file
View 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
View 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
View 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>
);
}

View 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;
}
}

View 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>
);
}

View 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>
);
}

View 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'
}}>&times;</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>
);

View 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)}>&times;</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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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;
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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);
}
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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;
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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 */
}

View 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;
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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;
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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 &gt; 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 &gt; 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 &gt; 3 days.
</p>
</div>
</div>
</div>
)}
</div>
</div>
);
}

View 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>
);
}

View 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>
);
}

View 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;
}

View 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;
}

View 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;
}
}

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View 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;
}

View 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
View 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
View 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>
);
}

View 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
View 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
View 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
View 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>
);
}

View 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
View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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
View 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)'}
>
&times;
</button>
</div>
);
};

View 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;
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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": "वापसी पर स्वागत है"
}
}

Some files were not shown because too many files have changed in this diff Show More