29 KiB
Electronic Document Management System (EDMS) - Technical Guide
Table of Contents
- System Overview
- Architecture
- Technology Stack
- Database Schema
- Installation & Setup
- Development Environment
- Component Structure
- API & Data Management
- Security & Authentication
- Deployment
- Maintenance & Monitoring
- Extension & Customization
System Overview
The Electronic Document Management System (EDMS) is a modern web application built with Nuxt.js 3 and Vue.js 3, designed to provide a comprehensive document management solution for organizations. The system implements a hierarchical document organization structure with role-based access control (RBAC) and supports integration with external authentication providers like Authentik.
Key Technical Features
- Modern Frontend: Vue.js 3 with Composition API and script setup syntax
- SSR/SPA Support: Nuxt.js 3 for optimal performance and SEO
- Database: Prisma ORM with MySQL/PostgreSQL support and migrations
- Styling: TailwindCSS with custom component system
- State Management: Pinia for reactive state management with persistence
- Authentication: Flexible authentication with JWT tokens and RBAC
- File Management: Server-side file handling with metadata and versioning
- Real-time Features: Vue reactivity for live updates and notifications
- Responsive Design: Mobile-first approach with adaptive layouts
Architecture
System Architecture
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Frontend │ │ Backend │ │ Database │
│ (Nuxt.js) │◄──►│ (Server API) │◄──►│ (MySQL/PG) │
│ │ │ │ │ │
│ • Vue.js 3 │ │ • Prisma ORM │ │ • User Data │
│ • TailwindCSS │ │ • File System │ │ • Documents │
│ • Pinia Store │ │ • Authentication│ │ • Permissions │
│ • Components │ │ • API Routes │ │ • Audit Logs │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ │ │
│ ┌─────────────────┐ │
│ │ File Storage │ │
└──────────────►│ • Documents │◄────────────┘
│ • Versions │
│ • Thumbnails │
│ • Temp Files │
└─────────────────┘
Application Flow
- Authentication: User authenticates via configured authentication provider
- Authorization: System validates user permissions against RBAC rules
- Navigation: User navigates document hierarchy with tree-based structure
- Data Fetching: Pinia store manages API calls with caching and error handling
- Rendering: Vue components render UI with reactive updates
- File Operations: Server handles secure file upload/download/preview
- Access Control: System enforces granular document permissions
- Audit Trail: All actions are logged for compliance and security
Technology Stack
Frontend Dependencies
{
"core": {
"nuxt": "^3.6.5",
"vue": "^3.x",
"@pinia/nuxt": "^0.4.11",
"@pinia-plugin-persistedstate/nuxt": "^1.1.1"
},
"styling": {
"@nuxtjs/tailwindcss": "^6.8.0",
"sass": "^1.62.0",
"postcss-import": "^15.1.0"
},
"forms": {
"@formkit/nuxt": "^1.0.0",
"@formkit/themes": "^1.0.0",
"@formkit/addons": "^1.0.0",
"@formkit/pro": "^0.115.3"
},
"ui": {
"apexcharts": "^3.36.0",
"vue3-apexcharts": "^1.4.1",
"@vueuse/core": "^9.5.0",
"@vueuse/nuxt": "^9.5.0",
"floating-vue": "^2.0.0-beta.24",
"vue3-dropzone": "^2.0.1"
},
"utilities": {
"luxon": "^3.1.0",
"uuid": "^10.0.0",
"crypto-js": "^4.1.1",
"@shimyshack/uid": "^0.1.7"
}
}
Backend Dependencies
{
"database": {
"@prisma/client": "^5.1.1",
"prisma": "^5.1.1"
},
"authentication": {
"jsonwebtoken": "^8.5.1"
},
"file-handling": {
"jspdf": "^2.5.1"
},
"security": {
"nuxt-security": "^0.13.0"
}
}
Development Tools
- ESLint: Code linting with Vue.js specific rules
- TypeScript: Type safety with Nuxt TypeScript integration
- Prettier: Consistent code formatting
- Vite: Lightning-fast build tool and HMR
- PostCSS: Advanced CSS processing with plugins
Database Schema
Core Models
User Management
model user {
userID Int @id @default(autoincrement())
userSecretKey String? @db.VarChar(255)
userUsername String? @unique @db.VarChar(255)
userPassword String? @db.VarChar(255)
userFullName String? @db.VarChar(255)
userEmail String? @unique @db.VarChar(255)
userPhone String? @db.VarChar(255)
userStatus String? @db.VarChar(255)
userCreatedDate DateTime? @default(now()) @db.DateTime(0)
userModifiedDate DateTime? @updatedAt @db.DateTime(0)
// Relationships
userrole userrole[]
accessRequests AccessRequest[]
permissions AccessPermission[]
documents Document[] @relation("DocumentCreator")
cabinets Cabinet[] @relation("CabinetCreator")
drawers Drawer[] @relation("DrawerCreator")
folders Folder[] @relation("FolderCreator")
subfolders Subfolder[] @relation("SubfolderCreator")
}
model role {
roleID Int @id @default(autoincrement())
roleName String @unique @db.VarChar(255)
roleDescription String? @db.Text
rolePermissions Json? // Flexible permissions structure
roleCreatedDate DateTime @default(now()) @db.DateTime(0)
roleModifiedDate DateTime @updatedAt @db.DateTime(0)
userrole userrole[]
permissions AccessPermission[]
}
Document Hierarchy
// Cabinet → Drawer → Folder → Subfolder → Document
model Cabinet {
id Int @id @default(autoincrement())
name String @db.VarChar(255)
description String? @db.Text
metadata Json? // Flexible metadata storage
createdAt DateTime @default(now()) @db.DateTime(0)
updatedAt DateTime @updatedAt @db.DateTime(0)
createdBy Int
status String @default("active") @db.VarChar(50)
user user @relation("CabinetCreator", fields: [createdBy], references: [userID])
drawers Drawer[]
permissions AccessPermission[]
@@index([createdBy])
@@index([status])
}
model Document {
id Int @id @default(autoincrement())
name String @db.VarChar(255)
description String? @db.Text
fileSize Int @default(0)
fileType String @db.VarChar(100)
fileExtension String @db.VarChar(20)
filePath String @db.VarChar(500)
thumbnailPath String? @db.VarChar(500)
checksum String? @db.VarChar(64)
version Int @default(1)
isTemplate Boolean @default(false)
isPublic Boolean @default(false)
metadata Json? // Custom metadata fields
tags String? @db.Text
folderId Int?
subfolderId Int?
createdAt DateTime @default(now()) @db.DateTime(0)
updatedAt DateTime @updatedAt @db.DateTime(0)
createdBy Int
status String @default("active") @db.VarChar(50)
folder Folder? @relation("FolderDocuments", fields: [folderId], references: [id], onDelete: SetNull)
subfolder Subfolder? @relation("SubfolderDocuments", fields: [subfolderId], references: [id], onDelete: SetNull)
user user @relation("DocumentCreator", fields: [createdBy], references: [userID])
accessRequests AccessRequest[]
permissions AccessPermission[]
versions DocumentVersion[]
@@index([folderId])
@@index([subfolderId])
@@index([createdBy])
@@index([fileType])
@@index([status])
@@fulltext([name, description, tags])
}
Access Control & Workflow
model AccessPermission {
id Int @id @default(autoincrement())
userId Int?
roleId Int?
documentId Int?
cabinetId Int?
drawerId Int?
folderId Int?
subfolderId Int?
permissionLevel String @db.VarChar(50) // view, download, print, edit, full, admin
conditions Json? // Additional permission conditions
createdAt DateTime @default(now()) @db.DateTime(0)
updatedAt DateTime @updatedAt @db.DateTime(0)
expiresAt DateTime? @db.DateTime(0)
createdBy Int?
user user? @relation(fields: [userId], references: [userID], onDelete: SetNull)
role role? @relation(fields: [roleId], references: [roleID], onDelete: SetNull)
document Document? @relation(fields: [documentId], references: [id], onDelete: Cascade)
cabinet Cabinet? @relation(fields: [cabinetId], references: [id], onDelete: Cascade)
drawer Drawer? @relation(fields: [drawerId], references: [id], onDelete: Cascade)
folder Folder? @relation(fields: [folderId], references: [id], onDelete: Cascade)
subfolder Subfolder? @relation(fields: [subfolderId], references: [id], onDelete: Cascade)
@@index([userId])
@@index([roleId])
@@index([documentId])
@@index([expiresAt])
}
model AccessRequest {
id Int @id @default(autoincrement())
documentId Int
userId Int
requestedLevel String @db.VarChar(50) // view, download, print, edit, full
duration String? @db.VarChar(50) // 7d, 14d, 30d, 60d, 90d, permanent
justification String? @db.Text
status String @default("pending") @db.VarChar(50) // pending, approved, rejected, expired
responseNote String? @db.Text
requestedAt DateTime @default(now()) @db.DateTime(0)
respondedAt DateTime? @db.DateTime(0)
respondedBy Int?
expiresAt DateTime? @db.DateTime(0)
document Document @relation(fields: [documentId], references: [id], onDelete: Cascade)
user user @relation(fields: [userId], references: [userID])
@@index([documentId])
@@index([userId])
@@index([status])
}
Installation & Setup
Prerequisites
- Node.js 18+ and npm/yarn/pnpm
- MySQL 8.0+ or PostgreSQL 13+
- Git for version control
- Optional: Docker for containerized deployment
Installation Steps
- Clone Repository
git clone <repository-url>
cd edms
- Install Dependencies
# Using npm
npm install
# Using yarn
yarn install
# Using pnpm
pnpm install
- Environment Configuration
Create
.env
file:
# Database Configuration
DATABASE_URL="mysql://username:password@localhost:3306/edms_db"
# For PostgreSQL: DATABASE_URL="postgresql://username:password@localhost:5432/edms_db"
# Authentication
JWT_SECRET="your-jwt-secret-key-min-256-bits"
SESSION_SECRET="your-session-secret-key"
# External Authentication (Optional)
AUTHENTIK_BASE_URL="https://auth.yourdomain.com"
AUTHENTIK_CLIENT_ID="your-client-id"
AUTHENTIK_CLIENT_SECRET="your-client-secret"
# File Storage
UPLOAD_PATH="/var/uploads/edms"
TEMP_PATH="/var/tmp/edms"
MAX_FILE_SIZE="104857600" # 100MB in bytes
ALLOWED_FILE_TYPES="pdf,doc,docx,xls,xlsx,ppt,pptx,txt,jpg,jpeg,png,gif"
# Application Configuration
NUXT_SECRET_KEY="your-nuxt-app-secret"
BASE_URL="http://localhost:3000"
NODE_ENV="development"
# Security
CORS_ORIGIN="http://localhost:3000"
RATE_LIMIT_MAX="100" # requests per window
RATE_LIMIT_WINDOW="15" # minutes
# Monitoring & Logging
LOG_LEVEL="info"
LOG_FILE="/var/log/edms/app.log"
- Database Setup
# Generate Prisma client
npx prisma generate
# Run database migrations
npx prisma db push
# Seed initial data (optional)
npx prisma db seed
# View database in Prisma Studio (optional)
npx prisma studio
- Development Server
# Start development server
npm run dev
# Access application at http://localhost:3000
Development Environment
Project Structure
edms/
├── components/ # Vue components
│ ├── dms/ # DMS-specific components
│ │ ├── explorer/ # Document explorer interface
│ │ ├── dialogs/ # Modal dialogs and forms
│ │ ├── viewers/ # Document preview components
│ │ └── navigation/ # Tree and breadcrumb navigation
│ ├── layouts/ # Layout components
│ ├── formkit/ # FormKit custom components
│ └── [Rs*].vue # Reusable UI components (RsButton, RsCard, etc.)
├── pages/ # Nuxt pages (file-based routing)
│ ├── dms/ # DMS-related pages
│ ├── dashboard/ # Dashboard and analytics
│ ├── login/ # Authentication pages
│ └── index.vue # Homepage
├── stores/ # Pinia state stores
│ ├── dms.js # Document management store
│ ├── auth.js # Authentication store
│ └── app.js # Global application store
├── middleware/ # Route middleware
│ ├── auth.js # Authentication middleware
│ └── rbac.js # Role-based access control
├── layouts/ # Nuxt layouts
│ ├── default.vue # Default application layout
│ └── auth.vue # Authentication layout
├── server/ # Server-side code
│ ├── api/ # API routes
│ └── middleware/ # Server middleware
├── prisma/ # Database configuration
│ ├── schema.prisma # Database schema
│ └── migrations/ # Database migrations
├── public/ # Static assets
├── assets/ # Build-time assets
├── plugins/ # Nuxt plugins
├── composables/ # Vue composables
└── utils/ # Utility functions
Component Architecture
Core EDMS Components
- DMSExplorer.vue: Main document browser with tree navigation, content views, and details panel
- DMSTreeView.vue: Recursive tree navigation for hierarchical structure
- DMSDocumentViewer.vue: Multi-format document viewer with zoom and controls
- DMSAccessRequestDialog.vue: Access request form with approval workflow
- DMSUploadDialog.vue: File upload interface with metadata assignment
Custom UI Components (Rs Prefix)
- RsButton.vue: Standardized button component with variants
- RsCard.vue: Container component with consistent styling
- RsModal.vue: Modal dialog base component
- RsInput.vue: Standardized input field component
- RsSelect.vue: Standardized dropdown/select component
- RsTextarea.vue: Standardized textarea component
- RsTable.vue: Data table with sorting and filtering
- RsDropdown.vue: Dropdown menu component
Design System
Overview
The EDMS implements a comprehensive design system built on a standardized component library with the "Rs" prefix (Reusable System). This ensures consistency, maintainability, and accessibility across the entire application.
Design Principles
- 🎯 Consistency: All components follow unified design patterns and naming conventions
- 🔧 Modularity: Components are reusable and composable for complex interfaces
- 🌙 Dark Mode: Universal support for both light and dark themes via CSS variables
- 📱 Responsive: Mobile-first approach with adaptive layouts
- ♿ Accessibility: WCAG-compliant with proper ARIA attributes and keyboard navigation
Component Library
Form Components
RsInput.vue
<rs-input
v-model="value"
label="Field Label"
placeholder="Enter text..."
:required="true"
:error="validationError"
size="md"
:disabled="false"
/>
Props:
modelValue
: String/Number - Input valuelabel
: String - Field labelplaceholder
: String - Placeholder texttype
: String - Input type (text, email, password, etc.)required
: Boolean - Required field indicatorerror
: String - Error messagesize
: String - Size variant (sm, md, lg)disabled
: Boolean - Disabled state
RsSelect.vue
<rs-select
v-model="selectedValue"
:options="optionsList"
label="Select Option"
placeholder="Choose..."
:multiple="false"
:required="true"
/>
Props:
modelValue
: String/Number/Array - Selected value(s)options
: Array - Options list (objects with value/label or simple strings)label
: String - Field labelplaceholder
: String - Placeholder textmultiple
: Boolean - Multiple selection supportrequired
: Boolean - Required field indicatorerror
: String - Error messagedisabled
: Boolean - Disabled state
RsTextarea.vue
<rs-textarea
v-model="content"
label="Message"
placeholder="Enter message..."
:rows="4"
resize="vertical"
:required="true"
/>
Props:
modelValue
: String - Textarea contentlabel
: String - Field labelplaceholder
: String - Placeholder textrows
: Number - Number of visible rowsresize
: String - Resize behavior (none, both, horizontal, vertical)required
: Boolean - Required field indicatorerror
: String - Error messagedisabled
: Boolean - Disabled state
UI Components
RsButton.vue
<rs-button
variant="primary"
size="md"
:disabled="false"
@click="handleClick"
>
Button Text
</rs-button>
Variants:
primary
: Blue primary buttonsecondary
: Gray secondary buttoninfo
: Blue info buttonsuccess
: Green success buttonwarning
: Yellow warning buttondanger
: Red danger buttonprimary-outline
: Outlined primary buttonsecondary-outline
: Outlined secondary buttonprimary-text
: Text-only primary button
Sizes:
sm
: Small button (padding: 0.25rem 0.75rem)md
: Medium button (padding: 0.5rem 1rem)lg
: Large button (padding: 0.75rem 1.25rem)
RsCard.vue
<rs-card>
<template #header>
Card Header
</template>
<template #body>
Card content goes here
</template>
<template #footer>
Card footer with actions
</template>
</rs-card>
RsModal.vue
<rs-modal :visible="showModal" @close="closeModal" size="md">
<template #header>
Modal Title
</template>
<template #body>
Modal content
</template>
<template #footer>
<rs-button variant="secondary" @click="closeModal">Cancel</rs-button>
<rs-button variant="primary" @click="confirm">Confirm</rs-button>
</template>
</rs-modal>
Sizes:
sm
: 300px widthmd
: 500px widthlg
: 800px widthxl
: 1000px widthfull
: Full screen width
Color System
The design system uses CSS custom properties for consistent theming:
:root {
/* Primary Colors */
--color-primary: 59 130 246; /* Blue */
--color-secondary: 100 116 139; /* Gray */
--color-info: 14 165 233; /* Sky Blue */
--color-success: 34 197 94; /* Green */
--color-warning: 251 191 36; /* Yellow */
--color-danger: 239 68 68; /* Red */
/* Background Colors */
--bg-1: 248 250 252; /* Light Gray */
--bg-2: 255 255 255; /* White */
/* Text Colors */
--text-color: 15 23 42; /* Slate 900 */
--text-muted: 100 116 139; /* Slate 500 */
/* Border Colors */
--border-color: 226 232 240; /* Slate 200 */
--fk-border-color: 209 213 219; /* Gray 300 */
}
Typography Scale
/* Font Sizes */
.text-xs { font-size: 0.75rem; } /* 12px */
.text-sm { font-size: 0.875rem; } /* 14px */
.text-base { font-size: 1rem; } /* 16px */
.text-lg { font-size: 1.125rem; } /* 18px */
.text-xl { font-size: 1.25rem; } /* 20px */
.text-2xl { font-size: 1.5rem; } /* 24px */
.text-3xl { font-size: 1.875rem; } /* 30px */
/* Font Weights */
.font-light { font-weight: 300; }
.font-normal { font-weight: 400; }
.font-medium { font-weight: 500; }
.font-semibold { font-weight: 600; }
.font-bold { font-weight: 700; }
Spacing Scale
/* Spacing (padding/margin) */
.p-1 { padding: 0.25rem; } /* 4px */
.p-2 { padding: 0.5rem; } /* 8px */
.p-3 { padding: 0.75rem; } /* 12px */
.p-4 { padding: 1rem; } /* 16px */
.p-6 { padding: 1.5rem; } /* 24px */
.p-8 { padding: 2rem; } /* 32px */
Usage Guidelines
✅ Best Practices
- Always use Rs components for UI elements instead of custom styling
- Use semantic variants (primary, secondary, danger) rather than color names
- Follow consistent spacing using the predefined scale
- Use CSS variables for colors instead of hardcoded values
- Test in both themes (light and dark mode)
- Maintain accessibility with proper labels and ARIA attributes
❌ Anti-patterns
- Don't use manual Tailwind classes for buttons (e.g.,
px-4 py-2 bg-blue-500
) - Don't create custom input styling without extending Rs components
- Don't hardcode colors or spacing values
- Don't mix different component styling approaches
- Don't forget dark mode support in custom components
FormKit Integration
The design system integrates with FormKit for advanced form handling:
// FormKit theme configuration
export default {
global: {
label: "formkit-label-global",
outer: "formkit-outer-global",
wrapper: "formkit-wrapper-global",
},
button: {
wrapper: "formkit-wrapper-button",
input: "formkit-input-button",
},
text: {
label: "formkit-outer-text",
inner: "formkit-inner-text",
input: "formkit-input-text",
}
}
Component Registration
All Rs components are globally registered via the component index:
// pages/devtool/code-playground/index.js
export {
RsAlert,
RsBadge,
RsButton,
RsCard,
RsInput,
RsModal,
RsSelect,
RsTextarea,
RsTable,
// ... other components
};
Design System Documentation
Access the interactive design system documentation at /dms/design-system
to:
- View all components with live examples
- Test different variants and states
- Copy code snippets for implementation
- Understand usage guidelines and best practices
Future Enhancements
Planned design system improvements:
- Animation Library: Consistent micro-interactions and transitions
- Icon System: Standardized icon library with consistent sizing
- Layout Components: Grid and flexbox utilities
- Advanced Form Components: Date pickers, multi-select, autocomplete
- Data Visualization: Chart and graph components
- Notification System: Toast and alert components
API & Data Management
Server API Routes
Document Management
// server/api/dms/documents/[id].get.js
export default defineEventHandler(async (event) => {
const id = getRouterParam(event, 'id')
// Fetch document with access control
})
// server/api/dms/upload.post.js
export default defineEventHandler(async (event) => {
// Handle file upload with validation
})
// server/api/dms/search.get.js
export default defineEventHandler(async (event) => {
const query = getQuery(event)
// Perform document search
})
Access Control
// server/api/dms/access-request.post.js
export default defineEventHandler(async (event) => {
// Process access requests
})
// server/api/dms/permissions/[id].get.js
export default defineEventHandler(async (event) => {
// Get user permissions for item
})
Data Validation
Prisma Schema Validation
- Field constraints and types
- Relationship integrity
- Index optimization
Frontend Validation
// FormKit validation rules
const documentValidation = {
title: 'required|length:3,255',
description: 'length:0,1000',
fileType: 'required|in:pdf,doc,docx,xls,xlsx,jpg,png',
accessLevel: 'required|in:public,private,personal'
}
Security & Authentication
Authentication Flow
- User Login: Redirect to Authentik
- Token Exchange: Receive JWT token
- Token Validation: Verify on each request
- Session Management: Store in secure cookie
- Token Refresh: Automatic renewal
Authorization Levels
- System Admin: Full system access
- Department Admin: Department-wide permissions
- Document Owner: Full access to owned documents
- User: Access based on granted permissions
File Security
- Path Traversal Protection: Validate file paths
- File Type Validation: Whitelist allowed formats
- Size Limits: Prevent large file uploads
- Virus Scanning: Optional integration
- Access Logging: Track file access
Data Protection
- SQL Injection Prevention: Parameterized queries via Prisma
- XSS Protection: Input sanitization
- CSRF Protection: Token validation
- Secure Headers: Security-focused HTTP headers
Deployment
Production Build
# Build for production
npm run build
# Generate static files (if needed)
npm run generate
# Preview production build
npm run preview
Environment Configuration
Production Environment Variables
NODE_ENV=production
DATABASE_URL="mysql://prod_user:password@db.server:3306/edms_prod"
NUXT_SECRET_KEY="production-secret-key"
BASE_URL="https://dms.jkr-kotabharu.gov.my"
Docker Deployment
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
EXPOSE 3000
CMD ["npm", "run", "start"]
Nginx Configuration
server {
listen 80;
server_name dms.jkr-kotabharu.gov.my;
location / {
proxy_pass http://localhost:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
location /uploads/ {
alias /var/www/uploads/;
expires 1y;
}
}
Maintenance & Monitoring
Database Maintenance
-- Regular maintenance queries
OPTIMIZE TABLE document;
ANALYZE TABLE access_permission;
-- Cleanup old access requests
DELETE FROM access_request
WHERE status = 'rejected'
AND requested_at < DATE_SUB(NOW(), INTERVAL 30 DAY);
Backup Strategy
- Database Backups: Daily automated backups
- File Storage Backups: Incremental file backups
- Configuration Backups: Version control for configs
Monitoring
- Application Performance: Response times, error rates
- Database Performance: Query performance, connection counts
- File Storage: Disk usage, file integrity
- Security Events: Failed logins, access violations
Logging
// Server-side logging
import { createLogger } from 'winston'
const logger = createLogger({
level: 'info',
format: winston.format.json(),
transports: [
new winston.transports.File({ filename: 'error.log', level: 'error' }),
new winston.transports.File({ filename: 'combined.log' })
]
})
Extension & Customization
Adding New Document Types
- Update Prisma schema with new file types
- Add validation rules for the file type
- Implement viewer component for the format
- Update upload dialog to support the type
Custom Access Control
- Extend AccessPermission model
- Implement custom permission logic
- Update UI to reflect new permissions
- Add admin interface for permission management
Integration Points
- External Authentication: LDAP/Active Directory
- Document Processing: OCR, text extraction
- Workflow Systems: Approval workflows
- Notification Systems: Email/SMS notifications
- Analytics: Document usage analytics
API Extensions
// Custom API endpoint example
// server/api/custom/analytics.get.js
export default defineEventHandler(async (event) => {
// Custom analytics logic
return {
totalDocuments: count,
storageUsed: size,
activeUsers: userCount
}
})
Theme Customization
// tailwind.config.js extensions
module.exports = {
theme: {
extend: {
colors: {
primary: {
50: '#eff6ff',
500: '#3b82f6',
900: '#1e3a8a'
}
}
}
}
}
Document Version: 2.0
Last Updated: December 2024
System Version: EDMS v1.0
Technology Stack: Nuxt.js 3, Vue.js 3, Prisma, TailwindCSS
Database: MySQL/PostgreSQL
Authentication: JWT with optional external providers