Production deployment with Docker and full system fixes
- Added Docker support (Dockerfiles, docker-compose.yml) - Fixed authentication and authorization (token storage, CORS, permissions) - Fixed API response transformations for all modules - Added production deployment scripts and guides - Fixed frontend permission checks and module access - Added database seeding script for production - Complete documentation for deployment and configuration Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
10
frontend/.dockerignore
Normal file
10
frontend/.dockerignore
Normal file
@@ -0,0 +1,10 @@
|
||||
node_modules
|
||||
npm-debug.log
|
||||
.next
|
||||
.env
|
||||
.env.local
|
||||
.git
|
||||
*.md
|
||||
.DS_Store
|
||||
out
|
||||
coverage
|
||||
52
frontend/Dockerfile
Normal file
52
frontend/Dockerfile
Normal file
@@ -0,0 +1,52 @@
|
||||
# Frontend Dockerfile
|
||||
FROM node:18-alpine AS base
|
||||
|
||||
# Install dependencies only when needed
|
||||
FROM base AS deps
|
||||
RUN apk add --no-cache libc6-compat
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package files
|
||||
COPY package*.json ./
|
||||
RUN npm ci
|
||||
|
||||
# Build stage
|
||||
FROM base AS builder
|
||||
WORKDIR /app
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
|
||||
# Build-time args for Next.js (NEXT_PUBLIC_* are baked into the bundle)
|
||||
ARG NEXT_PUBLIC_API_URL=https://zerp.atmata-group.com/api/v1
|
||||
ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL
|
||||
|
||||
# Set build-time environment variable
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
|
||||
# Build Next.js application
|
||||
RUN npm run build
|
||||
|
||||
# Production stage
|
||||
FROM base AS runner
|
||||
WORKDIR /app
|
||||
|
||||
ENV NODE_ENV=production
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
|
||||
# Create non-root user
|
||||
RUN addgroup --system --gid 1001 nodejs && \
|
||||
adduser --system --uid 1001 nextjs
|
||||
|
||||
# Copy necessary files
|
||||
COPY --from=builder /app/public ./public
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||
|
||||
USER nextjs
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
ENV PORT=3000
|
||||
ENV HOSTNAME="0.0.0.0"
|
||||
|
||||
CMD ["node", "server.js"]
|
||||
@@ -1,6 +1,7 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
reactStrictMode: true,
|
||||
output: 'standalone',
|
||||
env: {
|
||||
API_URL: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:5000/api/v1',
|
||||
},
|
||||
|
||||
31
frontend/package-lock.json
generated
31
frontend/package-lock.json
generated
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "mind14-frontend",
|
||||
"name": "z-crm-frontend",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "mind14-frontend",
|
||||
"name": "z-crm-frontend",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@tanstack/react-query": "^5.17.9",
|
||||
@@ -15,6 +15,7 @@
|
||||
"next": "14.0.4",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hot-toast": "^2.6.0",
|
||||
"recharts": "^2.10.3",
|
||||
"zustand": "^4.4.7"
|
||||
},
|
||||
@@ -3185,6 +3186,15 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/goober": {
|
||||
"version": "2.1.18",
|
||||
"resolved": "https://registry.npmjs.org/goober/-/goober-2.1.18.tgz",
|
||||
"integrity": "sha512-2vFqsaDVIT9Gz7N6kAL++pLpp41l3PfDuusHcjnGLfR6+huZkl6ziX+zgVC3ZxpqWhzH6pyDdGrCeDhMIvwaxw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"csstype": "^3.0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
@@ -4826,6 +4836,23 @@
|
||||
"react": "^18.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/react-hot-toast": {
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.6.0.tgz",
|
||||
"integrity": "sha512-bH+2EBMZ4sdyou/DPrfgIouFpcRLCJ+HoCA32UoAYHn6T3Ur5yfcDCeSr5mwldl6pFOsiocmrXMuoCJ1vV8bWg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"csstype": "^3.1.3",
|
||||
"goober": "^2.1.16"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16",
|
||||
"react-dom": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/react-is": {
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
|
||||
@@ -9,26 +9,26 @@
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"next": "14.0.4",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"@tanstack/react-query": "^5.17.9",
|
||||
"axios": "^1.6.5",
|
||||
"date-fns": "^3.0.6",
|
||||
"lucide-react": "^0.303.0",
|
||||
"zustand": "^4.4.7",
|
||||
"recharts": "^2.10.3"
|
||||
"next": "14.0.4",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hot-toast": "^2.6.0",
|
||||
"recharts": "^2.10.3",
|
||||
"zustand": "^4.4.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"typescript": "^5",
|
||||
"tailwindcss": "^3.4.0",
|
||||
"postcss": "^8",
|
||||
"autoprefixer": "^10.0.1",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "14.0.4"
|
||||
"eslint-config-next": "14.0.4",
|
||||
"postcss": "^8",
|
||||
"tailwindcss": "^3.4.0",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
0
frontend/public/.gitkeep
Normal file
0
frontend/public/.gitkeep
Normal file
@@ -115,7 +115,7 @@ export default function SystemSettings() {
|
||||
/>
|
||||
)}
|
||||
|
||||
{setting.type === 'select' && setting.options && (
|
||||
{setting.type === 'select' && 'options' in setting && setting.options && (
|
||||
<select
|
||||
defaultValue={setting.value as string}
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -83,14 +83,10 @@ function DashboardContent() {
|
||||
}
|
||||
]
|
||||
|
||||
// TEMPORARY: Show all modules for development/testing
|
||||
// Will implement role-based filtering after all features are verified
|
||||
const availableModules = allModules // Show all modules for now
|
||||
|
||||
// TODO: Re-enable permission filtering later:
|
||||
// const availableModules = allModules.filter(module =>
|
||||
// hasPermission(module.permission, module.permission, 'read')
|
||||
// )
|
||||
// Filter modules based on user permissions
|
||||
const availableModules = allModules.filter(module =>
|
||||
hasPermission(module.permission, 'view')
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100">
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,7 @@ import { Cairo, Readex_Pro } from 'next/font/google'
|
||||
import './globals.css'
|
||||
import { Providers } from './providers'
|
||||
import { AuthProvider } from '@/contexts/AuthContext'
|
||||
import { Toaster } from 'react-hot-toast'
|
||||
|
||||
const cairo = Cairo({
|
||||
subsets: ['latin', 'arabic'],
|
||||
@@ -31,6 +32,32 @@ export default function RootLayout({
|
||||
<body className={`${readexPro.variable} ${cairo.variable} font-readex`}>
|
||||
<AuthProvider>
|
||||
<Providers>{children}</Providers>
|
||||
<Toaster
|
||||
position="top-center"
|
||||
reverseOrder={false}
|
||||
toastOptions={{
|
||||
duration: 4000,
|
||||
style: {
|
||||
background: '#fff',
|
||||
color: '#363636',
|
||||
fontFamily: 'var(--font-readex)',
|
||||
},
|
||||
success: {
|
||||
duration: 3000,
|
||||
iconTheme: {
|
||||
primary: '#10B981',
|
||||
secondary: '#fff',
|
||||
},
|
||||
},
|
||||
error: {
|
||||
duration: 5000,
|
||||
iconTheme: {
|
||||
primary: '#EF4444',
|
||||
secondary: '#fff',
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</AuthProvider>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
35
frontend/src/components/LoadingSpinner.tsx
Normal file
35
frontend/src/components/LoadingSpinner.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
'use client'
|
||||
|
||||
import { Loader2 } from 'lucide-react'
|
||||
|
||||
interface LoadingSpinnerProps {
|
||||
size?: 'sm' | 'md' | 'lg'
|
||||
fullScreen?: boolean
|
||||
message?: string
|
||||
}
|
||||
|
||||
export default function LoadingSpinner({ size = 'md', fullScreen = false, message }: LoadingSpinnerProps) {
|
||||
const sizeClasses = {
|
||||
sm: 'h-4 w-4',
|
||||
md: 'h-8 w-8',
|
||||
lg: 'h-12 w-12'
|
||||
}
|
||||
|
||||
const spinner = (
|
||||
<div className="flex flex-col items-center justify-center gap-3">
|
||||
<Loader2 className={`${sizeClasses[size]} animate-spin text-primary-600`} />
|
||||
{message && <p className="text-sm text-gray-600">{message}</p>}
|
||||
</div>
|
||||
)
|
||||
|
||||
if (fullScreen) {
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-white bg-opacity-90">
|
||||
{spinner}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return spinner
|
||||
}
|
||||
|
||||
67
frontend/src/components/Modal.tsx
Normal file
67
frontend/src/components/Modal.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
'use client'
|
||||
|
||||
import { X } from 'lucide-react'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
interface ModalProps {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
title: string
|
||||
children: React.ReactNode
|
||||
size?: 'sm' | 'md' | 'lg' | 'xl' | '2xl'
|
||||
}
|
||||
|
||||
export default function Modal({ isOpen, onClose, title, children, size = 'lg' }: ModalProps) {
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
document.body.style.overflow = 'hidden'
|
||||
} else {
|
||||
document.body.style.overflow = 'unset'
|
||||
}
|
||||
return () => {
|
||||
document.body.style.overflow = 'unset'
|
||||
}
|
||||
}, [isOpen])
|
||||
|
||||
if (!isOpen) return null
|
||||
|
||||
const sizeClasses = {
|
||||
sm: 'max-w-md',
|
||||
md: 'max-w-lg',
|
||||
lg: 'max-w-2xl',
|
||||
xl: 'max-w-4xl',
|
||||
'2xl': 'max-w-6xl'
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 overflow-y-auto">
|
||||
{/* Backdrop */}
|
||||
<div
|
||||
className="fixed inset-0 bg-black bg-opacity-50 transition-opacity"
|
||||
onClick={onClose}
|
||||
/>
|
||||
|
||||
{/* Modal */}
|
||||
<div className="flex min-h-screen items-center justify-center p-4">
|
||||
<div className={`relative w-full ${sizeClasses[size]} bg-white rounded-xl shadow-2xl transform transition-all`}>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-6 border-b border-gray-200">
|
||||
<h3 className="text-2xl font-bold text-gray-900">{title}</h3>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
|
||||
>
|
||||
<X className="h-5 w-5 text-gray-600" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-6">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
import React, { createContext, useContext, useState, useEffect } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:5001/api/v1'
|
||||
|
||||
interface User {
|
||||
id: string
|
||||
employeeId: string
|
||||
@@ -20,12 +22,13 @@ interface User {
|
||||
interface Permission {
|
||||
id: string
|
||||
module: string
|
||||
canView: boolean
|
||||
canCreate: boolean
|
||||
canEdit: boolean
|
||||
canDelete: boolean
|
||||
canExport: boolean
|
||||
canApprove: boolean
|
||||
actions?: string[]
|
||||
canView?: boolean
|
||||
canCreate?: boolean
|
||||
canEdit?: boolean
|
||||
canDelete?: boolean
|
||||
canExport?: boolean
|
||||
canApprove?: boolean
|
||||
}
|
||||
|
||||
interface AuthContextType {
|
||||
@@ -46,7 +49,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
|
||||
// Check for existing token on mount
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem('token')
|
||||
const token = localStorage.getItem('accessToken')
|
||||
if (token) {
|
||||
// Verify token and get user data
|
||||
fetchUserData(token)
|
||||
@@ -55,9 +58,24 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Transform backend permissions format to frontend format
|
||||
const transformPermissions = (permissions: any[]): Permission[] => {
|
||||
return permissions.map(p => ({
|
||||
id: p.id,
|
||||
module: p.module,
|
||||
actions: p.actions,
|
||||
canView: p.actions?.includes('read') || false,
|
||||
canCreate: p.actions?.includes('create') || false,
|
||||
canEdit: p.actions?.includes('update') || false,
|
||||
canDelete: p.actions?.includes('delete') || false,
|
||||
canExport: p.actions?.includes('export') || false,
|
||||
canApprove: p.actions?.includes('approve') || false,
|
||||
}))
|
||||
}
|
||||
|
||||
const fetchUserData = async (token: string) => {
|
||||
try {
|
||||
const response = await fetch('http://localhost:5001/api/v1/auth/me', {
|
||||
const response = await fetch(`${API_URL}/auth/me`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
@@ -65,13 +83,17 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
|
||||
if (response.ok) {
|
||||
const userData = await response.json()
|
||||
setUser(userData.data)
|
||||
const user = userData.data
|
||||
if (user.role?.permissions) {
|
||||
user.role.permissions = transformPermissions(user.role.permissions)
|
||||
}
|
||||
setUser(user)
|
||||
} else {
|
||||
localStorage.removeItem('token')
|
||||
localStorage.removeItem('accessToken')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch user data:', error)
|
||||
localStorage.removeItem('token')
|
||||
localStorage.removeItem('accessToken')
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
@@ -79,7 +101,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
|
||||
const login = async (email: string, password: string) => {
|
||||
try {
|
||||
const response = await fetch('http://localhost:5001/api/v1/auth/login', {
|
||||
const response = await fetch(`${API_URL}/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
@@ -94,10 +116,15 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
}
|
||||
|
||||
// Store token
|
||||
localStorage.setItem('token', data.data.accessToken)
|
||||
localStorage.setItem('accessToken', data.data.accessToken)
|
||||
localStorage.setItem('refreshToken', data.data.refreshToken)
|
||||
|
||||
// Set user data
|
||||
setUser(data.data.user)
|
||||
// Transform permissions and set user data
|
||||
const userData = data.data.user
|
||||
if (userData.role?.permissions) {
|
||||
userData.role.permissions = transformPermissions(userData.role.permissions)
|
||||
}
|
||||
setUser(userData)
|
||||
|
||||
// Redirect to dashboard
|
||||
router.push('/dashboard')
|
||||
@@ -107,7 +134,8 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
}
|
||||
|
||||
const logout = () => {
|
||||
localStorage.removeItem('token')
|
||||
localStorage.removeItem('accessToken')
|
||||
localStorage.removeItem('refreshToken')
|
||||
setUser(null)
|
||||
router.push('/')
|
||||
}
|
||||
|
||||
140
frontend/src/lib/api/admin.ts
Normal file
140
frontend/src/lib/api/admin.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import { api } from '../api'
|
||||
|
||||
// Users API
|
||||
export interface User {
|
||||
id: string
|
||||
email: string
|
||||
username: string
|
||||
status: string
|
||||
employeeId?: string
|
||||
employee?: any
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
export interface CreateUserData {
|
||||
email: string
|
||||
username: string
|
||||
password: string
|
||||
employeeId?: string
|
||||
}
|
||||
|
||||
export interface UpdateUserData {
|
||||
email?: string
|
||||
username?: string
|
||||
password?: string
|
||||
status?: string
|
||||
employeeId?: string
|
||||
}
|
||||
|
||||
export const usersAPI = {
|
||||
getAll: async (): Promise<User[]> => {
|
||||
const response = await api.get('/auth/users')
|
||||
return response.data.data || response.data
|
||||
},
|
||||
|
||||
getById: async (id: string): Promise<User> => {
|
||||
const response = await api.get(`/auth/users/${id}`)
|
||||
return response.data.data
|
||||
},
|
||||
|
||||
create: async (data: CreateUserData): Promise<User> => {
|
||||
const response = await api.post('/auth/register', data)
|
||||
return response.data.data
|
||||
},
|
||||
|
||||
update: async (id: string, data: UpdateUserData): Promise<User> => {
|
||||
const response = await api.put(`/auth/users/${id}`, data)
|
||||
return response.data.data
|
||||
},
|
||||
|
||||
delete: async (id: string): Promise<void> => {
|
||||
await api.delete(`/auth/users/${id}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Roles & Permissions API
|
||||
export interface Role {
|
||||
id: string
|
||||
name: string
|
||||
nameAr?: string
|
||||
permissions: Permission[]
|
||||
}
|
||||
|
||||
export interface Permission {
|
||||
id: string
|
||||
module: string
|
||||
resource: string
|
||||
action: string
|
||||
}
|
||||
|
||||
export const rolesAPI = {
|
||||
getAll: async (): Promise<Role[]> => {
|
||||
const response = await api.get('/admin/roles')
|
||||
return response.data.data || []
|
||||
},
|
||||
|
||||
update: async (id: string, permissions: Permission[]): Promise<Role> => {
|
||||
const response = await api.put(`/admin/roles/${id}/permissions`, { permissions })
|
||||
return response.data.data
|
||||
}
|
||||
}
|
||||
|
||||
// Audit Logs API
|
||||
export interface AuditLog {
|
||||
id: string
|
||||
entityType: string
|
||||
entityId: string
|
||||
action: string
|
||||
userId: string
|
||||
user?: any
|
||||
changes?: any
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
export const auditLogsAPI = {
|
||||
getAll: async (filters?: any): Promise<AuditLog[]> => {
|
||||
const params = new URLSearchParams()
|
||||
if (filters?.entityType) params.append('entityType', filters.entityType)
|
||||
if (filters?.action) params.append('action', filters.action)
|
||||
if (filters?.startDate) params.append('startDate', filters.startDate)
|
||||
if (filters?.endDate) params.append('endDate', filters.endDate)
|
||||
|
||||
const response = await api.get(`/admin/audit-logs?${params.toString()}`)
|
||||
return response.data.data || []
|
||||
}
|
||||
}
|
||||
|
||||
// System Settings API
|
||||
export interface SystemSetting {
|
||||
key: string
|
||||
value: any
|
||||
description?: string
|
||||
}
|
||||
|
||||
export const settingsAPI = {
|
||||
getAll: async (): Promise<SystemSetting[]> => {
|
||||
const response = await api.get('/admin/settings')
|
||||
return response.data.data || []
|
||||
},
|
||||
|
||||
update: async (key: string, value: any): Promise<SystemSetting> => {
|
||||
const response = await api.put(`/admin/settings/${key}`, { value })
|
||||
return response.data.data
|
||||
}
|
||||
}
|
||||
|
||||
// System Health API
|
||||
export interface SystemHealth {
|
||||
status: string
|
||||
database: string
|
||||
memory: any
|
||||
uptime: number
|
||||
}
|
||||
|
||||
export const healthAPI = {
|
||||
check: async (): Promise<SystemHealth> => {
|
||||
const response = await api.get('/admin/health')
|
||||
return response.data.data || response.data
|
||||
}
|
||||
}
|
||||
99
frontend/src/lib/api/campaigns.ts
Normal file
99
frontend/src/lib/api/campaigns.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { api } from '../api'
|
||||
|
||||
export interface Campaign {
|
||||
id: string
|
||||
campaignNumber: string
|
||||
name: string
|
||||
nameAr?: string
|
||||
type: string // EMAIL, WHATSAPP, SOCIAL, EXHIBITION, MULTI_CHANNEL
|
||||
description?: string
|
||||
content?: any
|
||||
targetAudience?: any
|
||||
budget?: number
|
||||
actualCost?: number
|
||||
expectedROI?: number
|
||||
actualROI?: number
|
||||
startDate?: string
|
||||
endDate?: string
|
||||
status: string // PLANNED, ACTIVE, PAUSED, COMPLETED, CANCELLED
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
export interface CreateCampaignData {
|
||||
name: string
|
||||
nameAr?: string
|
||||
type: string
|
||||
description?: string
|
||||
budget?: number
|
||||
expectedROI?: number
|
||||
startDate?: string
|
||||
endDate?: string
|
||||
}
|
||||
|
||||
export interface UpdateCampaignData extends Partial<CreateCampaignData> {
|
||||
actualCost?: number
|
||||
actualROI?: number
|
||||
status?: string
|
||||
}
|
||||
|
||||
export interface CampaignFilters {
|
||||
search?: string
|
||||
type?: string
|
||||
status?: string
|
||||
page?: number
|
||||
pageSize?: number
|
||||
}
|
||||
|
||||
export interface CampaignsResponse {
|
||||
campaigns: Campaign[]
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
totalPages: number
|
||||
}
|
||||
|
||||
export const campaignsAPI = {
|
||||
// Get all campaigns with filters and pagination
|
||||
getAll: async (filters: CampaignFilters = {}): Promise<CampaignsResponse> => {
|
||||
const params = new URLSearchParams()
|
||||
if (filters.search) params.append('search', filters.search)
|
||||
if (filters.type) params.append('type', filters.type)
|
||||
if (filters.status) params.append('status', filters.status)
|
||||
if (filters.page) params.append('page', filters.page.toString())
|
||||
if (filters.pageSize) params.append('pageSize', filters.pageSize.toString())
|
||||
|
||||
const response = await api.get(`/marketing/campaigns?${params.toString()}`)
|
||||
const { data, pagination } = response.data
|
||||
return {
|
||||
campaigns: data || [],
|
||||
total: pagination?.total || 0,
|
||||
page: pagination?.page || 1,
|
||||
pageSize: pagination?.pageSize || 20,
|
||||
totalPages: pagination?.totalPages || 0,
|
||||
}
|
||||
},
|
||||
|
||||
// Get single campaign by ID
|
||||
getById: async (id: string): Promise<Campaign> => {
|
||||
const response = await api.get(`/marketing/campaigns/${id}`)
|
||||
return response.data.data
|
||||
},
|
||||
|
||||
// Create new campaign
|
||||
create: async (data: CreateCampaignData): Promise<Campaign> => {
|
||||
const response = await api.post('/marketing/campaigns', data)
|
||||
return response.data.data
|
||||
},
|
||||
|
||||
// Update existing campaign
|
||||
update: async (id: string, data: UpdateCampaignData): Promise<Campaign> => {
|
||||
const response = await api.put(`/marketing/campaigns/${id}`, data)
|
||||
return response.data.data
|
||||
},
|
||||
|
||||
// Delete campaign
|
||||
delete: async (id: string): Promise<void> => {
|
||||
await api.delete(`/marketing/campaigns/${id}`)
|
||||
}
|
||||
}
|
||||
171
frontend/src/lib/api/contacts.ts
Normal file
171
frontend/src/lib/api/contacts.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
import { api } from '../api'
|
||||
|
||||
export interface Contact {
|
||||
id: string
|
||||
uniqueContactId: string
|
||||
type: string
|
||||
name: string
|
||||
nameAr?: string
|
||||
email?: string
|
||||
phone?: string
|
||||
mobile?: string
|
||||
website?: string
|
||||
companyName?: string
|
||||
companyNameAr?: string
|
||||
taxNumber?: string
|
||||
commercialRegister?: string
|
||||
address?: string
|
||||
city?: string
|
||||
country?: string
|
||||
postalCode?: string
|
||||
status: string
|
||||
rating?: number
|
||||
source: string
|
||||
tags?: string[]
|
||||
customFields?: any
|
||||
categories?: any[]
|
||||
parent?: any
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
createdBy?: any
|
||||
}
|
||||
|
||||
export interface CreateContactData {
|
||||
type: string
|
||||
name: string
|
||||
nameAr?: string
|
||||
email?: string
|
||||
phone?: string
|
||||
mobile?: string
|
||||
website?: string
|
||||
companyName?: string
|
||||
companyNameAr?: string
|
||||
taxNumber?: string
|
||||
commercialRegister?: string
|
||||
address?: string
|
||||
city?: string
|
||||
country?: string
|
||||
postalCode?: string
|
||||
categories?: string[]
|
||||
tags?: string[]
|
||||
parentId?: string
|
||||
source: string
|
||||
customFields?: any
|
||||
}
|
||||
|
||||
export interface UpdateContactData extends Partial<CreateContactData> {
|
||||
status?: string
|
||||
rating?: number
|
||||
}
|
||||
|
||||
export interface ContactFilters {
|
||||
search?: string
|
||||
type?: string
|
||||
status?: string
|
||||
category?: string
|
||||
source?: string
|
||||
rating?: number
|
||||
page?: number
|
||||
pageSize?: number
|
||||
}
|
||||
|
||||
export interface ContactsResponse {
|
||||
contacts: Contact[]
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
totalPages: number
|
||||
}
|
||||
|
||||
export const contactsAPI = {
|
||||
// Get all contacts with filters and pagination
|
||||
getAll: async (filters: ContactFilters = {}): Promise<ContactsResponse> => {
|
||||
const params = new URLSearchParams()
|
||||
if (filters.search) params.append('search', filters.search)
|
||||
if (filters.type) params.append('type', filters.type)
|
||||
if (filters.status) params.append('status', filters.status)
|
||||
if (filters.category) params.append('category', filters.category)
|
||||
if (filters.source) params.append('source', filters.source)
|
||||
if (filters.rating) params.append('rating', filters.rating.toString())
|
||||
if (filters.page) params.append('page', filters.page.toString())
|
||||
if (filters.pageSize) params.append('pageSize', filters.pageSize.toString())
|
||||
|
||||
const response = await api.get(`/contacts?${params.toString()}`)
|
||||
const { data, pagination } = response.data
|
||||
return {
|
||||
contacts: data || [],
|
||||
total: pagination?.total || 0,
|
||||
page: pagination?.page || 1,
|
||||
pageSize: pagination?.pageSize || 20,
|
||||
totalPages: pagination?.totalPages || 0,
|
||||
}
|
||||
},
|
||||
|
||||
// Get single contact by ID
|
||||
getById: async (id: string): Promise<Contact> => {
|
||||
const response = await api.get(`/contacts/${id}`)
|
||||
return response.data.data
|
||||
},
|
||||
|
||||
// Create new contact
|
||||
create: async (data: CreateContactData): Promise<Contact> => {
|
||||
const response = await api.post('/contacts', data)
|
||||
return response.data.data
|
||||
},
|
||||
|
||||
// Update existing contact
|
||||
update: async (id: string, data: UpdateContactData): Promise<Contact> => {
|
||||
const response = await api.put(`/contacts/${id}`, data)
|
||||
return response.data.data
|
||||
},
|
||||
|
||||
// Archive contact (soft delete)
|
||||
archive: async (id: string, reason?: string): Promise<Contact> => {
|
||||
const response = await api.post(`/contacts/${id}/archive`, { reason })
|
||||
return response.data.data
|
||||
},
|
||||
|
||||
// Delete contact (hard delete)
|
||||
delete: async (id: string, reason: string): Promise<void> => {
|
||||
await api.delete(`/contacts/${id}`, { data: { reason } })
|
||||
},
|
||||
|
||||
// Get contact history
|
||||
getHistory: async (id: string): Promise<any[]> => {
|
||||
const response = await api.get(`/contacts/${id}/history`)
|
||||
return response.data.data
|
||||
},
|
||||
|
||||
// Merge contacts
|
||||
merge: async (sourceId: string, targetId: string, reason: string): Promise<Contact> => {
|
||||
const response = await api.post('/contacts/merge', { sourceId, targetId, reason })
|
||||
return response.data.data
|
||||
},
|
||||
|
||||
// Export contacts
|
||||
export: async (filters: ContactFilters = {}): Promise<Blob> => {
|
||||
const params = new URLSearchParams()
|
||||
if (filters.search) params.append('search', filters.search)
|
||||
if (filters.type) params.append('type', filters.type)
|
||||
if (filters.status) params.append('status', filters.status)
|
||||
|
||||
const response = await api.get(`/contacts/export?${params.toString()}`, {
|
||||
responseType: 'blob'
|
||||
})
|
||||
return response.data
|
||||
},
|
||||
|
||||
// Import contacts
|
||||
import: async (file: File): Promise<{ success: number; errors: any[] }> => {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
|
||||
const response = await api.post('/contacts/import', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
})
|
||||
return response.data.data
|
||||
}
|
||||
}
|
||||
|
||||
134
frontend/src/lib/api/deals.ts
Normal file
134
frontend/src/lib/api/deals.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
import { api } from '../api'
|
||||
|
||||
export interface Deal {
|
||||
id: string
|
||||
dealNumber: string
|
||||
name: string
|
||||
contactId: string
|
||||
contact?: any
|
||||
structure: string // B2B, B2C, B2G, PARTNERSHIP
|
||||
pipelineId: string
|
||||
pipeline?: any
|
||||
stage: string
|
||||
estimatedValue: number
|
||||
actualValue?: number
|
||||
currency: string
|
||||
probability?: number
|
||||
expectedCloseDate?: string
|
||||
actualCloseDate?: string
|
||||
ownerId: string
|
||||
owner?: any
|
||||
wonReason?: string
|
||||
lostReason?: string
|
||||
fiscalYear: number
|
||||
status: string
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
export interface CreateDealData {
|
||||
name: string
|
||||
contactId: string
|
||||
structure: string
|
||||
pipelineId: string
|
||||
stage: string
|
||||
estimatedValue: number
|
||||
probability?: number
|
||||
expectedCloseDate?: string
|
||||
ownerId?: string
|
||||
fiscalYear?: number
|
||||
}
|
||||
|
||||
export interface UpdateDealData extends Partial<CreateDealData> {
|
||||
actualValue?: number
|
||||
actualCloseDate?: string
|
||||
wonReason?: string
|
||||
lostReason?: string
|
||||
status?: string
|
||||
}
|
||||
|
||||
export interface DealFilters {
|
||||
search?: string
|
||||
structure?: string
|
||||
stage?: string
|
||||
status?: string
|
||||
ownerId?: string
|
||||
fiscalYear?: number
|
||||
page?: number
|
||||
pageSize?: number
|
||||
}
|
||||
|
||||
export interface DealsResponse {
|
||||
deals: Deal[]
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
totalPages: number
|
||||
}
|
||||
|
||||
export const dealsAPI = {
|
||||
// Get all deals with filters and pagination
|
||||
getAll: async (filters: DealFilters = {}): Promise<DealsResponse> => {
|
||||
const params = new URLSearchParams()
|
||||
if (filters.search) params.append('search', filters.search)
|
||||
if (filters.structure) params.append('structure', filters.structure)
|
||||
if (filters.stage) params.append('stage', filters.stage)
|
||||
if (filters.status) params.append('status', filters.status)
|
||||
if (filters.ownerId) params.append('ownerId', filters.ownerId)
|
||||
if (filters.fiscalYear) params.append('fiscalYear', filters.fiscalYear.toString())
|
||||
if (filters.page) params.append('page', filters.page.toString())
|
||||
if (filters.pageSize) params.append('pageSize', filters.pageSize.toString())
|
||||
|
||||
const response = await api.get(`/crm/deals?${params.toString()}`)
|
||||
const { data, pagination } = response.data
|
||||
return {
|
||||
deals: data || [],
|
||||
total: pagination?.total || 0,
|
||||
page: pagination?.page || 1,
|
||||
pageSize: pagination?.pageSize || 20,
|
||||
totalPages: pagination?.totalPages || 0,
|
||||
}
|
||||
},
|
||||
|
||||
// Get single deal by ID
|
||||
getById: async (id: string): Promise<Deal> => {
|
||||
const response = await api.get(`/crm/deals/${id}`)
|
||||
return response.data.data
|
||||
},
|
||||
|
||||
// Create new deal
|
||||
create: async (data: CreateDealData): Promise<Deal> => {
|
||||
const response = await api.post('/crm/deals', data)
|
||||
return response.data.data
|
||||
},
|
||||
|
||||
// Update existing deal
|
||||
update: async (id: string, data: UpdateDealData): Promise<Deal> => {
|
||||
const response = await api.put(`/crm/deals/${id}`, data)
|
||||
return response.data.data
|
||||
},
|
||||
|
||||
// Update deal stage
|
||||
updateStage: async (id: string, stage: string): Promise<Deal> => {
|
||||
const response = await api.patch(`/crm/deals/${id}/stage`, { stage })
|
||||
return response.data.data
|
||||
},
|
||||
|
||||
// Mark deal as won
|
||||
win: async (id: string, actualValue: number, wonReason: string): Promise<Deal> => {
|
||||
const response = await api.post(`/crm/deals/${id}/win`, { actualValue, wonReason })
|
||||
return response.data.data
|
||||
},
|
||||
|
||||
// Mark deal as lost
|
||||
lose: async (id: string, lostReason: string): Promise<Deal> => {
|
||||
const response = await api.post(`/crm/deals/${id}/lose`, { lostReason })
|
||||
return response.data.data
|
||||
},
|
||||
|
||||
// Get deal history
|
||||
getHistory: async (id: string): Promise<any[]> => {
|
||||
const response = await api.get(`/crm/deals/${id}/history`)
|
||||
return response.data.data
|
||||
}
|
||||
}
|
||||
134
frontend/src/lib/api/employees.ts
Normal file
134
frontend/src/lib/api/employees.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
import { api } from '../api'
|
||||
|
||||
export interface Employee {
|
||||
id: string
|
||||
uniqueEmployeeId: string
|
||||
firstName: string
|
||||
lastName: string
|
||||
firstNameAr?: string
|
||||
lastNameAr?: string
|
||||
email: string
|
||||
phone?: string
|
||||
mobile: string
|
||||
dateOfBirth?: string
|
||||
gender?: string
|
||||
nationality?: string
|
||||
nationalId?: string
|
||||
passportNumber?: string
|
||||
employmentType: string
|
||||
contractType?: string
|
||||
hireDate: string
|
||||
endDate?: string
|
||||
departmentId: string
|
||||
department?: any
|
||||
positionId: string
|
||||
position?: any
|
||||
reportingToId?: string
|
||||
reportingTo?: any
|
||||
baseSalary: number
|
||||
status: string
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
export interface CreateEmployeeData {
|
||||
firstName: string
|
||||
lastName: string
|
||||
firstNameAr?: string
|
||||
lastNameAr?: string
|
||||
email: string
|
||||
phone?: string
|
||||
mobile: string
|
||||
dateOfBirth?: string
|
||||
gender?: string
|
||||
nationality?: string
|
||||
nationalId?: string
|
||||
employmentType: string
|
||||
contractType?: string
|
||||
hireDate: string
|
||||
departmentId: string
|
||||
positionId: string
|
||||
reportingToId?: string
|
||||
baseSalary: number
|
||||
}
|
||||
|
||||
export interface UpdateEmployeeData extends Partial<CreateEmployeeData> {}
|
||||
|
||||
export interface EmployeeFilters {
|
||||
search?: string
|
||||
departmentId?: string
|
||||
positionId?: string
|
||||
status?: string
|
||||
page?: number
|
||||
pageSize?: number
|
||||
}
|
||||
|
||||
export interface EmployeesResponse {
|
||||
employees: Employee[]
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
totalPages: number
|
||||
}
|
||||
|
||||
export const employeesAPI = {
|
||||
// Get all employees with filters and pagination
|
||||
getAll: async (filters: EmployeeFilters = {}): Promise<EmployeesResponse> => {
|
||||
const params = new URLSearchParams()
|
||||
if (filters.search) params.append('search', filters.search)
|
||||
if (filters.departmentId) params.append('departmentId', filters.departmentId)
|
||||
if (filters.positionId) params.append('positionId', filters.positionId)
|
||||
if (filters.status) params.append('status', filters.status)
|
||||
if (filters.page) params.append('page', filters.page.toString())
|
||||
if (filters.pageSize) params.append('pageSize', filters.pageSize.toString())
|
||||
|
||||
const response = await api.get(`/hr/employees?${params.toString()}`)
|
||||
const { data, pagination } = response.data
|
||||
return {
|
||||
employees: data || [],
|
||||
total: pagination?.total || 0,
|
||||
page: pagination?.page || 1,
|
||||
pageSize: pagination?.pageSize || 20,
|
||||
totalPages: pagination?.totalPages || 0,
|
||||
}
|
||||
},
|
||||
|
||||
// Get single employee by ID
|
||||
getById: async (id: string): Promise<Employee> => {
|
||||
const response = await api.get(`/hr/employees/${id}`)
|
||||
return response.data.data
|
||||
},
|
||||
|
||||
// Create new employee
|
||||
create: async (data: CreateEmployeeData): Promise<Employee> => {
|
||||
const response = await api.post('/hr/employees', data)
|
||||
return response.data.data
|
||||
},
|
||||
|
||||
// Update existing employee
|
||||
update: async (id: string, data: UpdateEmployeeData): Promise<Employee> => {
|
||||
const response = await api.put(`/hr/employees/${id}`, data)
|
||||
return response.data.data
|
||||
},
|
||||
|
||||
// Delete employee
|
||||
delete: async (id: string): Promise<void> => {
|
||||
await api.delete(`/hr/employees/${id}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Departments API
|
||||
export const departmentsAPI = {
|
||||
getAll: async (): Promise<any[]> => {
|
||||
const response = await api.get('/hr/departments')
|
||||
return response.data.data
|
||||
}
|
||||
}
|
||||
|
||||
// Positions API
|
||||
export const positionsAPI = {
|
||||
getAll: async (): Promise<any[]> => {
|
||||
const response = await api.get('/hr/positions')
|
||||
return response.data.data
|
||||
}
|
||||
}
|
||||
130
frontend/src/lib/api/products.ts
Normal file
130
frontend/src/lib/api/products.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import { api } from '../api'
|
||||
|
||||
export interface Product {
|
||||
id: string
|
||||
sku: string
|
||||
name: string
|
||||
nameAr?: string
|
||||
description?: string
|
||||
categoryId: string
|
||||
category?: any
|
||||
brand?: string
|
||||
model?: string
|
||||
specifications?: any
|
||||
trackBy: string
|
||||
costPrice: number
|
||||
sellingPrice: number
|
||||
minStock: number
|
||||
maxStock?: number
|
||||
totalStock?: number
|
||||
inventoryItems?: any[]
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
export interface CreateProductData {
|
||||
sku: string
|
||||
name: string
|
||||
nameAr?: string
|
||||
description?: string
|
||||
categoryId: string
|
||||
brand?: string
|
||||
model?: string
|
||||
trackBy?: string
|
||||
costPrice: number
|
||||
sellingPrice: number
|
||||
minStock?: number
|
||||
maxStock?: number
|
||||
}
|
||||
|
||||
export interface UpdateProductData extends Partial<CreateProductData> {}
|
||||
|
||||
export interface ProductFilters {
|
||||
search?: string
|
||||
categoryId?: string
|
||||
brand?: string
|
||||
page?: number
|
||||
pageSize?: number
|
||||
}
|
||||
|
||||
export interface ProductsResponse {
|
||||
products: Product[]
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
totalPages: number
|
||||
}
|
||||
|
||||
export const productsAPI = {
|
||||
// Get all products with filters and pagination
|
||||
getAll: async (filters: ProductFilters = {}): Promise<ProductsResponse> => {
|
||||
const params = new URLSearchParams()
|
||||
if (filters.search) params.append('search', filters.search)
|
||||
if (filters.categoryId) params.append('categoryId', filters.categoryId)
|
||||
if (filters.brand) params.append('brand', filters.brand)
|
||||
if (filters.page) params.append('page', filters.page.toString())
|
||||
if (filters.pageSize) params.append('pageSize', filters.pageSize.toString())
|
||||
|
||||
const response = await api.get(`/inventory/products?${params.toString()}`)
|
||||
const { data, pagination } = response.data
|
||||
return {
|
||||
products: data || [],
|
||||
total: pagination?.total || 0,
|
||||
page: pagination?.page || 1,
|
||||
pageSize: pagination?.pageSize || 20,
|
||||
totalPages: pagination?.totalPages || 0,
|
||||
}
|
||||
},
|
||||
|
||||
// Get single product by ID
|
||||
getById: async (id: string): Promise<Product> => {
|
||||
const response = await api.get(`/inventory/products/${id}`)
|
||||
return response.data.data
|
||||
},
|
||||
|
||||
// Create new product
|
||||
create: async (data: CreateProductData): Promise<Product> => {
|
||||
const response = await api.post('/inventory/products', data)
|
||||
return response.data.data
|
||||
},
|
||||
|
||||
// Update existing product
|
||||
update: async (id: string, data: UpdateProductData): Promise<Product> => {
|
||||
const response = await api.put(`/inventory/products/${id}`, data)
|
||||
return response.data.data
|
||||
},
|
||||
|
||||
// Delete product
|
||||
delete: async (id: string): Promise<void> => {
|
||||
await api.delete(`/inventory/products/${id}`)
|
||||
},
|
||||
|
||||
// Adjust stock
|
||||
adjustStock: async (
|
||||
productId: string,
|
||||
warehouseId: string,
|
||||
quantity: number,
|
||||
type: 'ADD' | 'REMOVE'
|
||||
): Promise<any> => {
|
||||
const response = await api.post(`/inventory/products/${productId}/adjust-stock`, {
|
||||
warehouseId,
|
||||
quantity,
|
||||
type
|
||||
})
|
||||
return response.data.data
|
||||
},
|
||||
|
||||
// Get product history
|
||||
getHistory: async (id: string): Promise<any[]> => {
|
||||
const response = await api.get(`/inventory/products/${id}/history`)
|
||||
return response.data.data
|
||||
}
|
||||
}
|
||||
|
||||
// Categories API
|
||||
export const categoriesAPI = {
|
||||
getAll: async (): Promise<any[]> => {
|
||||
const response = await api.get('/inventory/categories')
|
||||
return response.data.data
|
||||
}
|
||||
}
|
||||
131
frontend/src/lib/api/tasks.ts
Normal file
131
frontend/src/lib/api/tasks.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import { api } from '../api'
|
||||
|
||||
export interface Task {
|
||||
id: string
|
||||
taskNumber: string
|
||||
projectId?: string
|
||||
project?: any
|
||||
phaseId?: string
|
||||
title: string
|
||||
description?: string
|
||||
assignedToId?: string
|
||||
assignedTo?: any
|
||||
priority: string // LOW, MEDIUM, HIGH, CRITICAL
|
||||
status: string // PENDING, IN_PROGRESS, REVIEW, COMPLETED, CANCELLED
|
||||
progress: number
|
||||
startDate?: string
|
||||
dueDate?: string
|
||||
completedAt?: string
|
||||
estimatedHours?: number
|
||||
actualHours?: number
|
||||
tags?: string[]
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
export interface CreateTaskData {
|
||||
projectId?: string
|
||||
title: string
|
||||
description?: string
|
||||
assignedToId?: string
|
||||
priority?: string
|
||||
status?: string
|
||||
progress?: number
|
||||
startDate?: string
|
||||
dueDate?: string
|
||||
estimatedHours?: number
|
||||
tags?: string[]
|
||||
}
|
||||
|
||||
export interface UpdateTaskData extends Partial<CreateTaskData> {
|
||||
progress?: number
|
||||
completedAt?: string
|
||||
actualHours?: number
|
||||
}
|
||||
|
||||
export interface TaskFilters {
|
||||
search?: string
|
||||
projectId?: string
|
||||
assignedToId?: string
|
||||
priority?: string
|
||||
status?: string
|
||||
page?: number
|
||||
pageSize?: number
|
||||
}
|
||||
|
||||
export interface TasksResponse {
|
||||
tasks: Task[]
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
totalPages: number
|
||||
}
|
||||
|
||||
export const tasksAPI = {
|
||||
// Get all tasks with filters and pagination
|
||||
getAll: async (filters: TaskFilters = {}): Promise<TasksResponse> => {
|
||||
const params = new URLSearchParams()
|
||||
if (filters.search) params.append('search', filters.search)
|
||||
if (filters.projectId) params.append('projectId', filters.projectId)
|
||||
if (filters.assignedToId) params.append('assignedToId', filters.assignedToId)
|
||||
if (filters.priority) params.append('priority', filters.priority)
|
||||
if (filters.status) params.append('status', filters.status)
|
||||
if (filters.page) params.append('page', filters.page.toString())
|
||||
if (filters.pageSize) params.append('pageSize', filters.pageSize.toString())
|
||||
|
||||
const response = await api.get(`/projects/tasks?${params.toString()}`)
|
||||
const { data, pagination } = response.data
|
||||
return {
|
||||
tasks: data || [],
|
||||
total: pagination?.total || 0,
|
||||
page: pagination?.page || 1,
|
||||
pageSize: pagination?.pageSize || 20,
|
||||
totalPages: pagination?.totalPages || 0,
|
||||
}
|
||||
},
|
||||
|
||||
// Get single task by ID
|
||||
getById: async (id: string): Promise<Task> => {
|
||||
const response = await api.get(`/projects/tasks/${id}`)
|
||||
return response.data.data
|
||||
},
|
||||
|
||||
// Create new task
|
||||
create: async (data: CreateTaskData): Promise<Task> => {
|
||||
const response = await api.post('/projects/tasks', data)
|
||||
return response.data.data
|
||||
},
|
||||
|
||||
// Update existing task
|
||||
update: async (id: string, data: UpdateTaskData): Promise<Task> => {
|
||||
const response = await api.put(`/projects/tasks/${id}`, data)
|
||||
return response.data.data
|
||||
},
|
||||
|
||||
// Delete task
|
||||
delete: async (id: string): Promise<void> => {
|
||||
await api.delete(`/projects/tasks/${id}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Projects API
|
||||
export interface Project {
|
||||
id: string
|
||||
projectNumber: string
|
||||
name: string
|
||||
nameAr?: string
|
||||
description?: string
|
||||
status: string
|
||||
startDate?: string
|
||||
endDate?: string
|
||||
budget?: number
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
export const projectsAPI = {
|
||||
getAll: async (): Promise<Project[]> => {
|
||||
const response = await api.get('/projects/projects')
|
||||
return response.data.data
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user