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:
Talal Sharabi
2026-02-11 11:25:20 +04:00
parent 35daa52767
commit f31d71ff5a
52 changed files with 9359 additions and 1578 deletions

10
frontend/.dockerignore Normal file
View 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
View 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"]

View File

@@ -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',
},

View File

@@ -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",

View File

@@ -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
View File

View 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

View File

@@ -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

View File

@@ -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

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

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

View File

@@ -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('/')
}

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

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

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

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

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

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

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