Merge branch 'development' of https://git.sena.my/corrad-software/EDMS into development

This commit is contained in:
MuhdAthir 2025-05-31 16:59:08 +08:00
commit 3c5a2e5704
29 changed files with 8161 additions and 2830 deletions

View File

@ -1,204 +0,0 @@
# Changelog
All notable changes to the Electronic Document Management System (EDMS) project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [2.0.0] - 2024-01-15
### 🎨 **Major Design System Overhaul**
#### Added
- **New Standardized Components**
- `RsInput.vue`: Comprehensive input component with validation, sizing, and error states
- `RsSelect.vue`: Dropdown/select component with single/multiple selection support
- `RsTextarea.vue`: Textarea component with configurable resize and validation
- Design system documentation page at `/dms/design-system`
- Interactive component playground with live examples
- Component usage guidelines and best practices
- **Enhanced Design System Features**
- Consistent prop structure across all Rs components (`modelValue`, `label`, `error`, `disabled`, `size`)
- Universal dark mode support via CSS variables
- Accessibility features with proper ARIA attributes
- Standardized sizing system (sm, md, lg)
- Error state handling with validation messaging
- Real-time validation and form state management
#### Changed
- **Settings Architecture Simplification**
- Reduced from 10 to 5 core settings categories:
- 🔐 User & Access Management
- 📁 Document & Folder Settings
- 📝 Metadata & Tagging
- 📤 Upload & Storage Settings
- 📅 System Settings
- Removed categories: Search & Indexing, Integration Settings, Audit & Compliance, Workflow & Automation, UI & Branding
- Streamlined settings structure for better usability and maintenance
- **Component Standardization**
- `DMSAccessRequestDialog.vue`: Replaced manual button styling with `RsButton` components
- Settings page: Updated key form inputs to use `RsInput`, `RsSelect`, `RsTextarea`
- Standardized modal footer patterns across all dialogs
- Consistent form validation and error handling
- **Enhanced Component Registration**
- Added new Rs components to global component registry
- Updated export system in `pages/devtool/code-playground/index.js`
- Improved component discovery and usage
#### Improved
- **Form Consistency**
- Unified form styling across all components
- Consistent error handling and validation states
- Standardized label and placeholder patterns
- Improved accessibility with proper form associations
- **User Experience**
- Better visual consistency across the application
- Improved dark mode support and theme switching
- Enhanced form validation with real-time feedback
- Standardized interaction patterns
### 📖 **Documentation Updates**
#### Added
- **Comprehensive Design System Documentation**
- Interactive component playground at `/dms/design-system`
- Component API documentation with props and examples
- Usage guidelines and best practices
- Color system and typography scale documentation
- Accessibility standards and compliance information
- **Enhanced Technical Guide**
- Complete design system section with component specifications
- Form component usage examples and API reference
- Color system documentation with CSS variable reference
- Typography and spacing scale definitions
- Integration patterns and best practices
#### Updated
- **User Guide Enhancements**
- Added comprehensive Settings Administration section
- Documented simplified 5-category settings structure
- Detailed explanation of each settings category
- Best practices for settings management
- Troubleshooting guide for settings issues
- **README Overhaul**
- Complete project overview with features and technology stack
- Design system highlights and component examples
- Comprehensive setup and installation instructions
- Development guidelines and contribution standards
- Security features and support information
### 🔧 **Technical Improvements**
#### Enhanced
- **CSS Architecture**
- Improved CSS variable system for theming
- Standardized component styling patterns
- Better organization of style files
- Enhanced dark mode support
- **Form Handling**
- Consistent v-model patterns across components
- Improved validation and error state management
- Better form component composition
- Enhanced accessibility features
- **State Management**
- Improved settings state management
- Better validation and error handling
- Enhanced form state synchronization
### 🚨 **Breaking Changes**
- Settings API structure changed due to category reduction
- Some component prop names standardized (may affect custom implementations)
- Manual form styling replaced with Rs components (update required for custom forms)
### 💻 **Development Experience**
#### Improved
- **Component Development**
- Standardized component patterns for easier development
- Better TypeScript support for component props
- Improved component documentation and examples
- Enhanced development guidelines
- **Design Consistency**
- Clear design system guidelines
- Standardized component usage patterns
- Better visual consistency across the application
- Improved maintainability
#### Added
- **Development Tools**
- Interactive design system playground
- Component examples and usage guidelines
- Development best practices documentation
- Contributing guidelines for design system
### 📱 **User Interface**
#### Enhanced
- **Visual Consistency**
- Unified button styles and interactions
- Consistent form field appearance
- Standardized modal and dialog layouts
- Improved color scheme and theming
- **Accessibility**
- Better keyboard navigation support
- Improved screen reader compatibility
- Consistent focus states and indicators
- WCAG 2.1 compliance improvements
- **Responsive Design**
- Better mobile form interactions
- Improved tablet layout handling
- Consistent breakpoint management
- Enhanced touch interaction support
### 🔄 **Migration Guide**
#### For Developers
1. **Update Form Components**: Replace manual form styling with Rs components
2. **Settings Integration**: Update any settings-related code for new 5-category structure
3. **Component Usage**: Follow new Rs component patterns for consistency
4. **Styling Updates**: Remove manual Tailwind classes in favor of Rs components
#### For Users
1. **Settings Location**: Settings now organized in 5 streamlined categories
2. **Interface Changes**: Improved form interactions and validation
3. **New Features**: Access to design system documentation and examples
### 🎯 **Future Roadmap**
- Additional Rs components (date picker, multi-select, autocomplete)
- Enhanced animation system for micro-interactions
- Expanded accessibility features and testing
- Advanced form validation and error handling
- Component testing and documentation automation
---
## [1.0.0] - 2023-12-01
### Initial Release
- Electronic Document Management System with hierarchical organization
- User authentication and role-based access control
- Document upload, preview, and management capabilities
- Basic settings and configuration system
- Initial UI components and styling
---
**Legend:**
- 🎨 Design System
- 📖 Documentation
- 🔧 Technical
- 🚨 Breaking Changes
- 💻 Development Experience
- 📱 User Interface
- 🔄 Migration
- 🎯 Roadmap

253
README.md
View File

@ -1,256 +1,45 @@
# Electronic Document Management System (EDMS)
# Nuxt 3 Minimal Starter
A modern, web-based document management system built with Nuxt.js 3 and Vue.js 3, featuring a comprehensive design system and hierarchical document organization.
Look at the [nuxt 3 documentation](https://v3.nuxtjs.org) to learn more.
## 🚀 Features
## Setup
### Core Functionality
- **Hierarchical Organization**: Cabinet → Drawer → Folder → Subfolder structure
- **Advanced Document Management**: Upload, version control, metadata management
- **Role-Based Access Control**: Granular permissions with access request workflows
- **Multi-format Document Viewer**: Built-in viewer for PDF, images, Office documents
- **Advanced Search**: Full-text search with metadata and tag filtering
- **Responsive Design**: Works seamlessly across desktop, tablet, and mobile
Make sure to install the dependencies:
### Design System
- **Standardized Components**: Complete "Rs" component library (RsButton, RsInput, RsSelect, etc.)
- **Consistent Styling**: Unified design patterns with dark/light mode support
- **Accessibility**: WCAG-compliant components with proper ARIA attributes
- **Interactive Documentation**: Design system playground at `/dms/design-system`
### Administration
- **Streamlined Settings**: 5 core configuration categories
- 🔐 User & Access Management
- 📁 Document & Folder Settings
- 📝 Metadata & Tagging
- 📤 Upload & Storage Settings
- 📅 System Settings
- **Import/Export**: Configuration backup and transfer capabilities
- **Real-time Validation**: Form validation with dependency checking
## 🛠️ Technology Stack
### Frontend
- **Nuxt.js 3**: Universal Vue.js framework with SSR/SPA support
- **Vue.js 3**: Progressive framework with Composition API
- **TailwindCSS**: Utility-first CSS framework with custom component system
- **Pinia**: Modern state management with persistence
- **FormKit**: Advanced form handling with custom theming
### Backend
- **Prisma ORM**: Type-safe database client with migrations
- **MySQL/PostgreSQL**: Relational database with comprehensive schema
- **File System Integration**: Secure file storage and management
- **JWT Authentication**: Token-based authentication with RBAC
### Development Tools
- **TypeScript**: Type safety and enhanced development experience
- **ESLint**: Code linting with Vue.js specific rules
- **Vite**: Lightning-fast build tool and HMR
## 📋 Setup
### Requirements
- Node.js 18+ and npm/yarn/pnpm
- MySQL 8+ or PostgreSQL 13+
- Modern web browser with JavaScript enabled
### Installation
1. **Clone Repository**
```bash
git clone https://github.com/your-repo/edms.git
cd edms
```
2. **Install Dependencies**
```bash
# Using npm
npm install
# Using yarn
# yarn
yarn install
# Using pnpm
pnpm install
# npm
npm install
# pnpm
pnpm install --shamefully-hoist
```
3. **Environment Configuration**
Create `.env` file:
```env
# Database
DATABASE_URL="mysql://username:password@localhost:3306/edms_db"
## Development Server
# Authentication
JWT_SECRET="your-jwt-secret-key-min-256-bits"
SESSION_SECRET="your-session-secret-key"
Start the development server on http://localhost:3000
# File Storage
UPLOAD_PATH="/var/uploads/edms"
MAX_FILE_SIZE="104857600" # 100MB
ALLOWED_FILE_TYPES="pdf,doc,docx,xls,xlsx,ppt,pptx,txt,jpg,jpeg,png"
# Application
NUXT_SECRET_KEY="your-nuxt-app-secret"
BASE_URL="http://localhost:3000"
```
4. **Database Setup**
```bash
# Generate Prisma client
npx prisma generate
# Run migrations
npx prisma db push
# Seed data (optional)
npx prisma db seed
```
5. **Development Server**
```bash
npm run dev
```
Access the application at `http://localhost:3000`
## Production
## 🎨 Design System
### Component Library
The EDMS uses a standardized component library with the "Rs" prefix:
#### Form Components
- **RsInput**: Standardized input fields with validation
- **RsSelect**: Dropdown/select with options support
- **RsTextarea**: Multi-line text input with resize controls
- **RsButton**: Buttons with multiple variants and sizes
#### UI Components
- **RsCard**: Container component with header/body/footer
- **RsModal**: Modal dialogs with standardized structure
- **RsTable**: Data tables with sorting and filtering
- **RsDropdown**: Dropdown menus and navigation
### Usage Examples
```vue
<!-- Input with validation -->
<rs-input
v-model="email"
label="Email Address"
type="email"
:required="true"
:error="emailError"
/>
<!-- Button with variants -->
<rs-button variant="primary" size="md" @click="save">
Save Changes
</rs-button>
<!-- Modal with standardized structure -->
<rs-modal :visible="showModal" @close="closeModal">
<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>
```
### Design Principles
- **🎯 Consistency**: Unified patterns across all components
- **🔧 Modularity**: Reusable and composable components
- **🌙 Dark Mode**: Universal theme support
- **📱 Responsive**: Mobile-first approach
- **♿ Accessibility**: WCAG-compliant with keyboard navigation
## 📖 Documentation
### Available Guides
- **[Technical Guide](docs/Technical_Guide.md)**: Comprehensive technical documentation
- **[User Guide](docs/User_Guide.md)**: End-user instructions and features
- **[Site Settings](docs/SITE_SETTINGS.md)**: Configuration and customization guide
### Interactive Documentation
- **Design System**: Visit `/dms/design-system` for component examples
- **API Documentation**: Available in development mode
- **Component Playground**: Test components with live examples
## 🔧 Development
### Project Structure
```
edms/
├── components/ # Vue components
│ ├── dms/ # DMS-specific components
│ │ ├── dialogs/ # Modal dialogs
│ │ ├── explorer/ # Document browser
│ │ ├── search/ # Search functionality
│ │ └── viewers/ # Document preview
│ └── Rs*.vue # Design system components
├── pages/ # File-based routing
│ ├── dms/ # DMS pages
│ └── devtool/ # Admin tools
├── stores/ # Pinia state management
├── server/ # API routes and middleware
├── prisma/ # Database schema and migrations
├── assets/ # Stylesheets and assets
└── docs/ # Documentation
```
### Building for Production
Build the application for production:
```bash
# Build application
npm run build
# Preview production build
npm run preview
# Generate static site (if applicable)
npm run generate
```
## 🔐 Security Features
Locally preview production build:
- **Role-Based Access Control**: Granular permission system
- **Document-level Security**: Individual document access controls
- **Access Request Workflow**: Approval system for restricted documents
- **Audit Trail**: Comprehensive activity logging
- **File Type Validation**: Security through file type restrictions
- **Session Management**: Secure token-based authentication
```bash
npm run preview
```
## 🤝 Contributing
Checkout the [deployment documentation](https://v3.nuxtjs.org/guide/deploy/presets) for more information.
# corradAF
1. Fork the repository
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
3. Follow the design system guidelines
4. Use standardized components (Rs library)
5. Add tests for new functionality
6. Commit changes (`git commit -m 'Add amazing feature'`)
7. Push to branch (`git push origin feature/amazing-feature`)
8. Open a Pull Request
### Development Guidelines
- Use Rs components instead of custom styling
- Follow semantic variant naming (primary, secondary, danger)
- Test in both light and dark modes
- Maintain accessibility standards
- Document new features in user/technical guides
## 📄 License
This project is part of the corradAF base project. See the license file for details.
## 🆘 Support
- **Technical Issues**: Check the [Technical Guide](docs/Technical_Guide.md)
- **User Questions**: Refer to the [User Guide](docs/User_Guide.md)
- **Component Usage**: Visit `/dms/design-system` for examples
- **Bug Reports**: Create an issue with detailed reproduction steps
---
Built with ❤️ using Nuxt.js 3, Vue.js 3, and modern web technologies.
This is the base project for corradAF.

View File

@ -1,115 +0,0 @@
<script setup>
const props = defineProps({
modelValue: {
type: [String, Number],
default: ''
},
type: {
type: String,
default: 'text'
},
placeholder: {
type: String,
default: ''
},
disabled: {
type: Boolean,
default: false
},
required: {
type: Boolean,
default: false
},
error: {
type: String,
default: ''
},
label: {
type: String,
default: ''
},
size: {
type: String,
default: 'md'
}
});
const emit = defineEmits(['update:modelValue']);
const updateValue = (event) => {
emit('update:modelValue', event.target.value);
};
</script>
<template>
<div class="rs-input-wrapper">
<label v-if="label" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{{ label }}
<span v-if="required" class="text-red-500">*</span>
</label>
<input
:value="modelValue"
@input="updateValue"
:type="type"
:placeholder="placeholder"
:disabled="disabled"
:required="required"
class="rs-input"
:class="{
'rs-input-sm': size === 'sm',
'rs-input-md': size === 'md',
'rs-input-lg': size === 'lg',
'rs-input-error': error,
'rs-input-disabled': disabled
}"
/>
<div v-if="error" class="rs-input-error-message">
{{ error }}
</div>
</div>
</template>
<style scoped>
.rs-input-wrapper {
@apply w-full;
}
.rs-input {
@apply w-full px-3 py-2 border rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 placeholder-gray-500 dark:placeholder-gray-400 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors;
border-color: rgb(var(--fk-border-color));
}
.rs-input:hover {
@apply border-gray-400 dark:border-gray-500;
}
.rs-input:focus {
@apply outline-none ring-2 ring-blue-500 border-blue-500;
}
.rs-input-sm {
@apply px-2 py-1 text-sm;
}
.rs-input-md {
@apply px-3 py-2 text-sm;
}
.rs-input-lg {
@apply px-4 py-3 text-base;
}
.rs-input-error {
@apply border-red-500 dark:border-red-500 focus:ring-red-500 focus:border-red-500;
}
.rs-input-disabled {
@apply bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400 cursor-not-allowed;
}
.rs-input-error-message {
@apply text-sm text-red-500 mt-1;
}
</style>

View File

@ -1,143 +0,0 @@
<script setup>
const props = defineProps({
modelValue: {
type: [String, Number, Array],
default: ''
},
options: {
type: Array,
required: true
},
placeholder: {
type: String,
default: 'Select an option'
},
disabled: {
type: Boolean,
default: false
},
required: {
type: Boolean,
default: false
},
error: {
type: String,
default: ''
},
label: {
type: String,
default: ''
},
size: {
type: String,
default: 'md'
},
multiple: {
type: Boolean,
default: false
}
});
const emit = defineEmits(['update:modelValue']);
const updateValue = (event) => {
emit('update:modelValue', event.target.value);
};
</script>
<template>
<div class="rs-select-wrapper">
<label v-if="label" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{{ label }}
<span v-if="required" class="text-red-500">*</span>
</label>
<select
:value="modelValue"
@change="updateValue"
:disabled="disabled"
:required="required"
:multiple="multiple"
class="rs-select"
:class="{
'rs-select-sm': size === 'sm',
'rs-select-md': size === 'md',
'rs-select-lg': size === 'lg',
'rs-select-error': error,
'rs-select-disabled': disabled
}"
>
<option v-if="!multiple && placeholder" value="" disabled>
{{ placeholder }}
</option>
<option
v-for="option in options"
:key="option.value || option"
:value="option.value || option"
>
{{ option.label || option }}
</option>
</select>
<div v-if="error" class="rs-select-error-message">
{{ error }}
</div>
</div>
</template>
<style scoped>
.rs-select-wrapper {
@apply w-full;
}
.rs-select {
@apply w-full px-3 py-2 border rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors appearance-none;
border-color: rgb(var(--fk-border-color));
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3e%3c/svg%3e");
background-position: right 0.5rem center;
background-repeat: no-repeat;
background-size: 1.5em 1.5em;
padding-right: 2.5rem;
}
.rs-select:hover {
@apply border-gray-400 dark:border-gray-500;
}
.rs-select:focus {
@apply outline-none ring-2 ring-blue-500 border-blue-500;
}
.rs-select-sm {
@apply px-2 py-1 text-sm;
padding-right: 2rem;
}
.rs-select-md {
@apply px-3 py-2 text-sm;
padding-right: 2.5rem;
}
.rs-select-lg {
@apply px-4 py-3 text-base;
padding-right: 3rem;
}
.rs-select-error {
@apply border-red-500 dark:border-red-500 focus:ring-red-500 focus:border-red-500;
}
.rs-select-disabled {
@apply bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400 cursor-not-allowed;
}
.rs-select-error-message {
@apply text-sm text-red-500 mt-1;
}
.rs-select[multiple] {
background-image: none;
padding-right: 0.75rem;
min-height: 6rem;
}
</style>

View File

@ -1,105 +0,0 @@
<script setup>
const props = defineProps({
modelValue: {
type: String,
default: ''
},
placeholder: {
type: String,
default: ''
},
disabled: {
type: Boolean,
default: false
},
required: {
type: Boolean,
default: false
},
error: {
type: String,
default: ''
},
label: {
type: String,
default: ''
},
rows: {
type: Number,
default: 4
},
resize: {
type: String,
default: 'vertical' // none, both, horizontal, vertical
}
});
const emit = defineEmits(['update:modelValue']);
const updateValue = (event) => {
emit('update:modelValue', event.target.value);
};
</script>
<template>
<div class="rs-textarea-wrapper">
<label v-if="label" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{{ label }}
<span v-if="required" class="text-red-500">*</span>
</label>
<textarea
:value="modelValue"
@input="updateValue"
:placeholder="placeholder"
:disabled="disabled"
:required="required"
:rows="rows"
class="rs-textarea"
:class="{
'rs-textarea-error': error,
'rs-textarea-disabled': disabled,
'resize-none': resize === 'none',
'resize-both': resize === 'both',
'resize-x': resize === 'horizontal',
'resize-y': resize === 'vertical'
}"
></textarea>
<div v-if="error" class="rs-textarea-error-message">
{{ error }}
</div>
</div>
</template>
<style scoped>
.rs-textarea-wrapper {
@apply w-full;
}
.rs-textarea {
@apply w-full px-3 py-2 border rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 placeholder-gray-500 dark:placeholder-gray-400 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors;
border-color: rgb(var(--fk-border-color));
min-height: 2.5rem;
}
.rs-textarea:hover {
@apply border-gray-400 dark:border-gray-500;
}
.rs-textarea:focus {
@apply outline-none ring-2 ring-blue-500 border-blue-500;
}
.rs-textarea-error {
@apply border-red-500 dark:border-red-500 focus:ring-red-500 focus:border-red-500;
}
.rs-textarea-disabled {
@apply bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400 cursor-not-allowed;
}
.rs-textarea-error-message {
@apply text-sm text-red-500 mt-1;
}
</style>

View File

@ -0,0 +1,296 @@
<script setup>
import { ref, computed } from 'vue';
const props = defineProps({
request: {
type: Object,
required: true
},
visible: {
type: Boolean,
default: false
}
});
const emit = defineEmits(['close', 'submit']);
// Form state
const approvalNotes = ref('');
const isSubmitting = ref(false);
const formError = ref('');
// Computed properties
const documentName = computed(() => {
return props.request?.documentName || 'Document';
});
const documentPath = computed(() => {
return props.request?.documentPath || '';
});
const requesterInfo = computed(() => {
return {
name: props.request?.requesterName || 'Unknown',
department: props.request?.requesterDepartment || '',
email: props.request?.requesterEmail || '',
justification: props.request?.justification || 'No justification provided'
};
});
const requestType = computed(() => {
return props.request?.accessType || 'view';
});
const requestDate = computed(() => {
if (!props.request?.requestDate) return '';
const date = new Date(props.request.requestDate);
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
});
const accessDuration = computed(() => {
return props.request?.accessDuration || '7 days';
});
// Methods
const closeDialog = () => {
// Reset form
approvalNotes.value = '';
formError.value = '';
emit('close');
};
const validateForm = () => {
if (approvalNotes.value.trim().length < 3) {
formError.value = 'Please provide notes for your decision';
return false;
}
return true;
};
const approveRequest = async () => {
if (!validateForm()) return;
isSubmitting.value = true;
formError.value = '';
try {
emit('submit', {
action: 'approve',
notes: approvalNotes.value
});
} catch (error) {
formError.value = 'Failed to approve request. Please try again.';
console.error('Approval error:', error);
isSubmitting.value = false;
}
};
const rejectRequest = async () => {
if (!validateForm()) return;
isSubmitting.value = true;
formError.value = '';
try {
emit('submit', {
action: 'reject',
notes: approvalNotes.value
});
} catch (error) {
formError.value = 'Failed to reject request. Please try again.';
console.error('Rejection error:', error);
isSubmitting.value = false;
}
};
// Format the access type label
const getAccessTypeLabel = (type) => {
const types = {
'view': 'View Only',
'download': 'Download',
'print': 'Print',
'full': 'Full Access'
};
return types[type] || type;
};
// Format the access type description
const getAccessTypeDescription = (type) => {
const descriptions = {
'view': 'Can only view the document',
'download': 'Can view and download',
'print': 'Can view and print',
'full': 'View, download and print'
};
return descriptions[type] || '';
};
</script>
<template>
<rs-modal
:visible="visible"
@close="closeDialog"
title="Review Access Request"
size="lg"
>
<template #body>
<div class="p-6">
<!-- Form error message -->
<div v-if="formError" class="mb-4 p-3 bg-red-50 border border-red-200 text-red-800 rounded-md text-sm">
{{ formError }}
</div>
<!-- Two-column layout for request details -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Left column - Document details -->
<div>
<h3 class="text-sm font-medium text-gray-900 dark:text-gray-100 mb-3">Document Information</h3>
<div class="bg-blue-50 dark:bg-blue-900/10 rounded-lg p-4 border border-blue-200 dark:border-blue-800 mb-4">
<div class="flex items-start mb-3">
<span class="flex-shrink-0 h-9 w-9 rounded-md bg-blue-100 dark:bg-blue-900/30 flex items-center justify-center text-blue-600 dark:text-blue-400 mr-3">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
</span>
<div>
<div class="text-sm font-medium text-blue-900 dark:text-blue-100">{{ documentName }}</div>
<div class="text-xs text-blue-800 dark:text-blue-200">{{ documentPath }}</div>
</div>
</div>
<div class="grid grid-cols-2 gap-2 text-sm">
<div class="text-gray-700 dark:text-gray-300 font-medium">Access Type:</div>
<div class="text-gray-900 dark:text-gray-100">{{ getAccessTypeLabel(requestType) }}</div>
<div class="text-gray-700 dark:text-gray-300 font-medium">Access Duration:</div>
<div class="text-gray-900 dark:text-gray-100">{{ accessDuration }}</div>
<div class="text-gray-700 dark:text-gray-300 font-medium">Requested:</div>
<div class="text-gray-900 dark:text-gray-100">{{ requestDate }}</div>
</div>
</div>
<h3 class="text-sm font-medium text-gray-900 dark:text-gray-100 mb-3">Access Type Details</h3>
<div class="bg-indigo-50 dark:bg-indigo-900/10 rounded-lg p-4 border border-indigo-200 dark:border-indigo-800">
<div class="flex items-center mb-2">
<div class="w-8 h-8 rounded-full bg-indigo-100 dark:bg-indigo-900/30 flex items-center justify-center mr-3">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-indigo-600 dark:text-indigo-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" />
</svg>
</div>
<div class="text-sm font-medium text-indigo-900 dark:text-indigo-100">
{{ getAccessTypeLabel(requestType) }}
</div>
</div>
<p class="text-sm text-indigo-800 dark:text-indigo-200 ml-11">
{{ getAccessTypeDescription(requestType) }}
</p>
</div>
</div>
<!-- Right column - Requester details and justification -->
<div>
<h3 class="text-sm font-medium text-gray-900 dark:text-gray-100 mb-3">Requester Information</h3>
<div class="bg-purple-50 dark:bg-purple-900/10 rounded-lg p-4 border border-purple-200 dark:border-purple-800 mb-4">
<div class="flex items-start mb-3">
<span class="flex-shrink-0 h-9 w-9 rounded-full bg-purple-100 dark:bg-purple-900/30 flex items-center justify-center text-purple-600 dark:text-purple-400 mr-3">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
</span>
<div>
<div class="text-sm font-medium text-purple-900 dark:text-purple-100">{{ requesterInfo.name }}</div>
<div class="text-xs text-purple-800 dark:text-purple-200">
{{ requesterInfo.department }}
<span v-if="requesterInfo.email"> {{ requesterInfo.email }}</span>
</div>
</div>
</div>
</div>
<h3 class="text-sm font-medium text-gray-900 dark:text-gray-100 mb-3">Request Justification</h3>
<div class="bg-green-50 dark:bg-green-900/10 rounded-lg p-4 border border-green-200 dark:border-green-800">
<div class="flex items-center mb-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-green-600 dark:text-green-400 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 8h10M7 12h4m1 8l-4-4H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-3l-4 4z" />
</svg>
<div class="text-sm font-medium text-green-900 dark:text-green-100">Justification</div>
</div>
<p class="text-sm text-green-800 dark:text-green-200 whitespace-pre-line">
{{ requesterInfo.justification }}
</p>
</div>
</div>
</div>
<!-- Approval notes -->
<div class="mt-6">
<h3 class="text-sm font-medium text-gray-900 dark:text-gray-100 mb-3">Decision Notes <span class="text-red-500">*</span></h3>
<div class="bg-gray-50 dark:bg-gray-800 rounded-lg p-4 border border-gray-200 dark:border-gray-700">
<textarea
v-model="approvalNotes"
rows="3"
placeholder="Enter your approval or rejection notes here..."
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 placeholder-gray-500 dark:placeholder-gray-400 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 resize-none"
></textarea>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-2">
Please provide a reason for your decision. This will be visible to the requester.
</p>
</div>
</div>
</div>
</template>
<template #footer>
<div class="flex justify-between gap-3 px-6 py-4 bg-gray-50 dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700">
<div>
<rs-button
@click="closeDialog"
:disabled="isSubmitting"
variant="secondary-outline"
size="sm"
>
Cancel
</rs-button>
</div>
<div class="flex gap-3">
<rs-button
@click="rejectRequest"
:disabled="isSubmitting"
variant="danger-outline"
size="sm"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
Reject
</rs-button>
<rs-button
@click="approveRequest"
:disabled="isSubmitting"
variant="success"
size="sm"
>
<svg v-if="isSubmitting" class="animate-spin -ml-1 mr-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<svg v-else xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
Approve
</rs-button>
</div>
</div>
</template>
</rs-modal>
</template>

View File

@ -1,5 +1,5 @@
<script setup>
import { ref, computed } from 'vue';
import { ref, computed, watch } from 'vue';
import { useDmsStore } from '~/stores/dms';
const props = defineProps({
@ -19,210 +19,273 @@ const emit = defineEmits(['close', 'submit']);
const dmsStore = useDmsStore();
// Form state
const selectedAccessType = ref('view');
const accessDuration = ref('7 days');
const accessType = ref('view');
const justification = ref('');
const accessDuration = ref('7 days');
const isSubmitting = ref(false);
const formError = ref('');
// Access type options
const accessTypes = [
{ id: 'view', label: 'View Only', description: 'Can only view the document' },
{ id: 'download', label: 'Download', description: 'Can view and download' },
{ id: 'print', label: 'Print', description: 'Can view and print' },
{ id: 'full', label: 'Full Access', description: 'View, download and print' }
// Durations
const durationOptions = [
{ value: '1 day', label: '1 Day' },
{ value: '7 days', label: '7 Days' },
{ value: '14 days', label: '14 Days' },
{ value: '30 days', label: '30 Days' },
{ value: '90 days', label: '90 Days' }
];
// Access duration options
const durationOptions = [
'7 days',
'14 days',
'30 days',
'60 days',
'90 days',
'Permanent'
// Access type options
const accessTypeOptions = [
{
value: 'view',
label: 'View Only',
description: 'View documents without the ability to download or edit',
icon: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle></svg>`
},
{
value: 'download',
label: 'Download',
description: 'View and download documents without editing',
icon: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>`
},
{
value: 'full',
label: 'Full Access',
description: 'View, download, and edit documents',
icon: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><circle cx="12" cy="12" r="3"></circle></svg>`
}
];
// Computed properties
const itemTitle = computed(() => {
const itemName = computed(() => {
return props.item?.name || 'Document';
});
const itemFileName = computed(() => {
if (props.item?.type === 'file') {
return props.item.name;
const itemPath = computed(() => {
if (!props.item) return '';
if (props.item.path) return props.item.path;
return props.item.parentPath ? `${props.item.parentPath}/${props.item.name}` : `/${props.item.name}`;
});
const itemType = computed(() => {
if (!props.item) return 'document';
if (props.item.type === 'cabinet' || props.item.type === 'cabinet-group') {
return 'cabinet';
} else if (props.item.type === 'drawer') {
return 'drawer';
} else if (props.item.type === 'folder' || props.item.type === 'subfolder') {
return 'folder';
} else {
return 'document';
}
return '';
});
const isValidForm = computed(() => {
return accessType.value && justification.value.length >= 10 && accessDuration.value;
});
// Methods
const closeDialog = () => {
// Reset form
selectedAccessType.value = 'view';
accessDuration.value = '7 days';
const resetForm = () => {
accessType.value = 'view';
justification.value = '';
accessDuration.value = '7 days';
formError.value = '';
emit('close');
};
const submitRequest = async () => {
// Validate form
if (!selectedAccessType.value) {
formError.value = 'Please select an access type';
return;
}
if (!justification.value.trim()) {
formError.value = 'Please provide a justification for your request';
const handleClose = () => {
emit('close');
// Reset form after closing
setTimeout(() => {
resetForm();
}, 300);
};
const handleSubmit = () => {
if (!isValidForm.value) {
formError.value = 'Please complete all required fields';
return;
}
isSubmitting.value = true;
formError.value = '';
try {
// Submit the request to the store
const request = await dmsStore.requestAccess(
props.item.id,
selectedAccessType.value,
justification.value,
accessDuration.value
);
// Emit success event
emit('submit', request);
// Close the dialog (this will also reset the form)
closeDialog();
emit('submit', {
accessType: accessType.value,
justification: justification.value,
accessDuration: accessDuration.value
});
} catch (error) {
formError.value = 'Failed to submit access request. Please try again.';
console.error('Access request error:', error);
console.error('Submit error:', error);
formError.value = error.message || 'Failed to submit request';
} finally {
isSubmitting.value = false;
}
};
// Watch for visibility changes
watch(() => props.visible, (newValue) => {
if (newValue) {
resetForm();
}
});
</script>
<template>
<rs-modal
:visible="visible"
@close="closeDialog"
title="Request Document Access"
<rs-modal
v-model="props.visible"
size="md"
@close="handleClose"
>
<template #header>
<div class="flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2 text-blue-600" viewBox="0 0 20 20" fill="currentColor">
<path d="M10 12a2 2 0 100-4 2 2 0 000 4z" />
<path fill-rule="evenodd" d="M.458 10C1.732 5.943 5.522 3 10 3s8.268 2.943 9.542 7c-1.274 4.057-5.064 7-9.542 7S1.732 14.057.458 10zM14 10a4 4 0 11-8 0 4 4 0 018 0z" clip-rule="evenodd" />
</svg>
<span>Request Access</span>
</div>
</template>
<template #body>
<div class="p-6">
<!-- Document Information Section -->
<div class="mb-6">
<div class="bg-blue-50 dark:bg-blue-900/10 rounded-lg p-4 border border-blue-200 dark:border-blue-800">
<h3 class="text-sm font-medium text-blue-900 dark:text-blue-100 mb-2">Document Information</h3>
<div class="space-y-1">
<div class="flex">
<span class="text-sm font-medium text-gray-700 dark:text-gray-300 w-12">Title:</span>
<span class="text-sm text-gray-900 dark:text-gray-100">{{ itemTitle }}</span>
</div>
<div class="flex">
<span class="text-sm font-medium text-gray-700 dark:text-gray-300 w-12">File:</span>
<span class="text-sm text-gray-600 dark:text-gray-400">{{ itemFileName }}</span>
</div>
<div class="p-2">
<!-- Item information -->
<div class="mb-6 bg-gray-50 dark:bg-gray-800 p-4 rounded-lg">
<h3 class="text-gray-700 dark:text-gray-300 text-sm font-medium mb-2">Requesting access to:</h3>
<div class="flex items-start">
<div class="mt-1 mr-3">
<span v-if="itemType === 'cabinet'" class="text-blue-600 dark:text-blue-400">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path d="M2 6a2 2 0 012-2h5l2 2h5a2 2 0 012 2v6a2 2 0 01-2 2H4a2 2 0 01-2-2V6z" />
</svg>
</span>
<span v-else-if="itemType === 'drawer'" class="text-blue-600 dark:text-blue-400">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path d="M2 6a2 2 0 012-2h5l2 2h5a2 2 0 012 2v6a2 2 0 01-2 2H4a2 2 0 01-2-2V6z" />
</svg>
</span>
<span v-else-if="itemType === 'folder'" class="text-blue-600 dark:text-blue-400">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path d="M2 6a2 2 0 012-2h5l2 2h5a2 2 0 012 2v6a2 2 0 01-2 2H4a2 2 0 01-2-2V6z" />
</svg>
</span>
<span v-else class="text-gray-600 dark:text-gray-400">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4z" clip-rule="evenodd" />
</svg>
</span>
</div>
<div>
<h4 class="text-gray-900 dark:text-gray-100 font-medium">{{ itemName }}</h4>
<p class="text-gray-500 dark:text-gray-400 text-xs mt-1">{{ itemPath }}</p>
</div>
</div>
</div>
<!-- Form error -->
<div v-if="formError" class="mb-4 p-3 bg-red-50 border border-red-200 text-red-800 rounded-md text-sm">
<div v-if="formError" class="mb-4 p-3 bg-red-50 dark:bg-red-900/20 border-l-4 border-red-500 text-red-700 dark:text-red-400 text-sm">
{{ formError }}
</div>
<!-- Access Type Section -->
<!-- Access type -->
<div class="mb-6">
<h3 class="text-sm font-medium text-gray-900 dark:text-gray-100 mb-3">Access Type</h3>
<div class="grid grid-cols-2 gap-3">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Access Type
</label>
<div class="space-y-3">
<div
v-for="accessType in accessTypes"
:key="accessType.id"
class="relative"
v-for="option in accessTypeOptions"
:key="option.value"
class="relative flex items-start"
>
<label
:for="accessType.id"
class="flex items-start p-3 border rounded-lg cursor-pointer transition-colors"
:class="selectedAccessType === accessType.id
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20'
: 'border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-800'"
>
<div class="flex items-center h-5">
<input
:id="accessType.id"
v-model="selectedAccessType"
:value="accessType.id"
type="radio"
class="w-4 h-4 text-blue-600 border-gray-300 focus:ring-blue-500"
/>
<div class="flex items-center h-5">
<input
:id="`access-type-${option.value}`"
type="radio"
v-model="accessType"
:value="option.value"
class="focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300 dark:border-gray-600 dark:bg-gray-800"
/>
</div>
<div class="ml-3 flex items-center">
<span v-html="option.icon" class="mr-2 text-gray-600 dark:text-gray-400"></span>
<div>
<label
:for="`access-type-${option.value}`"
class="font-medium text-gray-700 dark:text-gray-300 cursor-pointer"
>
{{ option.label }}
</label>
<p class="text-gray-500 dark:text-gray-400 text-xs">{{ option.description }}</p>
</div>
<div class="ml-3">
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">
{{ accessType.label }}
</div>
<div class="text-xs text-gray-500 dark:text-gray-400 mt-1">
{{ accessType.description }}
</div>
</div>
</label>
</div>
</div>
</div>
</div>
<!-- Access Duration Section -->
<!-- Justification -->
<div class="mb-6">
<rs-select
v-model="accessDuration"
:options="durationOptions"
label="Access Duration"
:required="false"
/>
</div>
<!-- Justification Section -->
<div class="mb-6">
<rs-textarea
<label for="justification" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Justification
<span class="text-red-500">*</span>
</label>
<textarea
id="justification"
v-model="justification"
label="Justification"
placeholder="Please explain why you need access to this document..."
:rows="4"
:required="true"
resize="none"
/>
rows="3"
class="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 dark:border-gray-600 dark:bg-gray-800 rounded-md"
placeholder="Please explain why you need access to this resource..."
></textarea>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
Minimum 10 characters. Please provide a detailed explanation.
</p>
</div>
<!-- Footer Note -->
<p class="text-xs text-gray-500 dark:text-gray-400 mb-6">
Your request will be reviewed by the document owner or administrator.
</p>
<!-- Access Duration -->
<div class="mb-6">
<label for="duration" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Access Duration
</label>
<select
id="duration"
v-model="accessDuration"
class="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 dark:border-gray-600 dark:bg-gray-800 rounded-md"
>
<option v-for="option in durationOptions" :key="option.value" :value="option.value">
{{ option.label }}
</option>
</select>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
How long do you need access to this resource?
</p>
</div>
</div>
</template>
<template #footer>
<div class="flex justify-end gap-3 px-6 py-4 bg-gray-50 dark:bg-gray-800">
<rs-button
@click="closeDialog"
:disabled="isSubmitting"
variant="secondary"
>
Cancel
</rs-button>
<rs-button
@click="submitRequest"
:disabled="isSubmitting"
variant="primary"
class="flex items-center"
>
<svg v-if="isSubmitting" class="animate-spin -ml-1 mr-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Submit Request
</rs-button>
<div class="flex justify-between items-center">
<div>
<p class="text-xs text-gray-500 dark:text-gray-400">
* Required fields
</p>
</div>
<div class="flex space-x-2">
<rs-button
variant="light"
@click="handleClose"
:disabled="isSubmitting"
>
Cancel
</rs-button>
<rs-button
variant="primary"
@click="handleSubmit"
:disabled="!isValidForm || isSubmitting"
:loading="isSubmitting"
>
Submit Request
</rs-button>
</div>
</div>
</template>
</rs-modal>

View File

@ -275,9 +275,9 @@ const getFieldComponent = (fieldType) => {
<div v-else>
<p class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">
Drag and drop files here, or
<button @click="openFileDialog" class="text-blue-600 hover:text-blue-500 dark:text-blue-400 dark:hover:text-blue-300">
<rs-button @click="openFileDialog" variant="primary-text" size="sm">
browse
</button>
</rs-button>
</p>
<p class="text-sm text-gray-500">
Supported: {{ dmsStore.systemSettings.upload.allowedFileTypes.join(', ').toUpperCase() }}
@ -310,12 +310,13 @@ const getFieldComponent = (fieldType) => {
<p class="text-sm text-gray-500">{{ formatFileSize(file.size) }}</p>
</div>
</div>
<button @click="removeFile(index)"
class="text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300">
<rs-button @click="removeFile(index)"
variant="danger-text"
size="sm">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18 6L6 18M6 6L18 18" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
</button>
</rs-button>
</div>
<!-- Validation Errors -->

View File

@ -5,6 +5,7 @@ import DMSDocumentViewer from '~/components/dms/viewers/DMSDocumentViewer.vue';
import DMSUploadDialog from '~/components/dms/dialogs/DMSUploadDialog.vue';
import DMSCreateNewDialog from '~/components/dms/dialogs/DMSCreateNewDialog.vue';
import DMSAccessRequestDialog from '~/components/dms/dialogs/DMSAccessRequestDialog.vue';
import { useDmsStore } from '~/stores/dms';
// Props for the explorer
const props = defineProps({
@ -36,6 +37,9 @@ const sortOrder = ref('asc'); // asc, desc
const searchQuery = ref('');
const showAccessRequestDialog = ref(false);
const accessRequestItem = ref(null);
const isRequestingAccess = ref(false);
const requestSuccess = ref(false);
const requestError = ref(null);
// Navigation history for back/forward functionality
const navigationHistory = ref([props.initialPath]);
@ -52,6 +56,7 @@ const currentDocument = ref(null);
// Mock data - replace with actual API calls
const allItems = ref([]);
const router = useRouter();
const dmsStore = useDmsStore();
// Methods
const loadItems = async () => {
@ -600,6 +605,39 @@ const closeAccessRequestDialog = () => {
accessRequestItem.value = null;
};
const handleAccessRequest = async (item) => {
if (!item) return;
accessRequestItem.value = item;
showAccessRequestDialog.value = true;
};
const submitAccessRequest = async (requestData) => {
isRequestingAccess.value = true;
requestError.value = null;
try {
await dmsStore.requestAccess(
accessRequestItem.value.id,
requestData.accessType,
requestData.justification,
requestData.accessDuration
);
requestSuccess.value = true;
setTimeout(() => {
showAccessRequestDialog.value = false;
requestSuccess.value = false;
accessRequestItem.value = null;
}, 2000);
} catch (error) {
requestError.value = error.message || 'Failed to submit access request. Please try again.';
console.error('Access request error:', error);
} finally {
isRequestingAccess.value = false;
}
};
// Computed properties
const currentItems = computed(() => {
let items = allItems.value.filter(item => item.parentPath === currentPath.value);
@ -1119,6 +1157,33 @@ const parentType = computed(() => {
<span v-if="item.accessType === 'private' && !checkItemAccess(item)"
v-html="getSvgIcon('lock', 16)"
class="absolute top-2 right-2 text-red-500 bg-white dark:bg-gray-800 rounded-full p-1"></span>
<!-- Locked Item Overlay -->
<div
v-if="item.isLocked || (item.accessType === 'private' && !item.hasAccess)"
class="absolute inset-0 flex items-center justify-center bg-black bg-opacity-60 rounded-md z-10"
>
<div class="text-center p-2">
<div class="bg-red-500 rounded-full p-2 inline-flex mb-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-white" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clip-rule="evenodd" />
</svg>
</div>
<p class="text-white text-xs mb-2">Restricted Access</p>
<button
v-if="!item.accessRequestStatus || item.accessRequestStatus === 'rejected'"
@click.stop="handleAccessRequest(item)"
class="px-2 py-1 bg-blue-600 hover:bg-blue-700 text-white text-xs rounded"
>
Request Access
</button>
<div
v-else-if="item.accessRequestStatus === 'pending'"
class="px-2 py-1 bg-yellow-500 text-white text-xs rounded"
>
Request Pending
</div>
</div>
</div>
</div>
</div>
@ -1345,21 +1410,60 @@ const parentType = computed(() => {
/>
<!-- Create New Dialog -->
<DMSCreateNewDialog
v-model:visible="showCreateDialog"
:current-path="currentPath"
:parent-type="parentType"
@create="handleCreateNew"
<DMSCreateNewDialog
v-if="showCreateDialog"
@close="showCreateDialog = false"
@create="handleCreateNew"
/>
<!-- Access Request Dialog -->
<DMSAccessRequestDialog
:visible="showAccessRequestDialog"
<DMSAccessRequestDialog
v-if="accessRequestItem"
:item="accessRequestItem"
@submit="handleAccessRequestSubmitted"
@close="closeAccessRequestDialog"
:visible="showAccessRequestDialog"
@close="showAccessRequestDialog = false"
@submit="submitAccessRequest"
/>
<!-- Success Notification -->
<div
v-if="requestSuccess"
class="fixed bottom-4 right-4 bg-green-100 border-l-4 border-green-500 text-green-700 p-4 rounded shadow-lg z-50"
>
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-green-500" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
</svg>
</div>
<div class="ml-3">
<p class="text-sm">Access request submitted successfully!</p>
</div>
</div>
</div>
<!-- Error Notification -->
<div
v-if="requestError"
class="fixed bottom-4 right-4 bg-red-100 border-l-4 border-red-500 text-red-700 p-4 rounded shadow-lg z-50"
>
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-red-500" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
</svg>
</div>
<div class="ml-3">
<p class="text-sm">{{ requestError }}</p>
<button
@click="requestError = null"
class="text-xs text-red-600 font-medium hover:text-red-500 mt-1"
>
Dismiss
</button>
</div>
</div>
</div>
</div>
</template>

View File

@ -0,0 +1,435 @@
<script setup>
import { ref, computed, onMounted } from 'vue';
import { useDmsStore } from '~/stores/dms';
// Store
const dmsStore = useDmsStore();
// Props
const props = defineProps({
userId: {
type: String,
default: null
},
timeRange: {
type: String,
default: '30days' // 7days, 30days, 90days, 365days
},
showPersonal: {
type: Boolean,
default: true
},
showDepartmental: {
type: Boolean,
default: true
}
});
// Component state
const isLoading = ref(true);
const accessRequests = ref([]);
const departmentRequests = ref([]);
const selectedTimeRange = ref(props.timeRange);
// Time ranges
const timeRangeOptions = [
{ id: '7days', label: 'Last 7 Days' },
{ id: '30days', label: 'Last 30 Days' },
{ id: '90days', label: 'Last 90 Days' },
{ id: '365days', label: 'Last Year' }
];
// Methods
const loadRequestData = async () => {
isLoading.value = true;
try {
// In a real implementation, this would come from an API
const [userRequests, deptRequests] = await Promise.all([
dmsStore.getAccessRequests(props.userId),
dmsStore.getDepartmentAccessRequests()
]);
accessRequests.value = userRequests;
departmentRequests.value = deptRequests;
} catch (error) {
console.error('Failed to load access request data:', error);
} finally {
isLoading.value = false;
}
};
// Update time range
const updateTimeRange = (range) => {
selectedTimeRange.value = range;
loadRequestData();
};
// Format time duration in hours
const formatDuration = (hours) => {
if (hours < 1) {
return `${Math.round(hours * 60)} minutes`;
} else if (hours < 24) {
return `${Math.round(hours)} hours`;
} else {
return `${Math.round(hours / 24)} days`;
}
};
// Format percentage
const formatPercentage = (value) => {
return `${Math.round(value * 100)}%`;
};
// Date formatters
const formatDate = (dateString) => {
if (!dateString) return '';
const date = new Date(dateString);
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric'
});
};
// Computed metrics
const personalMetrics = computed(() => {
// Filter requests by the selected time range
const requests = accessRequests.value.filter(req => {
const requestDate = new Date(req.requestDate);
const now = new Date();
const daysAgo = (now - requestDate) / (1000 * 60 * 60 * 24);
if (selectedTimeRange.value === '7days') return daysAgo <= 7;
if (selectedTimeRange.value === '30days') return daysAgo <= 30;
if (selectedTimeRange.value === '90days') return daysAgo <= 90;
if (selectedTimeRange.value === '365days') return daysAgo <= 365;
return true;
});
// Extract only resolved requests (approved or rejected)
const resolvedRequests = requests.filter(req =>
req.status === 'approved' || req.status === 'rejected'
);
// Total requests
const totalRequests = requests.length;
const pendingRequests = requests.filter(req => req.status === 'pending').length;
const approvedRequests = requests.filter(req => req.status === 'approved').length;
const rejectedRequests = requests.filter(req => req.status === 'rejected').length;
// Calculate response times
let totalResponseTime = 0;
let fastestResponse = Infinity;
let slowestResponse = 0;
resolvedRequests.forEach(req => {
const requestDate = new Date(req.requestDate);
const responseDate = new Date(req.responseDate);
const responseTime = (responseDate - requestDate) / (1000 * 60 * 60); // in hours
totalResponseTime += responseTime;
fastestResponse = Math.min(fastestResponse, responseTime);
slowestResponse = Math.max(slowestResponse, responseTime);
});
// Calculate averages
const avgResponseTime = resolvedRequests.length > 0
? totalResponseTime / resolvedRequests.length
: 0;
// Calculate overdue
const overdueRequests = requests.filter(req => {
if (req.status !== 'pending') return false;
const deadline = new Date(req.targetResolutionTime);
const now = new Date();
return now > deadline;
}).length;
const overduePercentage = totalRequests > 0
? overdueRequests / totalRequests
: 0;
return {
totalRequests,
pendingRequests,
approvedRequests,
rejectedRequests,
avgResponseTime,
fastestResponse: fastestResponse === Infinity ? 0 : fastestResponse,
slowestResponse,
overdueRequests,
overduePercentage
};
});
const departmentMetrics = computed(() => {
// Same calculations but for department requests
const requests = departmentRequests.value.filter(req => {
const requestDate = new Date(req.requestDate);
const now = new Date();
const daysAgo = (now - requestDate) / (1000 * 60 * 60 * 24);
if (selectedTimeRange.value === '7days') return daysAgo <= 7;
if (selectedTimeRange.value === '30days') return daysAgo <= 30;
if (selectedTimeRange.value === '90days') return daysAgo <= 90;
if (selectedTimeRange.value === '365days') return daysAgo <= 365;
return true;
});
// Extract only resolved requests (approved or rejected)
const resolvedRequests = requests.filter(req =>
req.status === 'approved' || req.status === 'rejected'
);
// Total requests
const totalRequests = requests.length;
const pendingRequests = requests.filter(req => req.status === 'pending').length;
const approvedRequests = requests.filter(req => req.status === 'approved').length;
const rejectedRequests = requests.filter(req => req.status === 'rejected').length;
// Calculate response times
let totalResponseTime = 0;
let fastestResponse = Infinity;
let slowestResponse = 0;
resolvedRequests.forEach(req => {
const requestDate = new Date(req.requestDate);
const responseDate = new Date(req.responseDate);
const responseTime = (responseDate - requestDate) / (1000 * 60 * 60); // in hours
totalResponseTime += responseTime;
fastestResponse = Math.min(fastestResponse, responseTime);
slowestResponse = Math.max(slowestResponse, responseTime);
});
// Calculate averages
const avgResponseTime = resolvedRequests.length > 0
? totalResponseTime / resolvedRequests.length
: 0;
// Calculate overdue
const overdueRequests = requests.filter(req => {
if (req.status !== 'pending') return false;
const deadline = new Date(req.targetResolutionTime);
const now = new Date();
return now > deadline;
}).length;
const overduePercentage = totalRequests > 0
? overdueRequests / totalRequests
: 0;
return {
totalRequests,
pendingRequests,
approvedRequests,
rejectedRequests,
avgResponseTime,
fastestResponse: fastestResponse === Infinity ? 0 : fastestResponse,
slowestResponse,
overdueRequests,
overduePercentage
};
});
// Lifecycle
onMounted(() => {
loadRequestData();
});
</script>
<template>
<div class="dms-access-request-tracker">
<!-- Header with time range selector -->
<div class="flex flex-wrap items-center justify-between mb-4 gap-3">
<div>
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">Access Request Metrics</h2>
</div>
<div class="flex items-center space-x-2">
<!-- Time range selector -->
<div class="inline-flex rounded-md shadow-sm">
<button
v-for="option in timeRangeOptions"
:key="option.id"
@click="updateTimeRange(option.id)"
class="relative inline-flex items-center px-3 py-1.5 text-sm font-medium border transition-colors"
:class="[
selectedTimeRange === option.id
? 'bg-blue-100 border-blue-300 text-blue-800 dark:bg-blue-900/20 dark:border-blue-700 dark:text-blue-300 z-10'
: 'bg-white border-gray-300 text-gray-700 hover:bg-gray-50 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300',
option.id === '7days' ? 'rounded-l-md' : '',
option.id === '365days' ? 'rounded-r-md' : ''
]"
>
{{ option.label }}
</button>
</div>
<!-- Refresh button -->
<button
@click="loadRequestData"
class="inline-flex items-center px-2 py-1.5 border border-gray-300 dark:border-gray-600 rounded-md text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
<span class="ml-1">Refresh</span>
</button>
</div>
</div>
<!-- Loading state -->
<div v-if="isLoading" class="flex items-center justify-center py-12">
<div class="text-center">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-4"></div>
<p class="text-gray-600 dark:text-gray-400">Loading metrics...</p>
</div>
</div>
<!-- KPI Dashboard -->
<div v-else class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Personal Metrics -->
<div v-if="props.showPersonal" class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden">
<div class="px-6 py-4 bg-blue-50 dark:bg-blue-900/10 border-b border-blue-200 dark:border-blue-800">
<h3 class="text-md font-medium text-blue-900 dark:text-blue-100">Personal Metrics</h3>
<p class="text-sm text-blue-700 dark:text-blue-300">Your access request performance</p>
</div>
<div class="p-6">
<!-- Status Metrics -->
<div class="grid grid-cols-2 sm:grid-cols-4 gap-4 mb-6">
<div class="bg-gray-50 dark:bg-gray-900/10 p-4 rounded-lg border border-gray-200 dark:border-gray-700">
<div class="text-xl font-bold text-gray-900 dark:text-gray-100">{{ personalMetrics.totalRequests }}</div>
<div class="text-sm text-gray-600 dark:text-gray-400">Total Requests</div>
</div>
<div class="bg-yellow-50 dark:bg-yellow-900/10 p-4 rounded-lg border border-yellow-200 dark:border-yellow-800">
<div class="text-xl font-bold text-yellow-700 dark:text-yellow-300">{{ personalMetrics.pendingRequests }}</div>
<div class="text-sm text-yellow-600 dark:text-yellow-400">Pending</div>
</div>
<div class="bg-green-50 dark:bg-green-900/10 p-4 rounded-lg border border-green-200 dark:border-green-800">
<div class="text-xl font-bold text-green-700 dark:text-green-300">{{ personalMetrics.approvedRequests }}</div>
<div class="text-sm text-green-600 dark:text-green-400">Approved</div>
</div>
<div class="bg-red-50 dark:bg-red-900/10 p-4 rounded-lg border border-red-200 dark:border-red-800">
<div class="text-xl font-bold text-red-700 dark:text-red-300">{{ personalMetrics.rejectedRequests }}</div>
<div class="text-sm text-red-600 dark:text-red-400">Rejected</div>
</div>
</div>
<!-- Response Time Metrics -->
<h4 class="text-sm font-medium text-gray-900 dark:text-gray-100 mb-3">Response Time Metrics</h4>
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-6">
<div class="bg-indigo-50 dark:bg-indigo-900/10 p-4 rounded-lg border border-indigo-200 dark:border-indigo-800">
<div class="text-xl font-bold text-indigo-700 dark:text-indigo-300">{{ formatDuration(personalMetrics.avgResponseTime) }}</div>
<div class="text-sm text-indigo-600 dark:text-indigo-400">Average Response</div>
</div>
<div class="bg-green-50 dark:bg-green-900/10 p-4 rounded-lg border border-green-200 dark:border-green-800">
<div class="text-xl font-bold text-green-700 dark:text-green-300">{{ formatDuration(personalMetrics.fastestResponse) }}</div>
<div class="text-sm text-green-600 dark:text-green-400">Fastest Response</div>
</div>
<div class="bg-red-50 dark:bg-red-900/10 p-4 rounded-lg border border-red-200 dark:border-red-800">
<div class="text-xl font-bold text-red-700 dark:text-red-300">{{ formatDuration(personalMetrics.slowestResponse) }}</div>
<div class="text-sm text-red-600 dark:text-red-400">Slowest Response</div>
</div>
</div>
<!-- Overdue Metrics -->
<h4 class="text-sm font-medium text-gray-900 dark:text-gray-100 mb-3">Overdue Metrics</h4>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="bg-orange-50 dark:bg-orange-900/10 p-4 rounded-lg border border-orange-200 dark:border-orange-800">
<div class="text-xl font-bold text-orange-700 dark:text-orange-300">{{ personalMetrics.overdueRequests }}</div>
<div class="text-sm text-orange-600 dark:text-orange-400">Overdue Requests</div>
</div>
<div class="bg-orange-50 dark:bg-orange-900/10 p-4 rounded-lg border border-orange-200 dark:border-orange-800">
<div class="text-xl font-bold text-orange-700 dark:text-orange-300">{{ formatPercentage(personalMetrics.overduePercentage) }}</div>
<div class="text-sm text-orange-600 dark:text-orange-400">Overdue Percentage</div>
</div>
</div>
</div>
</div>
<!-- Department Metrics -->
<div v-if="props.showDepartmental" class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden">
<div class="px-6 py-4 bg-purple-50 dark:bg-purple-900/10 border-b border-purple-200 dark:border-purple-800">
<h3 class="text-md font-medium text-purple-900 dark:text-purple-100">Department Metrics</h3>
<p class="text-sm text-purple-700 dark:text-purple-300">Department-wide performance</p>
</div>
<div class="p-6">
<!-- Status Metrics -->
<div class="grid grid-cols-2 sm:grid-cols-4 gap-4 mb-6">
<div class="bg-gray-50 dark:bg-gray-900/10 p-4 rounded-lg border border-gray-200 dark:border-gray-700">
<div class="text-xl font-bold text-gray-900 dark:text-gray-100">{{ departmentMetrics.totalRequests }}</div>
<div class="text-sm text-gray-600 dark:text-gray-400">Total Requests</div>
</div>
<div class="bg-yellow-50 dark:bg-yellow-900/10 p-4 rounded-lg border border-yellow-200 dark:border-yellow-800">
<div class="text-xl font-bold text-yellow-700 dark:text-yellow-300">{{ departmentMetrics.pendingRequests }}</div>
<div class="text-sm text-yellow-600 dark:text-yellow-400">Pending</div>
</div>
<div class="bg-green-50 dark:bg-green-900/10 p-4 rounded-lg border border-green-200 dark:border-green-800">
<div class="text-xl font-bold text-green-700 dark:text-green-300">{{ departmentMetrics.approvedRequests }}</div>
<div class="text-sm text-green-600 dark:text-green-400">Approved</div>
</div>
<div class="bg-red-50 dark:bg-red-900/10 p-4 rounded-lg border border-red-200 dark:border-red-800">
<div class="text-xl font-bold text-red-700 dark:text-red-300">{{ departmentMetrics.rejectedRequests }}</div>
<div class="text-sm text-red-600 dark:text-red-400">Rejected</div>
</div>
</div>
<!-- Response Time Metrics -->
<h4 class="text-sm font-medium text-gray-900 dark:text-gray-100 mb-3">Response Time Metrics</h4>
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-6">
<div class="bg-indigo-50 dark:bg-indigo-900/10 p-4 rounded-lg border border-indigo-200 dark:border-indigo-800">
<div class="text-xl font-bold text-indigo-700 dark:text-indigo-300">{{ formatDuration(departmentMetrics.avgResponseTime) }}</div>
<div class="text-sm text-indigo-600 dark:text-indigo-400">Average Response</div>
</div>
<div class="bg-green-50 dark:bg-green-900/10 p-4 rounded-lg border border-green-200 dark:border-green-800">
<div class="text-xl font-bold text-green-700 dark:text-green-300">{{ formatDuration(departmentMetrics.fastestResponse) }}</div>
<div class="text-sm text-green-600 dark:text-green-400">Fastest Response</div>
</div>
<div class="bg-red-50 dark:bg-red-900/10 p-4 rounded-lg border border-red-200 dark:border-red-800">
<div class="text-xl font-bold text-red-700 dark:text-red-300">{{ formatDuration(departmentMetrics.slowestResponse) }}</div>
<div class="text-sm text-red-600 dark:text-red-400">Slowest Response</div>
</div>
</div>
<!-- Overdue Metrics -->
<h4 class="text-sm font-medium text-gray-900 dark:text-gray-100 mb-3">Overdue Metrics</h4>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="bg-orange-50 dark:bg-orange-900/10 p-4 rounded-lg border border-orange-200 dark:border-orange-800">
<div class="text-xl font-bold text-orange-700 dark:text-orange-300">{{ departmentMetrics.overdueRequests }}</div>
<div class="text-sm text-orange-600 dark:text-orange-400">Overdue Requests</div>
</div>
<div class="bg-orange-50 dark:bg-orange-900/10 p-4 rounded-lg border border-orange-200 dark:border-orange-800">
<div class="text-xl font-bold text-orange-700 dark:text-orange-300">{{ formatPercentage(departmentMetrics.overduePercentage) }}</div>
<div class="text-sm text-orange-600 dark:text-orange-400">Overdue Percentage</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.dms-access-request-tracker {
width: 100%;
}
</style>

View File

@ -0,0 +1,539 @@
<script setup>
import { ref, computed, onMounted } from 'vue';
import { useDmsStore } from '~/stores/dms';
import DMSAccessApprovalDialog from '~/components/dms/dialogs/DMSAccessApprovalDialog.vue';
// Store
const dmsStore = useDmsStore();
// Props
const props = defineProps({
showClosed: {
type: Boolean,
default: false
},
filterByUser: {
type: String,
default: null
},
maxItems: {
type: Number,
default: 20
}
});
// Component state
const isLoading = ref(true);
const statusFilter = ref('all'); // all, pending, approved, rejected
const sortBy = ref('requestDate'); // requestDate, documentName, requester, deadline
const sortOrder = ref('desc'); // asc, desc
const accessRequests = ref([]);
const selectedRequest = ref(null);
const showApprovalDialog = ref(false);
// Status options
const statusOptions = [
{ id: 'all', label: 'All Requests', color: 'gray' },
{ id: 'pending', label: 'Pending', color: 'yellow' },
{ id: 'approved', label: 'Approved', color: 'green' },
{ id: 'rejected', label: 'Rejected', color: 'red' }
];
// Date formatters
const formatDate = (dateString) => {
if (!dateString) return '';
const date = new Date(dateString);
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric'
});
};
const formatDatetime = (dateString) => {
if (!dateString) return '';
const date = new Date(dateString);
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
};
// Duration formatter (e.g. "2 days ago")
const formatTimeAgo = (dateString) => {
if (!dateString) return '';
const date = new Date(dateString);
const now = new Date();
const diffMs = now - date;
const diffSec = Math.round(diffMs / 1000);
const diffMin = Math.round(diffSec / 60);
const diffHour = Math.round(diffMin / 60);
const diffDay = Math.round(diffHour / 24);
if (diffSec < 60) {
return `${diffSec} second${diffSec !== 1 ? 's' : ''} ago`;
} else if (diffMin < 60) {
return `${diffMin} minute${diffMin !== 1 ? 's' : ''} ago`;
} else if (diffHour < 24) {
return `${diffHour} hour${diffHour !== 1 ? 's' : ''} ago`;
} else if (diffDay < 7) {
return `${diffDay} day${diffDay !== 1 ? 's' : ''} ago`;
} else {
return formatDate(dateString);
}
};
// Calculate deadline status
const getDeadlineStatus = (request) => {
if (!request.targetResolutionTime) return { status: 'none', text: 'No deadline' };
if (request.status !== 'pending') return { status: 'none', text: 'Completed' };
const deadline = new Date(request.targetResolutionTime);
const now = new Date();
const diffMs = deadline - now;
const diffHours = diffMs / (1000 * 60 * 60);
if (diffHours < 0) {
return {
status: 'overdue',
text: `Overdue by ${Math.abs(Math.floor(diffHours / 24))} days`
};
} else if (diffHours < 24) {
return {
status: 'urgent',
text: `Due in ${Math.ceil(diffHours)} hours`
};
} else {
return {
status: 'normal',
text: `Due in ${Math.ceil(diffHours / 24)} days`
};
}
};
// Computed
const filteredRequests = computed(() => {
let result = [...accessRequests.value];
// Apply status filter
if (statusFilter.value !== 'all') {
result = result.filter(req => req.status === statusFilter.value);
}
// Apply show closed filter
if (!props.showClosed) {
result = result.filter(req => req.status === 'pending');
}
// Apply user filter
if (props.filterByUser) {
result = result.filter(req => req.requesterId === props.filterByUser || req.approverId === props.filterByUser);
}
// Apply sorting
result.sort((a, b) => {
let comparison = 0;
switch (sortBy.value) {
case 'requestDate':
comparison = new Date(a.requestDate) - new Date(b.requestDate);
break;
case 'documentName':
comparison = a.documentName.localeCompare(b.documentName);
break;
case 'requester':
comparison = a.requesterName.localeCompare(b.requesterName);
break;
case 'deadline':
// Sort by deadline with null values last
if (!a.targetResolutionTime && !b.targetResolutionTime) comparison = 0;
else if (!a.targetResolutionTime) comparison = 1;
else if (!b.targetResolutionTime) comparison = -1;
else comparison = new Date(a.targetResolutionTime) - new Date(b.targetResolutionTime);
break;
}
return sortOrder.value === 'asc' ? comparison : -comparison;
});
// Apply max items limit
if (props.maxItems > 0) {
result = result.slice(0, props.maxItems);
}
return result;
});
// Get status badge class
const getStatusClass = (status) => {
switch (status) {
case 'pending':
return 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-300';
case 'approved':
return 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300';
case 'rejected':
return 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300';
default:
return 'bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-300';
}
};
// Get deadline class
const getDeadlineClass = (deadlineStatus) => {
switch (deadlineStatus) {
case 'overdue':
return 'text-red-600 dark:text-red-400';
case 'urgent':
return 'text-orange-600 dark:text-orange-400';
case 'normal':
return 'text-blue-600 dark:text-blue-400';
default:
return 'text-gray-600 dark:text-gray-400';
}
};
// Actions
const loadAccessRequests = async () => {
isLoading.value = true;
try {
// In a real implementation, this would come from an API
// For now, use mock data from the store
accessRequests.value = await dmsStore.getAccessRequests();
} catch (error) {
console.error('Failed to load access requests:', error);
} finally {
isLoading.value = false;
}
};
const openApprovalDialog = (request) => {
selectedRequest.value = request;
showApprovalDialog.value = true;
};
const handleApprovalAction = async (result) => {
if (!selectedRequest.value) return;
if (result.action === 'approve') {
await dmsStore.approveAccessRequest(selectedRequest.value.id, result.notes);
} else if (result.action === 'reject') {
await dmsStore.rejectAccessRequest(selectedRequest.value.id, result.notes);
}
// Refresh the list
await loadAccessRequests();
// Close the dialog
showApprovalDialog.value = false;
selectedRequest.value = null;
};
// Change sorting
const toggleSort = (field) => {
if (sortBy.value === field) {
// Toggle sort direction
sortOrder.value = sortOrder.value === 'asc' ? 'desc' : 'asc';
} else {
// Set new sort field and default to ascending
sortBy.value = field;
sortOrder.value = 'asc';
}
};
// Lifecycle
onMounted(() => {
loadAccessRequests();
});
</script>
<template>
<div class="dms-approval-queue">
<!-- Header with filters -->
<div class="flex flex-wrap items-center justify-between mb-4 gap-3">
<div>
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">Access Requests</h2>
</div>
<div class="flex items-center space-x-2">
<!-- Status filter -->
<div class="inline-flex rounded-md shadow-sm">
<button
v-for="option in statusOptions"
:key="option.id"
@click="statusFilter = option.id"
class="relative inline-flex items-center px-3 py-1.5 text-sm font-medium border transition-colors"
:class="[
statusFilter === option.id
? `bg-${option.color}-100 border-${option.color}-300 text-${option.color}-800 dark:bg-${option.color}-900/20 dark:border-${option.color}-700 dark:text-${option.color}-300 z-10`
: 'bg-white border-gray-300 text-gray-700 hover:bg-gray-50 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300',
option.id === 'all' ? 'rounded-l-md' : '',
option.id === 'rejected' ? 'rounded-r-md' : ''
]"
>
{{ option.label }}
</button>
</div>
<!-- Refresh button -->
<button
@click="loadAccessRequests"
class="inline-flex items-center px-2 py-1.5 border border-gray-300 dark:border-gray-600 rounded-md text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
<span class="ml-1">Refresh</span>
</button>
</div>
</div>
<!-- Loading state -->
<div v-if="isLoading" class="flex items-center justify-center py-12">
<div class="text-center">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-4"></div>
<p class="text-gray-600 dark:text-gray-400">Loading access requests...</p>
</div>
</div>
<!-- Empty state -->
<div v-else-if="filteredRequests.length === 0" class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-8 text-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 mx-auto text-gray-400 dark:text-gray-500 mb-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-1">No access requests found</h3>
<p class="text-gray-600 dark:text-gray-400 mb-4">There are no access requests matching your current filters.</p>
<rs-button
@click="statusFilter = 'all'; loadAccessRequests()"
variant="secondary"
size="sm"
>
View all requests
</rs-button>
</div>
<!-- Requests table -->
<div v-else class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden">
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead class="bg-gray-50 dark:bg-gray-900/20">
<tr>
<th
@click="toggleSort('documentName')"
scope="col"
class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-800"
>
<div class="flex items-center">
<span>Document</span>
<svg
v-if="sortBy === 'documentName'"
xmlns="http://www.w3.org/2000/svg"
class="ml-1 h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
:d="sortOrder === 'asc' ? 'M5 15l7-7 7 7' : 'M19 9l-7 7-7-7'"
/>
</svg>
</div>
</th>
<th
@click="toggleSort('requester')"
scope="col"
class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-800"
>
<div class="flex items-center">
<span>Requester</span>
<svg
v-if="sortBy === 'requester'"
xmlns="http://www.w3.org/2000/svg"
class="ml-1 h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
:d="sortOrder === 'asc' ? 'M5 15l7-7 7 7' : 'M19 9l-7 7-7-7'"
/>
</svg>
</div>
</th>
<th
@click="toggleSort('requestDate')"
scope="col"
class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-800"
>
<div class="flex items-center">
<span>Requested</span>
<svg
v-if="sortBy === 'requestDate'"
xmlns="http://www.w3.org/2000/svg"
class="ml-1 h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
:d="sortOrder === 'asc' ? 'M5 15l7-7 7 7' : 'M19 9l-7 7-7-7'"
/>
</svg>
</div>
</th>
<th
@click="toggleSort('deadline')"
scope="col"
class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-800"
>
<div class="flex items-center">
<span>Deadline</span>
<svg
v-if="sortBy === 'deadline'"
xmlns="http://www.w3.org/2000/svg"
class="ml-1 h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
:d="sortOrder === 'asc' ? 'M5 15l7-7 7 7' : 'M19 9l-7 7-7-7'"
/>
</svg>
</div>
</th>
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Status
</th>
<th scope="col" class="px-4 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Actions
</th>
</tr>
</thead>
<tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
<tr
v-for="request in filteredRequests"
:key="request.id"
class="hover:bg-gray-50 dark:hover:bg-gray-900/10 transition-colors"
>
<!-- Document -->
<td class="px-4 py-3 whitespace-nowrap">
<div class="flex items-center">
<!-- Document icon based on type -->
<span class="flex-shrink-0 h-8 w-8 rounded-md bg-blue-100 dark:bg-blue-900/20 flex items-center justify-center text-blue-600 dark:text-blue-400 mr-3">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
</span>
<div>
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">
{{ request.documentName }}
</div>
<div class="text-xs text-gray-500 dark:text-gray-400">
{{ request.documentPath }}
</div>
</div>
</div>
</td>
<!-- Requester -->
<td class="px-4 py-3 whitespace-nowrap">
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">
{{ request.requesterName }}
</div>
<div class="text-xs text-gray-500 dark:text-gray-400">
{{ request.requesterDepartment }}
</div>
</td>
<!-- Request Date -->
<td class="px-4 py-3 whitespace-nowrap">
<div class="text-sm text-gray-900 dark:text-gray-100">
{{ formatDate(request.requestDate) }}
</div>
<div class="text-xs text-gray-500 dark:text-gray-400">
{{ formatTimeAgo(request.requestDate) }}
</div>
</td>
<!-- Deadline -->
<td class="px-4 py-3 whitespace-nowrap">
<div
v-if="request.targetResolutionTime"
class="text-sm"
:class="getDeadlineClass(getDeadlineStatus(request).status)"
>
{{ formatDate(request.targetResolutionTime) }}
</div>
<div
class="text-xs"
:class="getDeadlineClass(getDeadlineStatus(request).status)"
>
{{ getDeadlineStatus(request).text }}
</div>
</td>
<!-- Status -->
<td class="px-4 py-3 whitespace-nowrap">
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full" :class="getStatusClass(request.status)">
{{ request.status.charAt(0).toUpperCase() + request.status.slice(1) }}
</span>
</td>
<!-- Actions -->
<td class="px-4 py-3 whitespace-nowrap text-right text-sm font-medium">
<div class="flex justify-end space-x-2">
<button
v-if="request.status === 'pending'"
@click="openApprovalDialog(request)"
class="text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-200"
>
Review
</button>
<button
v-else
class="text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-200"
>
Details
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!-- Approval Dialog -->
<DMSAccessApprovalDialog
v-if="selectedRequest"
:visible="showApprovalDialog"
:request="selectedRequest"
@close="showApprovalDialog = false"
@submit="handleApprovalAction"
/>
</div>
</template>
<style scoped>
.dms-approval-queue {
width: 100%;
}
/* Dynamic color styles for table header hover effect */
.cursor-pointer {
transition: background-color 0.15s ease-in-out;
}
</style>

View File

@ -0,0 +1,346 @@
export const useDmsSettings = () => {
// Global DMS settings state
const dmsSettings = useState('dmsSettings', () => ({
// User & Access Management
access: {
userRoles: ['Admin', 'Editor', 'Viewer', 'Uploader'],
rbacEnabled: true,
userGroups: ['HR Department', 'Finance', 'IT', 'Legal'],
permissions: {
view: true,
edit: true,
delete: false,
download: true,
share: true
},
authentication: {
ssoEnabled: false,
mfaRequired: false,
ldapIntegration: false,
sessionTimeout: 8
}
},
// Document & Folder Settings
documents: {
folderHierarchy: {
maxDepth: 5,
defaultStructure: ['Department', 'Project', 'Category', 'Year'],
folderTemplates: ['Standard', 'Project-based', 'Department-based']
},
namingConventions: {
autoGenerate: true,
mandatoryFields: ['title', 'department', 'date'],
pattern: '{department}_{title}_{date}'
},
retention: {
enabled: true,
defaultDays: 2555, // 7 years
archiveBeforeDelete: true
},
versionControl: {
enabled: true,
maxVersions: 10,
autoVersioning: true
}
},
// Metadata & Tagging
metadata: {
customFields: [
{ name: 'Department', type: 'dropdown', required: true },
{ name: 'Priority', type: 'select', required: false },
{ name: 'Project Code', type: 'text', required: true },
{ name: 'Review Date', type: 'date', required: false }
],
tagging: {
predefinedTags: ['urgent', 'confidential', 'public', 'draft', 'final'],
userGeneratedTags: true,
tagSuggestions: true
},
classification: {
autoClassification: true,
rules: ['confidential-keywords', 'department-based', 'file-type']
}
},
// Workflow & Automation
workflow: {
approvalFlows: {
enabled: true,
defaultFlow: 'department-head-approval',
customFlows: ['legal-review', 'finance-approval', 'director-sign-off']
},
notifications: {
emailNotifications: true,
inAppNotifications: true,
uploadAlerts: true,
deadlineReminders: true
},
automation: {
triggers: ['document-uploaded', 'approval-completed', 'deadline-reached'],
actions: ['move-to-folder', 'send-notification', 'create-task']
}
},
// Upload & Storage Settings
upload: {
fileTypes: {
allowed: ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'jpg', 'png'],
blocked: ['exe', 'bat', 'cmd']
},
fileSizeLimit: 100, // MB
quotas: {
perUser: 5000, // MB
perGroup: 50000, // MB
perProject: 100000 // MB
},
storage: {
type: 'local', // local, s3, azure, google
path: '/var/uploads/edms',
backupEnabled: true,
compressionEnabled: false
}
},
// System Settings
system: {
timezone: 'Asia/Kuala_Lumpur',
backupSchedule: 'daily',
logLevel: 'info',
maintenanceMode: false,
autoUpdates: false,
systemMonitoring: true,
performanceMetrics: true
}
}));
// Loading state
const loading = useState('dmsSettingsLoading', () => false);
const saving = useState('dmsSettingsSaving', () => false);
// Load DMS settings from API
const loadDmsSettings = async () => {
loading.value = true;
try {
const response = await $fetch("/api/dms/settings", {
method: "GET",
});
if (response && response.data) {
dmsSettings.value = { ...dmsSettings.value, ...response.data };
console.log('[useDmsSettings] Settings loaded successfully:', response.data);
}
} catch (error) {
console.error("Error loading DMS settings:", error);
throw error;
} finally {
loading.value = false;
}
};
// Update DMS settings
const updateDmsSettings = async (newSettings) => {
console.log("[useDmsSettings] updateDmsSettings called with:", JSON.parse(JSON.stringify(newSettings)));
saving.value = true;
try {
const response = await $fetch("/api/dms/settings", {
method: "POST",
body: newSettings,
});
console.log("[useDmsSettings] API response received:", JSON.parse(JSON.stringify(response)));
if (response && response.statusCode === 200) {
// Reload settings after successful update
await loadDmsSettings();
console.log("[useDmsSettings] Returning success from updateDmsSettings.");
return { success: true, data: response.data };
}
let errorMessage = "Update operation failed: No data returned from server.";
if (response && typeof response === 'object' && response !== null && 'message' in response) {
errorMessage = response.message;
}
console.log("[useDmsSettings] Returning failure from updateDmsSettings:", errorMessage);
return { success: false, error: { message: errorMessage, details: response } };
} catch (error) {
console.error("[useDmsSettings] Error in updateDmsSettings catch block:", error);
let detailedMessage = "An unexpected error occurred during update.";
if (error.data && error.data.message) {
detailedMessage = error.data.message;
} else if (error.message) {
detailedMessage = error.message;
}
console.log("[useDmsSettings] Returning failure (catch block) from updateDmsSettings:", detailedMessage);
return { success: false, error: { message: detailedMessage, details: error } };
} finally {
saving.value = false;
}
};
// Reset settings to defaults
const resetToDefaults = async () => {
const defaultSettings = {
access: {
userRoles: ['Admin', 'Editor', 'Viewer', 'Uploader'],
rbacEnabled: true,
userGroups: ['HR Department', 'Finance', 'IT', 'Legal'],
permissions: {
view: true,
edit: true,
delete: false,
download: true,
share: true
},
authentication: {
ssoEnabled: false,
mfaRequired: false,
ldapIntegration: false,
sessionTimeout: 8
}
},
documents: {
folderHierarchy: {
maxDepth: 5,
defaultStructure: ['Department', 'Project', 'Category', 'Year'],
folderTemplates: ['Standard', 'Project-based', 'Department-based']
},
namingConventions: {
autoGenerate: true,
mandatoryFields: ['title', 'department', 'date'],
pattern: '{department}_{title}_{date}'
},
retention: {
enabled: true,
defaultDays: 2555,
archiveBeforeDelete: true
},
versionControl: {
enabled: true,
maxVersions: 10,
autoVersioning: true
}
},
metadata: {
customFields: [
{ name: 'Department', type: 'dropdown', required: true },
{ name: 'Priority', type: 'select', required: false },
{ name: 'Project Code', type: 'text', required: true },
{ name: 'Review Date', type: 'date', required: false }
],
tagging: {
predefinedTags: ['urgent', 'confidential', 'public', 'draft', 'final'],
userGeneratedTags: true,
tagSuggestions: true
},
classification: {
autoClassification: true,
rules: ['confidential-keywords', 'department-based', 'file-type']
}
},
workflow: {
approvalFlows: {
enabled: true,
defaultFlow: 'department-head-approval',
customFlows: ['legal-review', 'finance-approval', 'director-sign-off']
},
notifications: {
emailNotifications: true,
inAppNotifications: true,
uploadAlerts: true,
deadlineReminders: true
},
automation: {
triggers: ['document-uploaded', 'approval-completed', 'deadline-reached'],
actions: ['move-to-folder', 'send-notification', 'create-task']
}
},
upload: {
fileTypes: {
allowed: ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'jpg', 'png'],
blocked: ['exe', 'bat', 'cmd']
},
fileSizeLimit: 100,
quotas: {
perUser: 5000,
perGroup: 50000,
perProject: 100000
},
storage: {
type: 'local',
path: '/var/uploads/edms',
backupEnabled: true,
compressionEnabled: false
}
},
system: {
timezone: 'Asia/Kuala_Lumpur',
backupSchedule: 'daily',
logLevel: 'info',
maintenanceMode: false,
autoUpdates: false,
systemMonitoring: true,
performanceMetrics: true
}
};
return await updateDmsSettings(defaultSettings);
};
// Export settings to JSON
const exportSettings = () => {
const dataStr = JSON.stringify(dmsSettings.value, null, 2);
const dataUri = 'data:application/json;charset=utf-8,'+ encodeURIComponent(dataStr);
const exportFileDefaultName = 'dms-settings.json';
const linkElement = document.createElement('a');
linkElement.setAttribute('href', dataUri);
linkElement.setAttribute('download', exportFileDefaultName);
linkElement.click();
};
// Import settings from JSON
const importSettings = (jsonData) => {
try {
const importedSettings = JSON.parse(jsonData);
dmsSettings.value = { ...dmsSettings.value, ...importedSettings };
return { success: true };
} catch (error) {
console.error('Error importing settings:', error);
return { success: false, error: 'Invalid JSON format' };
}
};
// Get setting by category and key
const getSetting = (category, key) => {
if (dmsSettings.value[category]) {
return dmsSettings.value[category][key];
}
return null;
};
// Update specific setting
const updateSetting = async (category, key, value) => {
if (dmsSettings.value[category]) {
dmsSettings.value[category][key] = value;
// Save to backend
return await updateDmsSettings(dmsSettings.value);
}
return { success: false, error: 'Category not found' };
};
return {
dmsSettings: readonly(dmsSettings),
loading: readonly(loading),
saving: readonly(saving),
loadDmsSettings,
updateDmsSettings,
resetToDefaults,
exportSettings,
importSettings,
getSetting,
updateSetting
};
};

1019
docs/DMS_SETTINGS.md Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,161 +1,656 @@
# Site Settings Feature
# Site Settings System - Comprehensive Guide
## Overview
The Site Settings feature allows administrators to customize the appearance and branding of the application through a user-friendly interface. All settings are globally applied across the entire application including SEO, meta tags, and visual elements.
The Site Settings system provides administrators with comprehensive control over the visual appearance, branding, and global configuration of the Electronic Document Management System (EDMS). This system allows complete customization of the application's look and feel, SEO settings, and theme management through an intuitive web interface.
## Features
## Table of Contents
1. [System Integration](#system-integration)
2. [Features Overview](#features-overview)
3. [Accessing Site Settings](#accessing-site-settings)
4. [Configuration Categories](#configuration-categories)
5. [Basic Information Settings](#basic-information-settings)
6. [Branding & Visual Identity](#branding--visual-identity)
7. [SEO & Meta Tag Management](#seo--meta-tag-management)
8. [Theme System Integration](#theme-system-integration)
9. [Advanced Customization](#advanced-customization)
10. [API Integration](#api-integration)
11. [Technical Implementation](#technical-implementation)
12. [Best Practices](#best-practices)
13. [Troubleshooting](#troubleshooting)
## System Integration
### EDMS Ecosystem
The Site Settings system is fully integrated with the EDMS ecosystem and works alongside:
- **DMS Settings**: Document management configuration (separate system)
- **User Management**: Role-based access control for settings access
- **Theme System**: Dynamic theme switching and custom theme support
- **Component Library**: Rs component system integration
- **Global State**: Pinia store management for reactive updates
### Global Impact
Site settings affect the entire application including:
- **Header and Navigation**: Site logo, name, and branding elements
- **Loading Screens**: Custom loading logos and branding
- **Document Viewer**: Consistent branding across all interfaces
- **Authentication Pages**: Login page branding and styling
- **SEO Elements**: Meta tags, Open Graph, and social media integration
- **Theme Application**: Global color schemes and visual styling
## Features Overview
### Core Capabilities
- **Complete Branding Control**: Logos, site name, and visual identity
- **Theme Management**: Built-in themes plus custom theme support
- **SEO Optimization**: Comprehensive meta tag and social media optimization
- **Custom Styling**: CSS injection and custom theme file support
- **Real-time Preview**: Live preview of changes before applying
- **Global Integration**: Settings applied across entire application
- **Backup & Restore**: Export/import configuration for backup and migration
### Administrative Features
- **Role-Based Access**: Only administrators can modify site settings
- **Validation System**: Comprehensive validation for all settings
- **File Upload Management**: Secure file upload for logos and assets
- **Version Control**: Track changes and maintain configuration history
- **Multi-Environment Support**: Different configurations for different environments
## Accessing Site Settings
### Prerequisites
- **Administrative Privileges**: User must have admin role
- **Modern Browser**: Chrome 70+, Firefox 65+, Safari 12+, Edge 79+
- **JavaScript Enabled**: Required for interactive functionality
- **Network Access**: Stable connection for file uploads and saves
### Navigation Path
1. **Main Menu**: Navigate to **Pentadbiran** (Administration)
2. **Configuration Section**: Select **Konfigurasi** (Configuration)
3. **Site Settings**: Choose **Site Settings** from the submenu
4. **Alternative Access**: Direct URL navigation to `/admin/site-settings`
### Interface Layout
- **Tabbed Interface**: Basic Info, Branding, SEO, Advanced settings
- **Live Preview Panel**: Real-time preview of changes
- **Action Buttons**: Save, Reset, Export, Import functionality
- **Validation Feedback**: Immediate feedback on invalid settings
- **Loading States**: Visual feedback during save operations
## Configuration Categories
### 1. Basic Information
- **Site Name**: Customize the application name displayed globally in:
- Header and sidebar
- Browser title and meta tags
- SEO and Open Graph tags
- Loading screen
- All pages and components
- **Site Description**: Set a description used for:
- SEO meta descriptions
- Open Graph descriptions
- Twitter Card descriptions
- **Theme Selection**: Choose from available themes:
- Standard themes (from themeList.js)
- Accessibility themes (from themeList2.js)
- Custom themes added to theme.css
- **Site Name**: Application name displayed globally
- **Site Description**: Meta description for SEO and social sharing
- **Font Configuration**: Typography settings and font selection
- **Display Options**: Show/hide site name in header and other locations
### 2. Branding
- **Site Logo**: Upload a custom logo displayed in:
- Header (horizontal layout)
- Sidebar (vertical layout)
- Loading screen
- Login page
- Any component using site settings
- **Favicon**: Upload a custom favicon displayed in:
- Browser tabs
- Bookmarks
- Mobile home screen icons
- **Site Logo**: Main application logo for headers and navigation
- **Loading Logo**: Branded loading screen logo
- **Favicon**: Browser tab icon and bookmark icon
- **Login Logo**: Dedicated branding for authentication pages
### 3. Advanced Settings
- **Custom CSS**: Add custom CSS injected into document head
- **Custom Theme File**: Upload CSS files saved to `/assets/style/css/`
- **Add Custom Theme to theme.css**: Directly add themes to the main theme.css file
### 3. SEO & Social Media
- **Meta Tags**: Title, description, keywords, author information
- **Open Graph**: Facebook and social media sharing optimization
- **Twitter Cards**: Twitter-specific meta tag configuration
- **Analytics Integration**: Google Analytics, Tag Manager, Facebook Pixel
## How to Access
### 4. Advanced Settings
- **Custom CSS**: Global CSS injection for advanced styling
- **Custom Themes**: Upload and manage custom theme files
- **Theme Editor**: Direct editing of theme.css file
- **Developer Tools**: Advanced configuration options
1. Navigate to **Pentadbiran****Konfigurasi** → **Site Settings**
2. Use the tabbed interface:
- **Basic Info**: Site name, description, and theme selection
- **Branding**: Logo and favicon uploads
- **Advanced**: Custom CSS and theme management
3. Use the **Live Preview** panel to see changes in real-time
4. Click **Save Changes** to apply your settings
## Basic Information Settings
## Technical Implementation
### Site Name Configuration
- **Purpose**: Define the application name displayed throughout the system
- **Default Value**: "corradAF" (customizable)
- **Global Impact**: Updates header, page titles, meta tags, loading screens
- **Font Size Control**: Adjustable font size (12-36px range)
- **Character Limit**: Recommended maximum of 50 characters for optimal display
### Database Schema
The settings are stored in the `site_settings` table with the following fields:
- `siteName`, `siteDescription`
- `siteLogo`, `siteFavicon`
- `selectedTheme` - Selected theme name
- `customCSS`, `customThemeFile`
- Legacy fields maintained for backward compatibility
**Display Locations**:
- Header navigation (primary logo area)
- Browser page titles (prefixed to page names)
- Loading screen branding
- SEO meta tags and social sharing
- Authentication page headers
### API Endpoints
- `GET /api/devtool/config/site-settings` - Retrieve current settings
- `POST /api/devtool/config/site-settings` - Update settings
- `POST /api/devtool/config/upload-file` - Upload files (logos, themes)
- `POST /api/devtool/config/add-custom-theme` - Add custom theme to theme.css
### Site Description
- **Purpose**: Provide descriptive text for SEO and social media sharing
- **SEO Impact**: Used as meta description tag for search engines
- **Social Sharing**: Appears in social media link previews
- **Character Limit**: Recommended 150-160 characters for optimal SEO
- **Multi-language Support**: Single description applies globally
### File Upload Locations
- **Logo and Favicon files**: Saved to `public/uploads/site-settings/`
- **Theme CSS files**: Saved to `assets/style/css/` directory
- **Custom themes**: Added directly to `assets/style/css/base/theme.css`
### Typography Configuration
- **Font Selection**: Choose from available system fonts
- **Font Source**: Local fonts vs. web fonts (Google Fonts integration)
- **Font Size Controls**: Configurable sizes for different elements
- **Global Application**: Typography changes apply system-wide
### Composable
The `useSiteSettings()` composable provides:
- `siteSettings` - Reactive settings object
- `loadSiteSettings()` - Load settings from API
- `updateSiteSettings()` - Update settings
- `setTheme()` - Set theme using existing theme system
- `getCurrentTheme()` - Get current theme
- `applyThemeSettings()` - Apply theme changes to DOM
- `updateGlobalMeta()` - Update global meta tags and SEO
- `addCustomThemeToFile()` - Add custom theme to theme.css
## Branding & Visual Identity
### Global Integration
The site settings are globally integrated across:
### Logo Management System
#### Header Component
- Uses site settings for logo and name display
- Theme selection dropdown uses same system as site settings
- Synced with site settings theme selection
#### Site Logo (Primary)
- **Usage**: Main application logo in header and navigation
- **Recommended Dimensions**: 200x60px (maximum)
- **Supported Formats**: PNG, JPG, SVG (vector preferred)
- **File Size Limit**: 5MB maximum
- **Responsive Behavior**: Automatic scaling for different screen sizes
- **Fallback**: System default logo if custom logo fails to load
#### Loading Component
- Uses site logo if available, fallback to default
- Displays site name in loading screen
#### Loading Logo
- **Usage**: Branded loading screens and splash pages
- **Recommended Dimensions**: 100x100px (square preferred)
- **Animation Support**: Static images only (CSS animations can be added)
- **Multiple Screens**: Applied to all loading states throughout application
- **Brand Consistency**: Should complement primary site logo
#### App.vue
- Global meta tags updated from site settings
- Title, description, and favicon managed globally
- Theme initialization from site settings
#### Favicon Configuration
- **Usage**: Browser tab icons, bookmarks, mobile home screen icons
- **Required Format**: ICO format preferred (PNG acceptable)
- **Dimensions**: 16x16, 32x32, 48x48px (multi-size ICO recommended)
- **Mobile Icons**: Apple touch icons and Android home screen support
- **Browser Compatibility**: Cross-browser favicon support
#### SEO and Meta Tags
- Document title updated globally
- Meta descriptions for SEO
- Open Graph tags for social sharing
- Twitter Card tags
- Favicon and apple-touch-icon
#### Login Page Logo
- **Usage**: Dedicated branding for authentication and login pages
- **Design Consideration**: Can be different from main site logo for branding flexibility
- **Recommended Dimensions**: 250x80px maximum
- **Security Context**: Appears on security-sensitive pages
- **Brand Trust**: Important for user trust and brand recognition
### Theme System Integration
- Integrates with existing theme system (themeList.js, themeList2.js)
- Theme selection in header dropdown synced with site settings
- Custom themes can be added directly to theme.css
- Backward compatibility with existing theme structure
### File Upload System
- **Secure Upload**: File validation and virus scanning
- **Storage Location**: `public/uploads/site-settings/` directory
- **Backup Integration**: Uploaded files included in system backups
- **Version Control**: Maintain previous versions of uploaded assets
- **CDN Support**: Compatible with content delivery networks
### Custom Theme Structure
Custom themes added to theme.css should follow this structure:
## SEO & Meta Tag Management
### Search Engine Optimization
#### Basic SEO Settings
- **SEO Title**: Custom title for search engine results
- **Meta Description**: Description displayed in search results
- **Meta Keywords**: Keyword tags (legacy but still supported)
- **Meta Author**: Content author information
- **Canonical URL**: Preferred URL for content indexing
#### Advanced SEO Configuration
- **Robots Meta Tag**: Control search engine crawling behavior
- Default: "index, follow" (allow indexing and link following)
- Options: noindex, nofollow, noarchive, nosnippet
- **Structured Data**: Schema.org markup for rich snippets
- **XML Sitemap**: Automatic sitemap generation and submission
- **Page Speed Optimization**: Settings that impact page load times
### Social Media Integration
#### Open Graph (Facebook)
- **og:title**: Social media sharing title
- **og:description**: Social media sharing description
- **og:image**: Image displayed in social media previews
- **og:type**: Content type (website, article, etc.)
- **og:url**: Canonical URL for social sharing
#### Twitter Cards
- **twitter:card**: Card type (summary, summary_large_image, app, player)
- **twitter:title**: Twitter-specific sharing title
- **twitter:description**: Twitter-specific sharing description
- **twitter:image**: Twitter preview image
- **twitter:creator**: Twitter handle of content creator
### Analytics & Tracking
#### Google Analytics Integration
- **Tracking ID**: Google Analytics measurement ID (GA4 format)
- **Enhanced Ecommerce**: Advanced tracking for document interactions
- **Custom Events**: Track document downloads, views, searches
- **Privacy Compliance**: GDPR and privacy regulation compliance
#### Google Tag Manager
- **Container ID**: GTM container identifier
- **Custom Variables**: Document metadata as GTM variables
- **Event Tracking**: Advanced event tracking through GTM
- **Third-party Integration**: Easy integration with other analytics tools
#### Facebook Pixel
- **Pixel ID**: Facebook advertising pixel identifier
- **Conversion Tracking**: Track document interactions as conversions
- **Custom Audiences**: Build audiences based on document engagement
- **Privacy Controls**: Respect user privacy preferences
## Theme System Integration
### Built-in Theme Support
#### Standard Themes
The system includes several pre-built themes from `themeList.js`:
- **biasa**: Default neutral theme with balanced colors
- **terang**: Light theme with bright, clean aesthetics
- **gelap**: Dark theme for reduced eye strain
- **biru**: Blue-dominant professional theme
- **hijau**: Green nature-inspired theme
#### Accessibility Themes
Special themes from `themeList2.js` for enhanced accessibility:
- **high-contrast**: High contrast for visual impairments
- **large-text**: Increased font sizes for readability
- **color-blind**: Color-blind friendly color palettes
- **low-vision**: Optimized for users with low vision
### Custom Theme Development
#### Theme Structure
Custom themes follow CSS custom property structure:
```css
html[data-theme="your-theme-name"] {
html[data-theme="custom-theme-name"] {
--color-primary: 255, 0, 0;
--color-secondary: 0, 255, 0;
--color-success: 0, 255, 0;
--color-info: 0, 0, 255;
--color-warning: 255, 255, 0;
--color-danger: 255, 0, 0;
/* Add your theme variables here */
--color-light: 248, 249, 250;
--color-dark: 33, 37, 41;
/* Additional custom properties */
}
```
## Default Values
If no settings are configured, the system uses these defaults:
- Site Name: "corradAF"
- Site Description: "corradAF Base Project"
- Selected Theme: "biasa"
- Logo: Default corradAF logo
- Favicon: Default favicon
#### Color Variable System
- **RGB Values**: Colors defined as RGB triplets for alpha transparency support
- **Semantic Naming**: Colors named by purpose rather than appearance
- **Component Integration**: Variables used throughout Rs component system
- **Dark Mode Support**: Automatic dark mode variants
## Migration Notes
- Legacy color fields (primaryColor, secondaryColor, etc.) are maintained for backward compatibility
- `themeMode` field is mapped to `selectedTheme` for compatibility
- Existing installations will automatically use default values
- Theme selection integrates with existing theme dropdown in header
#### Theme File Management
- **Upload Location**: Custom themes saved to `assets/style/css/`
- **Integration Method**: Direct injection into `theme.css` file
- **Validation**: CSS syntax validation before integration
- **Backup**: Automatic backup before theme modifications
## Notes
- Changes are applied immediately in the preview
- Theme changes affect the entire application
- Custom CSS is injected into the document head
- Theme files are saved to `/assets/style/css/` for proper integration
- File uploads are validated for type and size
- Settings persist across browser sessions
- Site name and description updates are reflected globally and immediately
- All meta tags and SEO elements are automatically updated
- Logo changes are reflected in all components that use site settings
### Theme Switching Mechanism
- **Real-time Application**: Themes applied immediately without page refresh
- **Header Integration**: Theme selector synchronized with site settings
- **User Preferences**: Individual user theme preferences (if enabled)
- **Default Theme**: System-wide default theme setting
### Important Notes
- Changes are applied immediately in the preview
- Theme changes affect the entire application
- Custom CSS is injected into the document head
- Theme files are saved to `/assets/style/css/` for proper integration
- File uploads are validated for type and size
- Settings persist across browser sessions
- Site name and description updates are reflected globally and immediately
- All meta tags and SEO elements are automatically updated
- Logo changes are reflected in all components that use site settings
## Advanced Customization
### Custom CSS Injection
#### Global CSS Override
- **Injection Point**: CSS inserted into document `<head>` element
- **Priority**: Custom CSS has high specificity to override defaults
- **Validation**: Basic CSS syntax validation
- **Performance**: Minification and optimization of injected CSS
#### Best Practices for Custom CSS
- **Specificity**: Use appropriate CSS specificity for overrides
- **Performance**: Minimize custom CSS for better performance
- **Maintenance**: Document custom CSS changes for future reference
- **Testing**: Test across different browsers and devices
#### CSS Framework Integration
- **TailwindCSS Compatibility**: Custom CSS works alongside TailwindCSS
- **Component System**: Respect Rs component styling patterns
- **Responsive Design**: Ensure custom CSS is responsive
- **Dark Mode**: Consider dark mode implications
### Custom Theme File Upload
#### File Requirements
- **Format**: Standard CSS files (.css extension)
- **Size Limit**: 1MB maximum file size
- **Encoding**: UTF-8 encoding required
- **Syntax**: Valid CSS syntax required
#### Integration Process
1. **File Upload**: Secure file upload to server
2. **Validation**: CSS syntax and security validation
3. **Integration**: Append to main theme.css file
4. **Backup**: Create backup of previous theme.css
5. **Application**: Theme immediately available for selection
#### Security Considerations
- **Content Filtering**: Remove potentially dangerous CSS
- **Path Restrictions**: Prevent access to restricted file paths
- **Size Limits**: Prevent resource exhaustion attacks
- **Validation**: Comprehensive CSS validation
## API Integration
### Site Settings API Endpoints
#### GET `/api/devtool/config/site-settings`
**Purpose**: Retrieve current site settings configuration
**Response Format**:
```json
{
"statusCode": 200,
"message": "Success",
"data": {
"siteName": "Custom EDMS",
"siteDescription": "Enterprise Document Management System",
"siteLogo": "/uploads/site-settings/logo.png",
"siteFavicon": "/uploads/site-settings/favicon.ico",
"selectedTheme": "professional-blue",
"customCSS": ".custom-header { background: #blue; }",
"seoTitle": "EDMS - Document Management",
"seoDescription": "Comprehensive document management solution",
"seoKeywords": "document, management, enterprise",
"seoGoogleAnalytics": "GA-XXXXXXXXX-X"
}
}
```
#### POST `/api/devtool/config/site-settings`
**Purpose**: Update site settings configuration
**Request Format**:
```json
{
"siteName": "Updated EDMS Name",
"siteDescription": "Updated description",
"selectedTheme": "dark-professional",
"customCSS": ".updated-styles { color: red; }",
"seoTitle": "Updated SEO Title"
}
```
**Features**:
- Partial updates supported (only changed fields required)
- Automatic validation of all input data
- File path validation for security
- Database persistence with timestamps
#### POST `/api/devtool/config/upload-file`
**Purpose**: Secure file upload for logos and assets
**Request Format**: Multipart form data with file and metadata
**Response Format**:
```json
{
"statusCode": 200,
"message": "File uploaded successfully",
"data": {
"filename": "uploaded-logo.png",
"path": "/uploads/site-settings/logo.png",
"size": 15420,
"type": "image/png"
}
}
```
**Security Features**:
- File type validation (images only for logos)
- File size limits enforcement
- Virus scanning integration
- Secure file storage with proper permissions
#### POST `/api/devtool/config/add-custom-theme`
**Purpose**: Add custom theme directly to theme.css file
**Request Format**:
```json
{
"themeName": "custom-corporate",
"themeCSS": "html[data-theme=\"custom-corporate\"] { --color-primary: 0, 100, 200; }"
}
```
**Features**:
- CSS validation before integration
- Automatic backup of existing theme.css
- Immediate availability after integration
- Rollback capability if integration fails
### Composable Integration
#### useSiteSettings() Composable
The `useSiteSettings()` composable provides reactive state management:
```javascript
const {
siteSettings, // Reactive settings object
loading, // Loading state
loadSiteSettings, // Load settings from API
updateSiteSettings, // Update settings
setTheme, // Apply theme changes
getCurrentTheme, // Get current theme
applyThemeSettings, // Apply theme to DOM
updateGlobalMeta, // Update meta tags
addCustomThemeToFile // Add custom theme
} = useSiteSettings();
```
#### Real-time Updates
- **Reactive State**: Changes immediately reflected in UI
- **Global Sync**: Updates synchronized across all components
- **Meta Tag Updates**: SEO meta tags updated dynamically
- **Theme Application**: Theme changes applied without page refresh
## Technical Implementation
### Database Schema
Settings stored in `site_settings` table with comprehensive field structure:
```sql
CREATE TABLE site_settings (
settingID INT PRIMARY KEY AUTO_INCREMENT,
siteName VARCHAR(255) DEFAULT 'corradAF',
siteNameFontSize INT DEFAULT 18,
siteDescription TEXT,
siteLogo VARCHAR(500),
siteLoadingLogo VARCHAR(500),
siteFavicon VARCHAR(500),
siteLoginLogo VARCHAR(500),
showSiteNameInHeader BOOLEAN DEFAULT TRUE,
customCSS LONGTEXT,
themeMode VARCHAR(100) DEFAULT 'biasa',
customThemeFile VARCHAR(500),
currentFont VARCHAR(100),
fontSource VARCHAR(100),
seoTitle VARCHAR(255),
seoDescription TEXT,
seoKeywords TEXT,
seoAuthor VARCHAR(255),
seoOgImage VARCHAR(500),
seoTwitterCard VARCHAR(100) DEFAULT 'summary_large_image',
seoCanonicalUrl VARCHAR(500),
seoRobots VARCHAR(100) DEFAULT 'index, follow',
seoGoogleAnalytics VARCHAR(255),
seoGoogleTagManager VARCHAR(255),
seoFacebookPixel VARCHAR(255),
settingCreatedDate DATETIME DEFAULT CURRENT_TIMESTAMP,
settingModifiedDate DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
```
### File System Integration
- **Upload Directory**: `public/uploads/site-settings/`
- **Theme Directory**: `assets/style/css/`
- **Backup Directory**: `backups/site-settings/`
- **Permission Management**: Proper file system permissions
- **Cleanup Procedures**: Automatic cleanup of unused files
### Global Integration Points
#### Header Component
- **Logo Display**: Dynamic logo from site settings
- **Site Name**: Configurable site name display
- **Theme Selector**: Synchronized theme selection
- **Brand Colors**: Applied from theme settings
#### App.vue Global Configuration
- **Meta Tags**: Dynamically updated from settings
- **Title Management**: Global title prefix from site name
- **Theme Initialization**: Theme applied on app startup
- **Favicon**: Dynamic favicon from settings
#### Loading Components
- **Branded Loading**: Custom loading logo
- **Brand Colors**: Theme colors in loading screens
- **Consistent Experience**: Uniform branding across all loading states
## Best Practices
### Branding Guidelines
#### Logo Design
- **Vector Format**: Use SVG for scalability when possible
- **Optimization**: Optimize file sizes for web performance
- **Consistency**: Maintain consistent branding across all logos
- **Accessibility**: Ensure sufficient contrast for accessibility compliance
#### Color Schemes
- **Brand Consistency**: Align with organizational brand guidelines
- **Accessibility**: Meet WCAG contrast requirements
- **Cultural Sensitivity**: Consider cultural color associations
- **Dark Mode Support**: Provide appropriate dark mode variants
### SEO Optimization
#### Content Strategy
- **Unique Descriptions**: Unique meta descriptions for better SEO
- **Keyword Research**: Use relevant keywords in meta tags
- **Regular Updates**: Keep SEO content current and relevant
- **Performance**: Optimize for page load speed
#### Technical SEO
- **Structured Data**: Implement appropriate schema markup
- **Mobile Optimization**: Ensure mobile-friendly configuration
- **Page Speed**: Monitor and optimize loading times
- **Security**: Implement HTTPS and security headers
### Performance Considerations
#### File Optimization
- **Image Compression**: Compress logos and images appropriately
- **CSS Minification**: Minify custom CSS for better performance
- **Caching Strategy**: Implement appropriate caching headers
- **CDN Integration**: Consider CDN for static assets
#### Theme Performance
- **Minimal CSS**: Keep custom themes lightweight
- **Variable Usage**: Use CSS custom properties efficiently
- **Browser Compatibility**: Test across different browsers
- **Fallback Support**: Provide fallbacks for older browsers
### Security Best Practices
#### File Upload Security
- **File Type Validation**: Strict file type checking
- **Size Limits**: Appropriate file size restrictions
- **Virus Scanning**: Implement virus scanning for uploads
- **Path Traversal**: Prevent directory traversal attacks
#### CSS Security
- **Content Filtering**: Filter potentially dangerous CSS
- **XSS Prevention**: Prevent CSS-based XSS attacks
- **Input Validation**: Validate all CSS input thoroughly
- **Sanitization**: Sanitize user-provided CSS content
## Troubleshooting
### Common Issues
#### Settings Not Saving
**Symptoms**:
- Changes revert after page refresh
- Error messages during save operations
- Loading indicators that don't complete
**Resolution Steps**:
1. **Check Permissions**: Verify administrative access
2. **Network Issues**: Confirm stable internet connection
3. **Browser Console**: Check for JavaScript errors
4. **Server Status**: Verify server is operational
5. **Database Connection**: Check database connectivity
#### Logos Not Displaying
**Symptoms**:
- Broken image icons instead of logos
- Default logos showing instead of custom ones
- Images not loading properly
**Resolution Steps**:
1. **File Path**: Verify correct file paths in settings
2. **File Permissions**: Check server file permissions
3. **File Format**: Ensure supported image formats
4. **File Size**: Check if files exceed size limits
5. **Browser Cache**: Clear browser cache and reload
#### Theme Not Applying
**Symptoms**:
- Theme selection not taking effect
- Colors not changing as expected
- Custom themes not available
**Resolution Steps**:
1. **CSS Validation**: Check for CSS syntax errors
2. **Theme File**: Verify theme file integration
3. **Browser Compatibility**: Test with different browsers
4. **Cache Issues**: Clear browser and server cache
5. **CSS Conflicts**: Check for CSS specificity issues
### Performance Issues
#### Slow Loading
**Causes**:
- Large logo files
- Excessive custom CSS
- Network connectivity issues
- Server performance problems
**Optimization**:
1. **Image Optimization**: Compress logo files
2. **CSS Optimization**: Minimize custom CSS
3. **Caching**: Implement proper caching strategies
4. **CDN**: Use content delivery networks
5. **Server Optimization**: Monitor server performance
#### Memory Issues
**Symptoms**:
- Browser becoming unresponsive
- High memory usage
- Slow interface response
**Resolution**:
1. **File Size**: Reduce logo file sizes
2. **CSS Efficiency**: Optimize CSS selectors
3. **Browser Update**: Use latest browser versions
4. **System Resources**: Monitor system memory
5. **Cache Management**: Regular cache clearing
### Support Resources
#### Documentation
- **Technical Guide**: Comprehensive system documentation
- **User Guide**: End-user documentation
- **API Documentation**: Developer integration guides
- **Change Log**: System updates and changes
#### Contact Support
- **Internal IT**: Contact system administrators
- **Help Desk**: Organizational support procedures
- **Community**: User forums and knowledge bases
- **Vendor Support**: External support options
#### Information to Provide
When reporting issues:
- **Error Messages**: Exact error text
- **Browser Information**: Browser type and version
- **Steps to Reproduce**: Detailed reproduction steps
- **Screenshots**: Visual documentation of issues
- **System Details**: Relevant configuration information
This comprehensive guide provides complete coverage of the Site Settings system, enabling administrators to effectively customize and manage the visual appearance and global configuration of the EDMS application.

File diff suppressed because it is too large Load Diff

View File

@ -4,13 +4,13 @@
1. [Introduction](#introduction)
2. [Getting Started](#getting-started)
3. [Navigation](#navigation)
4. [Document Organization](#document-organization)
5. [Working with Documents](#working-with-documents)
6. [Access Control](#access-control)
7. [Search and Filtering](#search-and-filtering)
8. [Document Viewer](#document-viewer)
9. [My Documents](#my-documents)
10. [Settings Administration](#settings-administration)
4. [DMS Interface](#dms-interface)
5. [Access Level System](#access-level-system)
6. [Document Organization](#document-organization)
7. [Working with Documents](#working-with-documents)
8. [DMS Settings (Administrators)](#dms-settings-administrators)
9. [Search and Filtering](#search-and-filtering)
10. [Document Viewer](#document-viewer)
11. [Troubleshooting](#troubleshooting)
## Introduction
@ -18,9 +18,10 @@
The Electronic Document Management System (EDMS) is a modern web-based platform designed to efficiently organize, store, and manage digital documents in organizational environments. The system provides a secure, hierarchical structure for document storage with comprehensive role-based access control and advanced document management capabilities.
### Key Features
- **Hierarchical Organization**: Documents are organized in a Cabinet → Drawer → Folder → Subfolder structure
- **Access Level Organization**: Documents categorized as All, Public, Private, and Personal with color-coded tabs
- **Hierarchical Structure**: Documents organized in a Cabinet → Drawer → Folder → Subfolder structure
- **Multiple View Modes**: List, Grid, and Details views for browsing documents
- **Access Control**: Public, Private, and Personal document categories with granular permission management
- **Advanced Settings Management**: Comprehensive configuration system for administrators
- **Document Viewer**: Built-in viewer supporting multiple file formats (PDF, images, text files, spreadsheets)
- **Search Functionality**: Advanced search across document titles, descriptions, and metadata
- **Upload Management**: Drag-and-drop file upload with comprehensive metadata assignment
@ -55,27 +56,32 @@ The main dashboard provides:
### Main Navigation Menu
Access the EDMS through the main navigation menu:
- **Dashboard**: System overview and quick access to recent items
- **DMS**: Main document management interface
- **My Documents**: Personal document collection and workspace
- **Upload Document**: Dedicated document upload interface
- **DMS**: Main document management interface with access level tabs
- **DMS Settings**: Administrative configuration panel (admin only)
- **ERD**: Entity Relationship Diagram viewer for system structure
### EDMS Interface Layout
The EDMS interface consists of three main areas:
The EDMS interface consists of several main areas:
1. **Tree Navigation (Left Panel)**
1. **Access Level Tabs (Top)**
- **All Documents**: Complete view of all accessible documents (blue folder icon)
- **Public**: Documents available to all users (green unlock icon)
- **Private**: Restricted documents requiring special permissions (red lock icon)
- **Personal**: Individual user documents (purple user icon)
2. **Tree Navigation (Left Panel - when available)**
- Hierarchical view of cabinets, drawers, and folders
- Expandable/collapsible folder structure
- Color-coded access indicators for different permission levels
- Lock icons for restricted items requiring special access
2. **Content Area (Center)**
3. **Content Area (Center)**
- Document listing with multiple view modes (List, Grid, Details)
- Breadcrumb navigation showing current location
- Search and filter tools for finding documents
- View mode controls and sorting options
- Document tabs for filtering by access type
3. **Details Panel (Optional Right Panel)**
4. **Details Panel (Optional Right Panel)**
- Document properties and metadata information
- Quick actions and document tools
- Access control information and permissions
@ -87,6 +93,82 @@ The EDMS interface consists of three main areas:
- Helps maintain orientation in deep folder structures
- Displays full path from root to current location
## DMS Interface
### Access Level Tabs
The main DMS interface features a modern tabbed design for easy navigation between different document categories:
#### All Documents Tab
- **Icon**: Blue folder icon
- **Purpose**: Shows all documents you have permission to access
- **Color Scheme**: Blue theme with active indicator
- **Use Case**: Comprehensive view for finding any accessible document
#### Public Tab
- **Icon**: Green unlock icon
- **Purpose**: Displays documents accessible to all system users
- **Color Scheme**: Green theme indicating open access
- **Use Case**: Company policies, public announcements, shared resources
#### Private Tab
- **Icon**: Red lock icon
- **Purpose**: Shows restricted documents requiring special permissions
- **Color Scheme**: Red theme indicating restricted access
- **Use Case**: Confidential documents, sensitive information, executive files
#### Personal Tab
- **Icon**: Purple user icon
- **Purpose**: Displays your personal documents and private workspace
- **Color Scheme**: Purple theme indicating personal ownership
- **Use Case**: Individual workspace, draft documents, personal files
### Visual Design Features
- **Active Tab Indicators**: Current tab highlighted with "Active" badge
- **Hover Effects**: Smooth transitions when hovering over tabs
- **Loading States**: Visual feedback during system operations
- **Error Handling**: Clear error messages with retry options
## Access Level System
### Understanding Access Levels
#### Public Documents
- **Accessibility**: Visible and accessible to all system users
- **Visual Indicator**: Green color theme with unlock icon
- **Permissions**: No special permissions or approvals required
- **Best For**:
- Company policies and procedures
- Public announcements
- Shared templates and forms
- General information documents
#### Private Documents
- **Accessibility**: Restricted access requiring specific permissions
- **Visual Indicator**: Red color theme with lock icon
- **Permissions**: May require access request and approval workflow
- **Best For**:
- Confidential business information
- HR records and sensitive data
- Financial documents
- Executive communications
- Legal documents
#### Personal Documents
- **Accessibility**: Documents owned and controlled by individual users
- **Visual Indicator**: Purple color theme with user icon
- **Permissions**: Full control over sharing and access
- **Best For**:
- Individual workspace documents
- Draft documents before publication
- Personal notes and references
- Work-in-progress files
### Access Level Switching
- Click any access level tab to filter documents by that category
- Visual feedback shows which tab is currently active
- Document count may vary between tabs based on your permissions
- Some tabs may be empty if no documents exist in that category
## Document Organization
### Hierarchical Structure
@ -106,8 +188,9 @@ Documents are organized in a four-level hierarchy designed for maximum flexibili
### Visual Indicators
The system uses color coding and icons to indicate access levels:
- **Green**: Public documents (open access for all users)
- **Blue**: Personal documents (owned by you)
- **Blue**: All accessible documents (comprehensive view)
- **Red**: Private documents (restricted access)
- **Purple**: Personal documents (owned by you)
- **Lock Icon**: Documents you cannot currently access
- **Template Icon**: Document templates available for use
@ -117,7 +200,7 @@ The system uses color coding and icons to indicate access levels:
#### Single File Upload
1. Navigate to the desired location in the hierarchy
2. Click the "Upload" button or use the dedicated "Upload Document" page
2. Click the "Upload" button or use the dedicated upload interface
3. Select file(s) using the file picker or drag-and-drop interface
4. Fill in comprehensive document metadata:
- Document title and description
@ -136,16 +219,17 @@ The system uses color coding and icons to indicate access levels:
5. Review and confirm successful uploads
### Document Metadata
For each document, you can specify comprehensive metadata:
For each document, you can specify comprehensive metadata based on system configuration:
- **Title**: Descriptive document name/title
- **Description**: Brief summary of document content and purpose
- **Keywords**: Search terms for improved discoverability (comma-separated)
- **Category**: Document type (Technical Specification, Contract, Report, Policy, etc.)
- **Category**: Document type (configurable in DMS settings)
- **Status**: Current state (Draft, Under Review, Approved, Archived, etc.)
- **Department**: Responsible department or team
- **Access Level**: Determines who can view and interact with the document
- **Retention Period**: How long the document should be retained
- **Version**: Document version number and change notes
- **Custom Fields**: Additional metadata fields as configured by administrators
### Viewing Documents
1. Click on any document to view its details and metadata
@ -158,392 +242,218 @@ For each document, you can specify comprehensive metadata:
- **Move Documents**: Drag and drop to reorganize into different locations
- **Delete Documents**: Remove documents with confirmation and audit trail
- **Copy Documents**: Duplicate documents to multiple locations
- **Version Control**: Track document versions and view change history
- **Version Control**: Track document versions and view change history (if enabled)
- **Share Documents**: Generate sharing links and manage access permissions
## Access Control
### Understanding Access Levels
#### Public Documents
- Visible and accessible to all system users
- No special permissions or approvals required
- Green color indicator for easy identification
- Suitable for general information and policies
#### Private Documents
- Restricted access requiring specific permissions
- Red color indicator with lock icon
- May require access request and approval workflow
- Ideal for sensitive or confidential information
#### Personal Documents
- Documents owned and controlled by individual users
- Blue color indicator showing ownership
- Full control over sharing and permissions
- Private workspace for individual document management
### Requesting Access
When you encounter a private document you cannot access:
1. **Identify Restricted Item**: Look for red color coding and lock icon
2. **Initiate Access Request**: Click the "Request Access" button
3. **Complete Request Form**:
- **Document Information**: Pre-filled with document title and file details
- **Access Type**: Choose appropriate permission level:
- View Only: Read-only access to document content
- Download: View and download permissions for offline access
- Print: View and print permissions for physical copies
- Full Access: Complete access rights including editing
- **Access Duration**: Select appropriate timeframe:
- 7 days, 14 days, 30 days, 60 days, 90 days, or Permanent
- **Justification**: Detailed explanation of why access is needed (required field)
4. **Submit Request**: Click "Submit Request" to send for approval
5. **Monitor Status**: Track request status and wait for approval from document owner or administrator
### Access Request Status
- **Pending**: Request submitted and awaiting review by approvers
- **Approved**: Access granted for specified duration and permission level
- **Rejected**: Request denied with explanation from approver
- **Expired**: Previously approved access has reached its expiration date
## Search and Filtering
### Basic Search
1. Use the search box in the top navigation bar
2. Enter keywords, document titles, or content descriptions
3. Press Enter or click the search icon to execute search
4. Results display with matching documents and highlighted search terms
5. Refine results using additional filters and sorting options
### Advanced Search Options
- **Filter by Type**: Documents, folders, cabinets, or specific file formats
- **Filter by Category**: Technical, Administrative, Financial, HR, etc.
- **Filter by Date Range**: Creation date, modification date, or access date
- **Filter by Owner**: Documents by specific users or departments
- **Filter by Access Level**: Public, private, personal, or template documents
- **Filter by Status**: Draft, approved, archived, or under review
- **Filter by File Format**: PDF, Word, Excel, images, etc.
### Search Tips for Better Results
- Use quotation marks for exact phrase matching
- Combine multiple keywords with spaces for broader results
- Search includes document titles, descriptions, metadata, and content (where supported)
- Use wildcards (*) for partial word matching
- Utilize Boolean operators (AND, OR, NOT) for complex searches
### Document Tabs for Quick Filtering
Filter documents by category using convenient tabs:
- **All**: Display all accessible documents in current location
- **Public**: Show only public documents available to all users
- **Private**: Display private documents you have access to
- **Personal**: Show your personal documents and workspace items
## Document Viewer
### Supported File Formats
The built-in document viewer supports multiple formats:
- **PDF Documents**: Full preview with zoom, navigation, and text selection
- **Images**: JPG, PNG, GIF, BMP with zoom and pan functionality
- **Text Files**: TXT, RTF with formatted preview and search
- **Microsoft Office**: DOC, DOCX, XLS, XLSX, PPT, PPTX (basic preview)
- **Other Formats**: Download option provided for unsupported formats
### Viewer Features and Controls
- **Zoom Controls**: Zoom in/out from 25% to 400% for optimal readability
- **Page Navigation**: Navigate through multi-page documents with thumbnails
- **Full Screen Mode**: Maximize viewer for immersive document experience
- **Download Function**: Save document locally for offline access
- **Print Support**: Print document directly from viewer interface
- **Search in Document**: Find specific text within document content
- **Rotation Controls**: Rotate document pages for better viewing
### Viewer Interface Controls
- **Zoom Options**: 25%, 50%, 75%, 100%, 125%, 150%, 200%, Fit to Width, Fit to Page
- **Navigation**: Previous/Next page buttons, page number input, thumbnail sidebar
- **View Options**: Single page, continuous scroll, two-page spread
- **Tools**: Text selection, annotation tools (if enabled), measurement tools
- **Close**: Return to document list or previous view
## My Documents
### Personal Document Workspace
The "My Documents" section provides a personalized workspace showing:
- Documents you've created and own
- Documents you've recently accessed or modified
- Documents shared with you by other users
- Recent document activity and version history
- Personal folders and organizational structure
### Organization and Management Features
- **Quick Search**: Search specifically within your personal documents
- **Sort Options**: Sort by name, date created, date modified, file type, or size
- **View Modes**: Switch between list, grid, or detailed view layouts
- **Bulk Actions**: Select and manage multiple documents simultaneously
- **Folder Creation**: Create personal folders for document organization
- **Favorites**: Mark frequently used documents for quick access
### Personal Document Statistics and Insights
View comprehensive information about your document usage:
- Total number of documents owned and accessible
- Storage space used and available quota
- Recent upload activity and document creation trends
- Document categories and type distribution
- Most accessed documents and usage patterns
## Settings Administration
## DMS Settings (Administrators)
### Overview
The EDMS Settings module provides administrators with comprehensive control over system configuration and behavior. Access to settings is restricted to users with administrative privileges and affects system-wide operations.
Administrators have access to comprehensive DMS settings through the **DMS Settings** page, allowing complete customization of the document management system behavior.
### Accessing Settings
1. Navigate to **DMS****Settings** from the main navigation menu
2. Select a settings category from the left sidebar navigation
3. Configure settings within each category using standardized form components
4. Save changes to apply configurations across the system
### Accessing DMS Settings
1. Navigate to **DMS****Settings** in the main menu
2. Requires administrative privileges to access
3. Settings are organized into six main categories with tabbed navigation
### Settings Categories
The EDMS settings are organized into 5 core categories for streamlined administration:
#### 1. User & Access Management 🔐
- **User Roles**: Define and manage custom user roles (Admin, Editor, Viewer, Uploader, etc.)
- **Access Permissions**: Configure granular permissions for view, edit, delete, download, and share
- **Authentication Settings**:
- Enable/disable Single Sign-On (SSO)
- Require Multi-Factor Authentication (MFA)
- LDAP/Active Directory integration
- Session timeout configuration (1-24 hours)
#### 🔐 User & Access Management
Configure user permissions, roles, and authentication settings:
#### 2. Document & Folder Settings 📁
- **Naming Conventions**:
- Auto-generate document names
- Configure naming patterns (e.g., `{department}_{title}_{date}`)
- Set mandatory fields for document metadata
- **Version Control**:
- Enable/disable document versioning
- Set maximum number of versions to retain
- Configure automatic versioning behavior
**User Roles & Permissions**
- Define user roles (Admin, Editor, Viewer, Uploader)
- Set granular permissions for each role:
- View Documents: Read-only access to document content
- Edit Documents: Modify document metadata and properties
- Delete Documents: Remove documents with audit trail
- Download Documents: Save documents for offline access
- Share Documents: Generate sharing links and manage access
- Manage user groups by department or project
- Enable Role-Based Access Control (RBAC) system-wide
#### 3. Metadata & Tagging 📝
- **Custom Metadata Fields**:
- Add/remove custom fields (text, dropdown, date, number, multi-select)
- Set required fields for document uploads
- Configure field types and validation rules
- **Tagging System**:
- Manage predefined tags (urgent, confidential, public, draft, final, etc.)
- Enable/disable user-generated tags
- Configure tag suggestions and auto-completion
**Authentication Settings**
- Single Sign-On (SSO) integration with external providers
- Multi-Factor Authentication (MFA) enforcement
- LDAP/Active Directory integration for enterprise environments
- Session timeout configuration (1-24 hours)
- Password policies and security requirements
#### 4. Workflow & Automation 🔄
- **Approval Workflows**:
- Enable/disable approval workflows
- Configure default approval flow (department-head-approval, legal-review, etc.)
- Set up custom approval processes
- **Notifications**:
- Email notifications for document activities
- In-app notifications and alerts
- Upload alerts and deadline reminders
#### 📁 Document & Folder Settings
Configure document organization and structure:
#### 5. Upload & Storage Settings 📤
- **File Type Management**:
- Allowed file types (pdf, doc, docx, xls, xlsx, ppt, pptx, txt, jpg, png, etc.)
- Blocked file types (exe, bat, cmd, etc.)
- Configure MIME type validation
- **Storage Quotas**:
- Maximum file size limit (MB)
- Per-user storage quota
- Per-group storage quota
- Per-project storage quota
**Folder Hierarchy**
- Set maximum folder depth (1-10 levels)
- Choose default folder structure templates:
- Standard: General-purpose organization
- Project-based: Project-focused hierarchy
- Department-based: Organizational structure alignment
- Custom: User-defined structure
- Configure folder creation permissions and restrictions
**Document Naming Conventions**
- Enable automatic document name generation
- Define mandatory metadata fields for document creation
- Set naming patterns using variables:
- `{department}`: Document department/owner
- `{title}`: Document title or name
- `{date}`: Creation or upload date
- `{project}`: Project code or identifier
- `{category}`: Document category/type
**Document Retention & Version Control**
- Enable document retention policies with configurable retention periods
- Set automatic archiving before deletion
- Configure version control system:
- Maximum versions to retain (1-50)
- Automatic versioning on document updates
- Version history tracking and comparison tools
#### 📝 Metadata & Tagging
Configure document metadata and classification:
**Custom Metadata Fields**
Define custom fields for enhanced document organization:
- Field types: Text, Dropdown, Date, Number, Multi-select
- Required field enforcement for document uploads
- Department-specific metadata schemas
- Dynamic field validation and formatting rules
**Predefined Metadata Fields**
- Department: Organizational unit assignment
- Priority: Document importance level (Low, Medium, High, Critical)
- Project Code: Project or initiative identifier
- Review Date: Scheduled review or expiration date
**Tagging System**
- Configure predefined tags: urgent, confidential, public, draft, final
- Enable user-generated tags for flexible categorization
- Tag suggestion system for consistent tagging
- Auto-classification based on:
- Keyword recognition in document content
- Department-based classification rules
- File type and format analysis
#### 📤 Upload & Storage Settings
Configure file upload restrictions and storage management:
**File Type Management**
- **Allowed File Types**: Specify permitted file formats
- Default: pdf, doc, docx, xls, xlsx, ppt, pptx, txt, jpg, png
- Configurable list for organizational requirements
- **Blocked File Types**: Prevent potentially harmful files
- Default: exe, bat, cmd (executable files)
- Customizable blacklist for security compliance
**File Size & Storage Quotas**
- Maximum file size per upload (typically 100MB)
- User storage quotas (default: 5GB per user)
- Group storage quotas (default: 50GB per group)
- Project storage quotas (default: 100GB per project)
- System-wide storage monitoring and alerts
**Storage Configuration**
- Storage type selection (Local, AWS S3, Azure, Google Cloud)
- Storage path configuration for file organization
- Backup settings and automated backup scheduling
- Compression settings for storage optimization
#### 📅 System Settings
Configure global system behavior and preferences:
**General System Configuration**
- **System Timezone**: Set default timezone for timestamps
- Options: Asia/Kuala_Lumpur, UTC, America/New_York, Europe/London
- Affects document timestamps, scheduling, and user interface
- **Backup Schedule**: Configure automated system backups
- Options: Hourly, Daily, Weekly, Monthly
- Affects data protection and recovery capabilities
- **Log Level**: Set system logging verbosity
- Options: Debug, Info, Warning, Error
- Affects troubleshooting and system monitoring
**System Monitoring & Maintenance**
- **Maintenance Mode**: Enable system-wide maintenance mode
- Displays maintenance message to users
- Restricts access during system updates
- **Automatic Updates**: Configure automatic system updates
- Security patches and feature updates
- Scheduled during low-usage periods
- **System Monitoring**: Enable performance monitoring
- Track system performance metrics
- Monitor user activity and system health
- Generate usage reports and analytics
#### 6. System Settings 📅
- **General Configuration**:
- System timezone (Asia/Kuala_Lumpur, UTC, etc.)
- Backup schedule (hourly, daily, weekly, monthly)
- System log level (debug, info, warning, error)
- **System Maintenance**:
- Enable/disable maintenance mode
- Configure automatic updates
- System monitoring and performance metrics
### Settings Management Features
#### Import/Export Configuration
- **Export Settings**: Save current configuration as JSON file
- Backup settings before major changes
- Transfer settings between environments
- Documentation and compliance purposes
- **Import Settings**: Load configuration from JSON file
- Restore previous configurations
- Deploy settings across multiple systems
- Standardize configurations across environments
#### Save & Load Settings
- **Auto-save**: Settings are saved immediately when modified
- **Loading States**: Visual feedback during save operations
- **Error Handling**: Clear error messages if save fails
- **Success Confirmation**: Confirmation messages with auto-dismiss
#### Import/Export Functionality
- **Export Settings**: Download complete settings as JSON file for backup
- **Import Settings**: Upload JSON file to restore settings
- **Settings Migration**: Easy transfer between environments
- **Backup Management**: Regular backups of configuration
#### Reset to Defaults
- **Category Reset**: Reset individual setting categories to default values
- **System Reset**: Reset entire system configuration to factory defaults
- **Selective Reset**: Choose specific settings to reset while preserving others
- **One-click Reset**: Reset all settings to system defaults
- **Confirmation Dialog**: Prevents accidental resets
- **Selective Reset**: Reset individual setting categories (future enhancement)
- **Default Values**: Sensible defaults for all configuration options
#### Settings Validation
- **Real-time Validation**: Form validation prevents invalid configurations
- **Dependency Checking**: Settings are validated for interdependencies
- **Conflict Resolution**: System identifies and resolves setting conflicts
- **Preview Mode**: Test settings before applying system-wide
#### Dynamic Field Management
- **Add Custom Fields**: Create new metadata fields with validation
- **Remove Fields**: Delete unused custom fields
- **Reorder Fields**: Arrange custom fields in preferred order
- **Field Types**: Support for text, dropdown, date, number, and multi-select fields
### Best Practices for Settings Management
### Configuration Best Practices
#### Security Considerations
- Regularly review user permissions and access levels
- Enable MFA for administrative accounts
- Use strong session timeout values for security
- Regularly audit user roles and group memberships
#### Security Settings
- Enable MFA for sensitive environments
- Configure appropriate session timeouts
- Set up proper user roles and permissions
- Regularly review access control settings
#### Storage Management
- Monitor storage quotas and usage patterns
- Implement retention policies for compliance
- Regular backup verification and testing
- Plan for storage scaling based on usage growth
- Set realistic file size limits based on storage capacity
- Configure appropriate quotas for users and groups
- Enable compression for large files when possible
- Set up regular backup schedules
#### System Performance
- Monitor log levels to balance debugging needs with performance
- Schedule maintenance during off-peak hours
- Regular cleanup of temporary files and old versions
- Performance monitoring for system optimization
#### Workflow Optimization
- Configure approval workflows based on organizational structure
- Enable relevant notifications to keep users informed
- Set up appropriate retention policies
- Configure automatic tagging rules for efficiency
#### Documentation & Compliance
- Document all configuration changes with justification
- Maintain change logs for audit purposes
- Regular export of settings for backup and documentation
- Review settings for compliance with organizational policies
## Search and Filtering
### Troubleshooting Settings Issues
### Search Functionality
- **Global Search**: Search across all accessible documents
- **Tab-Specific Search**: Search within specific access level tabs
- **Keyword Search**: Search document titles, descriptions, and metadata
- **Advanced Filters**: Filter by date, document type, department, etc.
#### Common Settings Problems
- **Permission Conflicts**: Resolve conflicting role assignments
- **Upload Failures**: Check file type restrictions and size limits
- **Authentication Issues**: Verify SSO and authentication settings
- **Storage Problems**: Monitor quotas and storage allocation
### Search Tips
- Use quotation marks for exact phrase searches
- Combine multiple keywords for more specific results
- Use the predefined tags for quick filtering
- Take advantage of custom metadata fields for precise searches
#### Settings Recovery
- Use exported settings files to restore configurations
- Reset specific categories if issues persist
- Contact system administrator for complex configuration issues
- Review audit logs for recent configuration changes
## Document Viewer
### Supported File Types
The built-in document viewer supports:
- **PDF Documents**: Full PDF viewing with zoom and navigation
- **Microsoft Office**: Word, Excel, PowerPoint documents
- **Images**: JPG, PNG, GIF, and other common image formats
- **Text Files**: Plain text, CSV, and other text-based formats
### Viewer Features
- **Zoom Controls**: Zoom in/out for better readability
- **Page Navigation**: Navigate through multi-page documents
- **Full Screen Mode**: Distraction-free document viewing
- **Download Option**: Download documents for offline access
- **Print Support**: Direct printing from the viewer
## Troubleshooting
### Common Issues and Solutions
### Common Issues
#### Cannot Access Document
- **Check Access Level**: Verify document permissions and look for lock icons
- **Request Access**: Use the access request feature for private documents
- **Contact Administrator**: Reach out for urgent access needs or system issues
- **Verify Account Status**: Ensure your user account is active and properly configured
#### Documents Not Loading
1. Check your internet connection
2. Verify you have permission to access the document
3. Try refreshing the page or switching tabs
4. Contact administrator if the issue persists
#### Upload Problems
- **File Size Limits**: Check maximum file size restrictions (typically 50MB)
- **File Format Support**: Ensure file format is supported by the system
- **Network Connection**: Verify stable internet connection for large uploads
- **Upload Permissions**: Confirm you have upload permissions for the target location
- **Browser Issues**: Try clearing cache or using a different browser
#### Upload Failures
1. Check file size limits (configured in DMS settings)
2. Verify file type is allowed (check blocked file types in settings)
3. Ensure you have upload permissions in current location
4. Try uploading one file at a time if bulk upload fails
#### Search Not Working Properly
- **Check Spelling**: Verify search terms are spelled correctly
- **Try Alternative Keywords**: Use different or broader search terms
- **Clear Active Filters**: Remove any filters that might be limiting results
- **Refresh Application**: Reload the page to reset search functionality
- **Check Indexing Status**: Some documents may take time to be indexed for search
#### Search Not Working
1. Clear browser cache and cookies
2. Try different search terms or keywords
3. Check if you're searching in the correct access level tab
4. Verify documents exist in the location you're searching
#### Document Viewer Issues
- **Browser Compatibility**: Ensure you're using a supported modern browser
- **Enable JavaScript**: Verify JavaScript is enabled in browser settings
- **Clear Browser Cache**: Clear cache and cookies to resolve display issues
- **Plugin Requirements**: Install any required browser plugins for specific file types
- **Download Alternative**: Use download option if viewer is not functioning
#### Access Denied Errors
1. Verify you have appropriate permissions for the document/folder
2. Check if the document is in the correct access level category
3. Request access from document owner or administrator
4. Contact administrator if permissions seem incorrect
### Getting Help and Support
- **Contact IT Support**: Reach out to technical support for system issues
- **Check System Status**: Verify overall system availability and maintenance schedules
- **User Training**: Request additional training sessions for advanced features
- **Documentation**: Refer to technical guide for detailed system information
- **Community Resources**: Access user forums and knowledge bases if available
### Performance Issues
### Best Practices for Optimal System Use
1. **Systematic Organization**: Create and maintain clear, logical folder structures
2. **Descriptive Naming**: Use meaningful, descriptive document titles and filenames
3. **Regular Maintenance**: Periodically review and clean up outdated documents
4. **Backup Strategy**: Keep local copies of critical documents as backup
5. **Consistent Conventions**: Follow organizational naming and filing conventions
6. **Metadata Accuracy**: Keep document metadata current and accurate
7. **Access Control Respect**: Follow organizational policies for document sharing and access
8. **Version Management**: Use version control features to track document changes
9. **Security Awareness**: Be mindful of document sensitivity and appropriate access levels
10. **Regular Updates**: Stay informed about system updates and new features
#### Slow Loading
1. Check internet connection speed
2. Clear browser cache and temporary files
3. Close unnecessary browser tabs
4. Contact administrator about server performance
#### Browser Compatibility
- Use modern browsers (Chrome 70+, Firefox 65+, Safari 12+, Edge 79+)
- Enable JavaScript in browser settings
- Update browser to latest version
- Disable conflicting browser extensions
### Getting Help
#### Contact Information
- **System Administrator**: Contact your IT department
- **User Support**: Check internal documentation or help desk
- **Technical Issues**: Report to system administrator with:
- Error messages (exact text)
- Steps to reproduce the issue
- Browser and version information
- Screenshots if applicable
#### Best Practices
- **Regular Backups**: Keep local copies of important documents
- **Organize Properly**: Use consistent naming and folder structures
- **Tag Documents**: Use relevant tags for easy searching
- **Stay Updated**: Keep track of system updates and new features
- **Follow Security**: Use strong passwords and follow access control policies
This user guide provides comprehensive coverage of the EDMS interface and functionality, designed to help users effectively navigate and utilize the document management system. The system is designed for ease of use while providing powerful document management capabilities.
---

View File

@ -4,30 +4,61 @@ export default [
"description": "Document Management System",
"child": [
{
"title": "ERD",
"path": "/dms/erd",
"icon": "material-symbols:database-sharp",
"child": [],
"meta": {}
},
{
"title": "Document Management",
"title": "Document Explorer",
"path": "/dms",
"icon": "ic:outline-folder",
"child": [],
"meta": {}
},
{
"title": "Access Requests",
"path": "/dms/access-requests",
"icon": "ic:outline-security",
"title": "Admin Dashboard",
"path": "/dms/admin-dashboard",
"icon": "material-symbols:dashboard",
"child": [],
"meta": {
"auth": {
"role": ["admin", "superadmin"]
}
}
},
{
"title": "Access Management",
"path": "/dms/access-management",
"icon": "ic:baseline-security",
"child": [],
"meta": {
"auth": {
"role": ["admin", "superadmin"]
}
}
},
{
"title": "Role Management",
"path": "/dms/role-management",
"icon": "mdi:account-key",
"child": [],
"meta": {
"auth": {
"role": ["superadmin"]
}
}
},
{
"title": "Switch Role",
"path": "/dms/switch-roles",
"icon": "ic:outline-swap-horiz",
"child": []
},
{
"title": "Settings",
"path": "/dms/settings",
"icon": "ic:outline-settings",
"child": []
"child": [],
"meta": {
"auth": {
"role": ["admin", "superadmin"]
}
}
}
],
"meta": {}
@ -99,7 +130,7 @@ export default [
"meta": {
"auth": {
"role": [
"Developer"
"superadmin"
]
}
}

View File

@ -8,14 +8,11 @@ import RsCollapseItem from "../../../components/RsCollapseItem.vue";
import RsDropdown from "../../../components/RsDropdown.vue";
import RsDropdownItem from "../../../components/RsDropdownItem.vue";
import RsFieldset from "../../../components/RsFieldset.vue";
import RsInput from "../../../components/RsInput.vue";
import RsModal from "../../../components/RsModal.vue";
import RsProgressBar from "../../../components/RsProgressBar.vue";
import RsSelect from "../../../components/RsSelect.vue";
import RsTab from "../../../components/RsTab.vue";
import RsTabItem from "../../../components/RsTabItem.vue";
import RsTable from "../../../components/RsTable.vue";
import RsTextarea from "../../../components/RsTextarea.vue";
import RsWizard from "../../../components/RsWizard.vue";
export {
@ -29,13 +26,10 @@ export {
RsDropdown,
RsDropdownItem,
RsFieldset,
RsInput,
RsModal,
RsProgressBar,
RsSelect,
RsTab,
RsTabItem,
RsTable,
RsTextarea,
RsWizard,
};

View File

@ -0,0 +1,236 @@
<script setup>
import { ref, computed, onMounted, defineAsyncComponent } from 'vue';
import { useDmsStore } from '~/stores/dms';
// Define page metadata
definePageMeta({
title: "Access Management",
middleware: ["auth"],
requiresAuth: true,
breadcrumb: [
{
name: "DMS",
path: "/dms",
},
{
name: "Access Management",
path: "/dms/access-management",
},
],
});
// Lazy load components to improve initial page load
const DMSApprovalQueue = defineAsyncComponent(() =>
import('~/components/dms/workflows/DMSApprovalQueue.vue')
);
const DMSAccessRequestTracker = defineAsyncComponent(() =>
import('~/components/dms/workflows/DMSAccessRequestTracker.vue')
);
// Store
const dmsStore = useDmsStore();
// Component state
const activeTab = ref('requests');
const isLoading = ref(true);
const hasError = ref(false);
const errorMessage = ref('');
// User role and permissions
const userPermissions = ref({
canApprove: false,
canReject: false,
canViewAll: false
});
// Tabs definition
const tabs = [
{
id: 'requests',
label: 'Access Requests',
icon: 'lock-access',
description: 'Manage pending and historical access requests'
},
{
id: 'metrics',
label: 'Performance Metrics',
icon: 'chart',
description: 'Track access request KPIs and time metrics'
}
];
// Methods
const changeTab = (tabId) => {
activeTab.value = tabId;
};
// Load user permissions
const loadUserPermissions = async () => {
try {
isLoading.value = true;
// In a real implementation, this would be fetched from an authentication service
// For now we'll simulate it with the store
const userId = dmsStore.currentUser.id;
const userRole = dmsStore.currentUser.role;
// Get permissions from the store (simulated Authentik integration)
const permissions = await dmsStore.getRbacPermissions(userId);
// Set the permissions
userPermissions.value = {
canApprove: permissions.permissions.accessRequests.approve || false,
canReject: permissions.permissions.accessRequests.reject || false,
canViewAll: permissions.permissions.accessRequests.viewAll || false
};
} catch (error) {
console.error('Failed to load user permissions:', error);
hasError.value = true;
errorMessage.value = 'Failed to load user permissions. Please try again.';
} finally {
isLoading.value = false;
}
};
// Get SVG icon
const getSvgIcon = (iconName) => {
const icons = {
'lock-access': `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"></path></svg>`,
'chart': `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="20" x2="18" y2="10"></line><line x1="12" y1="20" x2="12" y2="4"></line><line x1="6" y1="20" x2="6" y2="14"></line></svg>`,
'user': `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path><circle cx="12" cy="7" r="4"></circle></svg>`
};
return icons[iconName] || '';
};
// Lifecycle hooks
onMounted(() => {
loadUserPermissions();
});
</script>
<template>
<div class="dms-access-management">
<LayoutsBreadcrumb />
<rs-card class="h-full">
<template #body>
<div class="h-full flex flex-col">
<!-- Loading State -->
<div v-if="isLoading" class="flex items-center justify-center h-full">
<div class="text-center">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-4"></div>
<p class="text-gray-600 dark:text-gray-400">Loading access management...</p>
</div>
</div>
<!-- Error State -->
<div v-else-if="hasError" class="flex items-center justify-center h-full">
<div class="text-center p-6">
<div class="text-red-500 text-5xl mb-4"></div>
<h2 class="text-xl font-semibold text-red-600 mb-2">Error Loading Access Management</h2>
<p class="text-gray-600 dark:text-gray-400 mb-4">{{ errorMessage }}</p>
<rs-button @click="loadUserPermissions" variant="primary">
Retry
</rs-button>
</div>
</div>
<!-- Main Content -->
<div v-else class="h-full flex flex-col">
<!-- Header with user information -->
<div class="bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
<div class="px-6 py-4 flex items-center justify-between">
<div class="flex items-center">
<div class="w-10 h-10 rounded-full bg-blue-100 dark:bg-blue-900/30 flex items-center justify-center mr-3">
<span v-html="getSvgIcon('user')" class="text-blue-600 dark:text-blue-400"></span>
</div>
<div>
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">{{ dmsStore.currentUser.name }}</h2>
<p class="text-sm text-gray-600 dark:text-gray-400">{{ dmsStore.currentUser.department }}</p>
</div>
</div>
<div class="flex items-center space-x-2">
<span
v-if="userPermissions.canApprove"
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300"
>
Approver
</span>
<span
v-if="userPermissions.canViewAll"
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300"
>
Administrator
</span>
</div>
</div>
</div>
<!-- Tabs Navigation -->
<div class="border-b border-gray-200 dark:border-gray-700">
<div class="px-6">
<nav class="-mb-px flex space-x-6" aria-label="Tabs">
<button
v-for="tab in tabs"
:key="tab.id"
@click="changeTab(tab.id)"
class="py-4 px-1 border-b-2 font-medium text-sm transition-colors"
:class="[
activeTab === tab.id
? 'border-blue-500 text-blue-600 dark:border-blue-400 dark:text-blue-400'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-200 dark:hover:border-gray-600'
]"
>
<div class="flex items-center">
<span v-html="getSvgIcon(tab.icon)" class="mr-2"></span>
<span>{{ tab.label }}</span>
</div>
</button>
</nav>
</div>
</div>
<!-- Tab Description -->
<div class="bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 px-6 py-2">
<p class="text-sm text-gray-600 dark:text-gray-400">
{{ tabs.find(t => t.id === activeTab)?.description }}
</p>
</div>
<!-- Tab Content -->
<div class="flex-1 p-6 overflow-auto">
<!-- Access Requests Tab -->
<div v-if="activeTab === 'requests'" class="h-full">
<DMSApprovalQueue
:showClosed="userPermissions.canViewAll"
:filterByUser="userPermissions.canViewAll ? null : dmsStore.currentUser.id"
:maxItems="0"
/>
</div>
<!-- Metrics Tab -->
<div v-else-if="activeTab === 'metrics'" class="h-full">
<DMSAccessRequestTracker
:userId="userPermissions.canViewAll ? null : dmsStore.currentUser.id"
:timeRange="'30days'"
:showPersonal="true"
:showDepartmental="userPermissions.canViewAll"
/>
</div>
</div>
</div>
</div>
</template>
</rs-card>
</div>
</template>
<style scoped>
.dms-access-management {
height: calc(100vh - 64px);
}
</style>

View File

@ -0,0 +1,541 @@
<script setup>
import { ref, computed, onMounted } from 'vue';
import { useDmsStore } from '~/stores/dms';
// Define page metadata
definePageMeta({
title: "Admin Dashboard",
middleware: ["auth"],
requiresAuth: true,
breadcrumb: [
{ name: "DMS", path: "/dms" },
{ name: "Admin Dashboard", path: "/dms/admin-dashboard" }
],
});
// Store
const dmsStore = useDmsStore();
// Component state
const isLoading = ref(true);
const timeRange = ref('30days');
const accessRequests = ref([]);
const deptRequests = ref([]);
// Time range options
const timeRangeOptions = [
{ id: '7days', label: 'Last 7 Days' },
{ id: '30days', label: 'Last 30 Days' },
{ id: '90days', label: 'Last 90 Days' },
{ id: '365days', label: 'Last Year' }
];
// Check if user is admin
const isAdmin = computed(() => {
return dmsStore.currentUser.role === 'admin';
});
// Calculate KPIs
const kpis = computed(() => {
if (!accessRequests.value.length) return null;
const total = accessRequests.value.length;
const pending = accessRequests.value.filter(req => req.status === 'pending').length;
const approved = accessRequests.value.filter(req => req.status === 'approved').length;
const rejected = accessRequests.value.filter(req => req.status === 'rejected').length;
// Calculate response times
const resolvedRequests = accessRequests.value.filter(
req => req.status === 'approved' || req.status === 'rejected'
);
let totalResponseTime = 0;
let fastestResponse = Infinity;
let slowestResponse = 0;
resolvedRequests.forEach(req => {
const requestDate = new Date(req.requestDate);
const responseDate = new Date(req.responseDate);
const responseTimeHours = (responseDate - requestDate) / (1000 * 60 * 60);
totalResponseTime += responseTimeHours;
fastestResponse = Math.min(fastestResponse, responseTimeHours);
slowestResponse = Math.max(slowestResponse, responseTimeHours);
});
const avgResponseTime = resolvedRequests.length > 0
? totalResponseTime / resolvedRequests.length
: 0;
// Calculate overdue
const overdueRequests = accessRequests.value.filter(req => {
if (req.status !== 'pending') return false;
const deadline = new Date(req.targetResolutionTime);
const now = new Date();
return now > deadline;
}).length;
const overduePercentage = total > 0
? (overdueRequests / total) * 100
: 0;
// Calculate SLA compliance
const slaCompliance = resolvedRequests.length > 0
? (resolvedRequests.filter(req => {
const requestDate = new Date(req.requestDate);
const responseDate = new Date(req.responseDate);
const targetDate = new Date(req.targetResolutionTime);
return responseDate <= targetDate;
}).length / resolvedRequests.length) * 100
: 0;
// Approval rate
const approvalRate = resolvedRequests.length > 0
? (approved / resolvedRequests.length) * 100
: 0;
return {
total,
pending,
approved,
rejected,
avgResponseTime,
fastestResponse: fastestResponse === Infinity ? 0 : fastestResponse,
slowestResponse,
overdueRequests,
overduePercentage,
slaCompliance,
approvalRate
};
});
// Department performance metrics
const deptPerformance = computed(() => {
if (!deptRequests.value.length) return [];
// Group by department
const departments = {};
deptRequests.value.forEach(req => {
const dept = req.approverDepartment || 'Unassigned';
if (!departments[dept]) {
departments[dept] = {
name: dept,
total: 0,
approved: 0,
rejected: 0,
pending: 0,
avgResponseHours: 0,
responseCount: 0,
totalResponseHours: 0,
overdueCount: 0
};
}
const deptData = departments[dept];
deptData.total++;
if (req.status === 'approved') deptData.approved++;
if (req.status === 'rejected') deptData.rejected++;
if (req.status === 'pending') deptData.pending++;
// Calculate response time
if (req.responseDate) {
const requestDate = new Date(req.requestDate);
const responseDate = new Date(req.responseDate);
const responseTimeHours = (responseDate - requestDate) / (1000 * 60 * 60);
deptData.totalResponseHours += responseTimeHours;
deptData.responseCount++;
}
// Check if overdue
if (req.status === 'pending') {
const deadline = new Date(req.targetResolutionTime);
const now = new Date();
if (now > deadline) {
deptData.overdueCount++;
}
}
});
// Calculate averages and format data
return Object.values(departments).map(dept => {
dept.avgResponseHours = dept.responseCount > 0
? dept.totalResponseHours / dept.responseCount
: 0;
dept.overduePercentage = dept.total > 0
? (dept.overdueCount / dept.total) * 100
: 0;
dept.approvalRate = (dept.approved + dept.rejected) > 0
? (dept.approved / (dept.approved + dept.rejected)) * 100
: 0;
return dept;
}).sort((a, b) => b.total - a.total);
});
// Format helpers
const formatDuration = (hours) => {
if (hours < 1) {
return `${Math.round(hours * 60)} minutes`;
} else if (hours < 24) {
return `${Math.round(hours)} hours`;
} else {
return `${Math.round(hours / 24)} days`;
}
};
const formatPercentage = (value) => {
return `${Math.round(value)}%`;
};
// Load data
const loadData = async () => {
isLoading.value = true;
try {
// Get the store instance
const store = useDmsStore();
// Load access metrics based on selected time range
const metrics = await store.getAccessRequestMetrics(timeRange.value);
// Load all access requests
const [requests, departmentReqs] = await Promise.all([
store.getAccessRequests(),
store.getDepartmentAccessRequests()
]);
accessRequests.value = requests;
deptRequests.value = departmentReqs;
} catch (error) {
console.error('Failed to load dashboard data:', error);
} finally {
isLoading.value = false;
}
};
// Update time range
const updateTimeRange = (range) => {
timeRange.value = range;
loadData();
};
// Lifecycle
onMounted(() => {
loadData();
});
</script>
<template>
<div class="admin-dashboard">
<LayoutsBreadcrumb />
<rs-card class="h-full">
<template #body>
<div class="h-full flex flex-col overflow-hidden">
<!-- Admin check -->
<div v-if="!isAdmin" class="flex-1 flex items-center justify-center overflow-hidden">
<div class="text-center p-6 max-w-md">
<div class="text-red-500 text-5xl mb-4"></div>
<h2 class="text-xl font-semibold text-red-600 mb-2">Admin Access Required</h2>
<p class="text-gray-600 dark:text-gray-400 mb-4">
You need administrator privileges to access this dashboard.
</p>
<NuxtLink to="/dms/switch-roles" class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md text-sm font-medium inline-block">
Switch Role
</NuxtLink>
</div>
</div>
<!-- Admin Dashboard -->
<template v-else>
<!-- Dashboard Header -->
<div class="bg-indigo-50 dark:bg-indigo-900/20 px-6 py-4 border-b border-indigo-200 dark:border-indigo-800 flex-shrink-0">
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>
<h1 class="text-xl font-semibold text-indigo-900 dark:text-indigo-100">
Access Request Performance Dashboard
</h1>
<p class="text-indigo-700 dark:text-indigo-300 text-sm">
Monitor approval KPIs, response times, and department performance
</p>
</div>
<!-- Time Range Selector -->
<div class="flex items-center self-end sm:self-center">
<div class="inline-flex rounded-md shadow-sm mr-2">
<button
v-for="option in timeRangeOptions"
:key="option.id"
@click="updateTimeRange(option.id)"
class="relative inline-flex items-center px-3 py-1.5 text-sm font-medium border transition-colors whitespace-nowrap"
:class="[
timeRange === option.id
? 'bg-indigo-100 border-indigo-300 text-indigo-800 dark:bg-indigo-900/20 dark:border-indigo-700 dark:text-indigo-300 z-10'
: 'bg-white border-gray-300 text-gray-700 hover:bg-gray-50 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300',
option.id === '7days' ? 'rounded-l-md' : '',
option.id === '365days' ? 'rounded-r-md' : ''
]"
>
{{ option.label }}
</button>
</div>
<button
@click="loadData"
class="inline-flex items-center px-3 py-1.5 border border-gray-300 dark:border-gray-600 rounded-md text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
Refresh
</button>
</div>
</div>
</div>
<!-- Loading State -->
<div v-if="isLoading" class="flex-1 flex items-center justify-center overflow-hidden">
<div class="text-center">
<div class="animate-spin rounded-full h-10 w-10 border-b-2 border-indigo-600 mx-auto mb-4"></div>
<p class="text-gray-600 dark:text-gray-400">Loading dashboard data...</p>
</div>
</div>
<!-- Dashboard Content -->
<div v-else class="flex-1 overflow-auto p-4 md:p-6">
<!-- Navigation Buttons -->
<div class="flex flex-wrap justify-end mb-4 gap-2">
<NuxtLink
to="/dms"
class="inline-flex items-center px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
</svg>
Return to DMS
</NuxtLink>
<NuxtLink
to="/dms/switch-roles"
class="inline-flex items-center px-3 py-2 border border-indigo-300 dark:border-indigo-600 rounded-md text-sm font-medium text-indigo-700 dark:text-indigo-300 bg-white dark:bg-gray-800 hover:bg-indigo-50 dark:hover:bg-indigo-900/10"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
Switch Role
</NuxtLink>
</div>
<!-- KPI Summary Cards -->
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
<!-- Total Requests -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-4 border border-gray-200 dark:border-gray-700">
<div class="flex items-center">
<div class="w-12 h-12 rounded-lg bg-blue-100 dark:bg-blue-900/20 flex items-center justify-center mr-4">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-blue-600 dark:text-blue-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
</div>
<div>
<div class="text-2xl font-bold text-gray-900 dark:text-gray-100">{{ kpis?.total || 0 }}</div>
<div class="text-sm text-gray-500 dark:text-gray-400">Total Requests</div>
</div>
</div>
</div>
<!-- Pending Requests -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-4 border border-gray-200 dark:border-gray-700">
<div class="flex items-center">
<div class="w-12 h-12 rounded-lg bg-yellow-100 dark:bg-yellow-900/20 flex items-center justify-center mr-4">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-yellow-600 dark:text-yellow-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<div>
<div class="text-2xl font-bold text-gray-900 dark:text-gray-100">{{ kpis?.pending || 0 }}</div>
<div class="text-sm text-gray-500 dark:text-gray-400">Pending Requests</div>
</div>
</div>
</div>
<!-- Avg Response Time -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-4 border border-gray-200 dark:border-gray-700">
<div class="flex items-center">
<div class="w-12 h-12 rounded-lg bg-green-100 dark:bg-green-900/20 flex items-center justify-center mr-4">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-green-600 dark:text-green-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
</div>
<div>
<div class="text-2xl font-bold text-gray-900 dark:text-gray-100">{{ formatDuration(kpis?.avgResponseTime || 0) }}</div>
<div class="text-sm text-gray-500 dark:text-gray-400">Avg Response Time</div>
</div>
</div>
</div>
<!-- SLA Compliance -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-4 border border-gray-200 dark:border-gray-700">
<div class="flex items-center">
<div class="w-12 h-12 rounded-lg bg-purple-100 dark:bg-purple-900/20 flex items-center justify-center mr-4">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-purple-600 dark:text-purple-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<div>
<div class="text-2xl font-bold text-gray-900 dark:text-gray-100">{{ formatPercentage(kpis?.slaCompliance || 0) }}</div>
<div class="text-sm text-gray-500 dark:text-gray-400">SLA Compliance</div>
</div>
</div>
</div>
</div>
<!-- Performance Metrics -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<!-- Response Time Metrics -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow border border-gray-200 dark:border-gray-700 overflow-hidden">
<div class="px-4 py-3 border-b border-gray-200 dark:border-gray-700">
<h2 class="font-medium text-gray-900 dark:text-gray-100">Response Time Metrics</h2>
</div>
<div class="p-4">
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div class="p-3 bg-blue-50 dark:bg-blue-900/10 rounded-lg border border-blue-100 dark:border-blue-800">
<div class="text-sm text-blue-700 dark:text-blue-300 mb-1">Average</div>
<div class="text-xl font-bold text-blue-900 dark:text-blue-100 truncate" title="{{ formatDuration(kpis?.avgResponseTime || 0) }}">{{ formatDuration(kpis?.avgResponseTime || 0) }}</div>
</div>
<div class="p-3 bg-green-50 dark:bg-green-900/10 rounded-lg border border-green-100 dark:border-green-800">
<div class="text-sm text-green-700 dark:text-green-300 mb-1">Fastest</div>
<div class="text-xl font-bold text-green-900 dark:text-green-100 truncate" title="{{ formatDuration(kpis?.fastestResponse || 0) }}">{{ formatDuration(kpis?.fastestResponse || 0) }}</div>
</div>
<div class="p-3 bg-red-50 dark:bg-red-900/10 rounded-lg border border-red-100 dark:border-red-800">
<div class="text-sm text-red-700 dark:text-red-300 mb-1">Slowest</div>
<div class="text-xl font-bold text-red-900 dark:text-red-100 truncate" title="{{ formatDuration(kpis?.slowestResponse || 0) }}">{{ formatDuration(kpis?.slowestResponse || 0) }}</div>
</div>
</div>
<!-- Time Trend Visualization Placeholder -->
<div class="mt-4 h-40 bg-gray-50 dark:bg-gray-700/30 rounded-lg border border-gray-200 dark:border-gray-600 flex items-center justify-center">
<p class="text-gray-500 dark:text-gray-400 text-sm">Response Time Trend Chart</p>
</div>
</div>
</div>
<!-- Approval Metrics -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow border border-gray-200 dark:border-gray-700 overflow-hidden">
<div class="px-4 py-3 border-b border-gray-200 dark:border-gray-700">
<h2 class="font-medium text-gray-900 dark:text-gray-100">Approval Metrics</h2>
</div>
<div class="p-4">
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div class="p-3 bg-green-50 dark:bg-green-900/10 rounded-lg border border-green-100 dark:border-green-800">
<div class="text-sm text-green-700 dark:text-green-300 mb-1">Approved</div>
<div class="text-xl font-bold text-green-900 dark:text-green-100 truncate" title="{{ kpis?.approved || 0 }}">{{ kpis?.approved || 0 }}</div>
</div>
<div class="p-3 bg-red-50 dark:bg-red-900/10 rounded-lg border border-red-100 dark:border-red-800">
<div class="text-sm text-red-700 dark:text-red-300 mb-1">Rejected</div>
<div class="text-xl font-bold text-red-900 dark:text-red-100 truncate" title="{{ kpis?.rejected || 0 }}">{{ kpis?.rejected || 0 }}</div>
</div>
<div class="p-3 bg-purple-50 dark:bg-purple-900/10 rounded-lg border border-purple-100 dark:border-purple-800">
<div class="text-sm text-purple-700 dark:text-purple-300 mb-1">Approval Rate</div>
<div class="text-xl font-bold text-purple-900 dark:text-purple-100 truncate" title="{{ formatPercentage(kpis?.approvalRate || 0) }}">{{ formatPercentage(kpis?.approvalRate || 0) }}</div>
</div>
</div>
<!-- Approval Visualization Placeholder -->
<div class="mt-4 h-40 bg-gray-50 dark:bg-gray-700/30 rounded-lg border border-gray-200 dark:border-gray-600 flex items-center justify-center">
<p class="text-gray-500 dark:text-gray-400 text-sm">Approval/Rejection Ratio Chart</p>
</div>
</div>
</div>
</div>
<!-- Department Performance Table -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow border border-gray-200 dark:border-gray-700 overflow-hidden">
<div class="px-4 py-3 border-b border-gray-200 dark:border-gray-700">
<h2 class="font-medium text-gray-900 dark:text-gray-100">Department Performance</h2>
</div>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead class="bg-gray-50 dark:bg-gray-900/20">
<tr>
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Department
</th>
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Total Requests
</th>
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Avg Response Time
</th>
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Approval Rate
</th>
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Pending
</th>
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Overdue %
</th>
</tr>
</thead>
<tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
<tr v-for="dept in deptPerformance" :key="dept.name" class="hover:bg-gray-50 dark:hover:bg-gray-700/30">
<td class="px-4 py-3 whitespace-nowrap">
<div class="text-sm font-medium text-gray-900 dark:text-gray-100 truncate max-w-[200px]" :title="dept.name">{{ dept.name }}</div>
</td>
<td class="px-4 py-3 whitespace-nowrap">
<div class="text-sm text-gray-900 dark:text-gray-100">{{ dept.total }}</div>
</td>
<td class="px-4 py-3 whitespace-nowrap">
<div class="text-sm text-gray-900 dark:text-gray-100">{{ formatDuration(dept.avgResponseHours) }}</div>
</td>
<td class="px-4 py-3 whitespace-nowrap">
<div class="text-sm text-gray-900 dark:text-gray-100">{{ formatPercentage(dept.approvalRate) }}</div>
</td>
<td class="px-4 py-3 whitespace-nowrap">
<div class="text-sm text-gray-900 dark:text-gray-100">{{ dept.pending }}</div>
</td>
<td class="px-4 py-3 whitespace-nowrap">
<div
class="text-sm font-medium"
:class="{
'text-red-600 dark:text-red-400': dept.overduePercentage > 10,
'text-yellow-600 dark:text-yellow-400': dept.overduePercentage > 0 && dept.overduePercentage <= 10,
'text-green-600 dark:text-green-400': dept.overduePercentage === 0
}"
>
{{ formatPercentage(dept.overduePercentage) }}
</div>
</td>
</tr>
<!-- Empty state -->
<tr v-if="deptPerformance.length === 0">
<td colspan="6" class="px-4 py-8 text-center">
<p class="text-gray-500 dark:text-gray-400">No department data available for the selected time period.</p>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</template>
</div>
</template>
</rs-card>
</div>
</template>
<style scoped>
.admin-dashboard {
height: calc(100vh - 64px);
}
</style>

127
pages/dms/check-role.vue Normal file
View File

@ -0,0 +1,127 @@
<script setup>
import { useDmsStore } from '~/stores/dms';
import { onMounted } from 'vue';
// Define page metadata
definePageMeta({
title: "Check User Role",
middleware: ["auth"],
requiresAuth: true,
breadcrumb: [
{
name: "DMS",
path: "/dms",
},
{
name: "Check Role",
path: "/dms/check-role",
},
],
});
// Get the DMS store
const dmsStore = useDmsStore();
// Get current user details
const currentUser = dmsStore.currentUser;
// Log user role to console on component mount
onMounted(() => {
console.log('---------------------------------------');
console.log('Current User Information:');
console.log('---------------------------------------');
console.log('Name:', currentUser.name);
console.log('Email:', currentUser.email);
console.log('Role:', currentUser.role);
console.log('Department:', currentUser.department);
console.log('User ID:', currentUser.id);
console.log('---------------------------------------');
// Check if user has admin permissions
const isAdmin = currentUser.role === 'admin';
console.log('Has Admin Privileges:', isAdmin ? 'YES' : 'NO');
// Get detailed permissions (async)
dmsStore.getRbacPermissions(currentUser.id).then(permissions => {
console.log('---------------------------------------');
console.log('Detailed Permissions:');
console.log('---------------------------------------');
console.log(JSON.stringify(permissions, null, 2));
console.log('---------------------------------------');
});
});
</script>
<template>
<div class="check-role-page">
<LayoutsBreadcrumb />
<rs-card class="h-full">
<template #body>
<div class="p-6">
<h1 class="text-2xl font-semibold mb-4">User Role Information</h1>
<div class="bg-blue-50 dark:bg-blue-900/10 rounded-lg p-5 mb-6 border border-blue-200 dark:border-blue-800">
<div class="flex items-center mb-4">
<div class="w-12 h-12 rounded-full bg-blue-100 dark:bg-blue-900/30 flex items-center justify-center mr-4">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-blue-600 dark:text-blue-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
</div>
<div>
<h2 class="text-xl font-medium text-blue-900 dark:text-blue-100">{{ currentUser.name }}</h2>
<p class="text-blue-700 dark:text-blue-300">{{ currentUser.email }}</p>
</div>
</div>
<div class="grid grid-cols-2 gap-4 mb-4">
<div class="bg-white dark:bg-gray-800 rounded-lg p-4 border border-blue-100 dark:border-blue-900/20">
<h3 class="text-sm font-medium text-gray-500 dark:text-gray-400 mb-1">Role</h3>
<p class="text-lg font-semibold text-blue-700 dark:text-blue-400">{{ currentUser.role }}</p>
</div>
<div class="bg-white dark:bg-gray-800 rounded-lg p-4 border border-blue-100 dark:border-blue-900/20">
<h3 class="text-sm font-medium text-gray-500 dark:text-gray-400 mb-1">Department</h3>
<p class="text-lg font-semibold text-gray-700 dark:text-gray-300">{{ currentUser.department }}</p>
</div>
</div>
<div class="bg-gray-50 dark:bg-gray-800 rounded-lg p-4 border border-gray-200 dark:border-gray-700">
<div class="flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-gray-400 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<p class="text-sm text-gray-600 dark:text-gray-400">Role information has been logged to the browser console. Open Developer Tools (F12) and check the console tab for detailed permission information.</p>
</div>
</div>
</div>
<div class="mt-6">
<h3 class="text-lg font-medium mb-3">Instructions</h3>
<ol class="list-decimal list-inside space-y-2 text-gray-700 dark:text-gray-300">
<li>Press <kbd class="px-2 py-1 bg-gray-100 dark:bg-gray-800 rounded border border-gray-300 dark:border-gray-600 text-sm">F12</kbd> to open Developer Tools</li>
<li>Navigate to the <strong>Console</strong> tab</li>
<li>View your detailed role and permissions information</li>
</ol>
</div>
<div class="mt-6 flex justify-end">
<NuxtLink to="/dms" class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md text-sm font-medium transition-colors">
Back to DMS
</NuxtLink>
</div>
</div>
</template>
</rs-card>
</div>
</template>
<style scoped>
.check-role-page {
height: calc(100vh - 64px);
}
kbd {
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
}
</style>

View File

@ -1,277 +0,0 @@
<script setup>
import { ref } from 'vue';
const examples = ref({
input: 'Sample text',
select: 'option2',
textarea: 'Sample textarea content',
error: '',
disabled: false
});
const selectOptions = [
{ value: 'option1', label: 'Option 1' },
{ value: 'option2', label: 'Option 2' },
{ value: 'option3', label: 'Option 3' }
];
const buttonVariants = ['primary', 'secondary', 'info', 'success', 'warning', 'danger'];
const buttonSizes = ['sm', 'md', 'lg'];
</script>
<template>
<div class="design-system-page p-6">
<LayoutsBreadcrumb />
<rs-card>
<template #header>
<h1 class="text-2xl font-bold">🎨 DMS Design System</h1>
</template>
<template #body>
<div class="space-y-8">
<!-- Design Principles -->
<section>
<h2 class="text-xl font-semibold mb-4">Design Principles</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<rs-card>
<template #body>
<h3 class="font-medium mb-2">🎯 Consistency</h3>
<p class="text-sm text-gray-600">All components follow the same design patterns and naming conventions</p>
</template>
</rs-card>
<rs-card>
<template #body>
<h3 class="font-medium mb-2">🔧 Modularity</h3>
<p class="text-sm text-gray-600">Components are reusable and can be composed together</p>
</template>
</rs-card>
<rs-card>
<template #body>
<h3 class="font-medium mb-2">🌙 Dark Mode</h3>
<p class="text-sm text-gray-600">All components support both light and dark themes</p>
</template>
</rs-card>
</div>
</section>
<!-- Buttons -->
<section>
<h2 class="text-xl font-semibold mb-4">Buttons</h2>
<div class="space-y-4">
<div>
<h3 class="font-medium mb-2">Variants</h3>
<div class="flex flex-wrap gap-2">
<rs-button v-for="variant in buttonVariants" :key="variant" :variant="variant">
{{ variant.charAt(0).toUpperCase() + variant.slice(1) }}
</rs-button>
</div>
</div>
<div>
<h3 class="font-medium mb-2">Sizes</h3>
<div class="flex flex-wrap gap-2 items-center">
<rs-button v-for="size in buttonSizes" :key="size" :size="size" variant="primary">
{{ size.toUpperCase() }}
</rs-button>
</div>
</div>
<div>
<h3 class="font-medium mb-2">States</h3>
<div class="flex flex-wrap gap-2">
<rs-button variant="primary">Normal</rs-button>
<rs-button variant="primary" :disabled="true">Disabled</rs-button>
</div>
</div>
</div>
</section>
<!-- Form Components -->
<section>
<h2 class="text-xl font-semibold mb-4">Form Components</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Input -->
<div>
<h3 class="font-medium mb-3">Input Field</h3>
<rs-input
v-model="examples.input"
label="Input Label"
placeholder="Enter text here..."
:required="true"
/>
<rs-input
v-model="examples.input"
label="With Error"
placeholder="Enter text here..."
error="This field is required"
/>
<rs-input
v-model="examples.input"
label="Disabled"
placeholder="Enter text here..."
:disabled="true"
/>
</div>
<!-- Select -->
<div>
<h3 class="font-medium mb-3">Select Dropdown</h3>
<rs-select
v-model="examples.select"
:options="selectOptions"
label="Select Option"
placeholder="Choose an option"
:required="true"
/>
<rs-select
v-model="examples.select"
:options="selectOptions"
label="Multiple Select"
:multiple="true"
/>
</div>
<!-- Textarea -->
<div class="md:col-span-2">
<h3 class="font-medium mb-3">Textarea</h3>
<rs-textarea
v-model="examples.textarea"
label="Message"
placeholder="Enter your message..."
:rows="4"
:required="true"
/>
</div>
</div>
</section>
<!-- Cards -->
<section>
<h2 class="text-xl font-semibold mb-4">Cards</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<rs-card>
<template #header>
Card with Header
</template>
<template #body>
This is a card with a header section.
</template>
</rs-card>
<rs-card>
<template #body>
This is a basic card with only body content.
</template>
</rs-card>
<rs-card>
<template #body>
This card has both body and footer.
</template>
<template #footer>
<rs-button size="sm" variant="primary">Action</rs-button>
</template>
</rs-card>
</div>
</section>
<!-- Modals -->
<section>
<h2 class="text-xl font-semibold mb-4">Modals</h2>
<p class="text-gray-600 mb-4">Modals should use RsModal component with standardized header, body, and footer structure.</p>
<div class="bg-gray-100 dark:bg-gray-800 p-4 rounded-lg">
<pre class="text-sm"><code>&lt;rs-modal :visible="showModal" @close="closeModal"&gt;
&lt;template #header&gt;
Modal Title
&lt;/template&gt;
&lt;template #body&gt;
Modal content here
&lt;/template&gt;
&lt;template #footer&gt;
&lt;rs-button variant="secondary"&gt;Cancel&lt;/rs-button&gt;
&lt;rs-button variant="primary"&gt;Confirm&lt;/rs-button&gt;
&lt;/template&gt;
&lt;/rs-modal&gt;</code></pre>
</div>
</section>
<!-- Color System -->
<section>
<h2 class="text-xl font-semibold mb-4">Color System</h2>
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4">
<div class="text-center">
<div class="w-full h-12 bg-primary rounded mb-2"></div>
<span class="text-sm">Primary</span>
</div>
<div class="text-center">
<div class="w-full h-12 bg-secondary rounded mb-2"></div>
<span class="text-sm">Secondary</span>
</div>
<div class="text-center">
<div class="w-full h-12 bg-info rounded mb-2"></div>
<span class="text-sm">Info</span>
</div>
<div class="text-center">
<div class="w-full h-12 bg-success rounded mb-2"></div>
<span class="text-sm">Success</span>
</div>
<div class="text-center">
<div class="w-full h-12 bg-warning rounded mb-2"></div>
<span class="text-sm">Warning</span>
</div>
<div class="text-center">
<div class="w-full h-12 bg-danger rounded mb-2"></div>
<span class="text-sm">Danger</span>
</div>
</div>
</section>
<!-- Usage Guidelines -->
<section>
<h2 class="text-xl font-semibold mb-4">Usage Guidelines</h2>
<div class="space-y-4">
<rs-card>
<template #body>
<h3 class="font-medium mb-2"> Do</h3>
<ul class="list-disc pl-6 space-y-1 text-sm">
<li>Use RsButton, RsInput, RsSelect, RsTextarea for all form elements</li>
<li>Use RsModal for all dialog components</li>
<li>Follow consistent spacing and sizing patterns</li>
<li>Use semantic variant names (primary, secondary, danger, etc.)</li>
</ul>
</template>
</rs-card>
<rs-card>
<template #body>
<h3 class="font-medium mb-2"> Don't</h3>
<ul class="list-disc pl-6 space-y-1 text-sm">
<li>Use manual button styling with px-4 py-2 classes</li>
<li>Create custom input styles without using Rs components</li>
<li>Use hardcoded colors instead of CSS variables</li>
<li>Mix different component styling approaches</li>
</ul>
</template>
</rs-card>
</div>
</section>
</div>
</template>
</rs-card>
</div>
</template>
<style scoped>
pre {
overflow-x: auto;
}
code {
color: rgb(var(--text-color));
}
</style>

View File

@ -1,8 +1,9 @@
<script setup>
import { ref, computed, onMounted } from 'vue';
import { useDmsStore } from '~/stores/dms';
import DMSNavigation from '~/components/dms/navigation/DMSNavigation.vue';
import DMSExplorer from '~/components/dms/explorer/DMSExplorer.vue';
import { ref, onMounted, nextTick, computed } from 'vue';
import { useRouter } from 'vue-router';
// Router
const router = useRouter();
// Define page metadata
definePageMeta({
@ -17,122 +18,111 @@ definePageMeta({
],
});
// Set up store
const dmsStore = useDmsStore();
// Import DMS components dynamically to handle potential import errors
let DMSExplorer = null;
let useDmsStore = null;
// Local state
const showFileViewer = ref(false);
const currentDocument = ref(null);
const searchQuery = ref('');
const isSearching = ref(false);
const currentPath = ref('JKR Cawangan Kota Bharu, Kelantan');
const viewMode = ref('explorer'); // explorer, cabinets, list
const selectedItem = ref(null);
// Basic state
const activeTab = ref('all');
const isLoading = ref(true);
const hasError = ref(false);
const errorMessage = ref('');
const componentsLoaded = ref(false);
// File selection state
const selectedFiles = ref([]);
const isSelecting = ref(false);
// Toggle file selection
const toggleFileSelection = (file) => {
const index = selectedFiles.value.findIndex(f => f.id === file.id);
if (index === -1) {
selectedFiles.value.push(file);
} else {
selectedFiles.value.splice(index, 1);
// Tab definitions with icons
const tabs = [
{
id: 'all',
label: 'All Documents',
icon: 'folder',
color: 'blue'
},
{
id: 'public',
label: 'Public',
icon: 'unlock',
color: 'green'
},
{
id: 'private',
label: 'Private',
icon: 'lock',
color: 'red'
},
{
id: 'personal',
label: 'Personal',
icon: 'user',
color: 'purple'
}
};
// Clear selection
const clearSelection = () => {
selectedFiles.value = [];
isSelecting.value = false;
};
// Select all files
const selectAllFiles = () => {
selectedFiles.value = [...dmsStore.currentItems];
isSelecting.value = true;
};
// Check if a file is selected
const isFileSelected = (file) => {
return selectedFiles.value.some(f => f.id === file.id);
};
// Toggle view mode
const changeViewMode = (mode) => {
viewMode.value = mode;
};
// View a file
const viewFile = (file) => {
if (isSelecting.value) {
toggleFileSelection(file);
return;
}
currentDocument.value = file;
showFileViewer.value = true;
};
// Navigate to a location
const navigateTo = (path) => {
currentPath.value = path;
// In a real app, we would fetch the contents of this location
clearSelection();
};
// Search functionality
const handleSearch = async () => {
if (!searchQuery.value.trim()) return;
isSearching.value = true;
await dmsStore.searchDocuments(searchQuery.value);
isSearching.value = false;
};
// Clear search
const clearSearch = () => {
searchQuery.value = '';
dmsStore.clearSearch();
};
// Format file size
const formatFileSize = (size) => {
if (!size) return '0 B';
if (typeof size === 'string') {
// If already formatted (like "4MB"), return as is
if (size.endsWith('B')) return size;
// Try to parse the size if it's a number in string form
const parsed = parseFloat(size);
if (isNaN(parsed)) return size;
size = parsed;
}
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
let i = 0;
while (size >= 1024 && i < units.length - 1) {
size /= 1024;
i++;
}
return `${size.toFixed(2)} ${units[i]}`;
};
// Document category tabs
const documentTabs = [
{ id: 'all', label: 'All Documents', icon: 'folder' },
{ id: 'public', label: 'Public', icon: 'unlock' },
{ id: 'private', label: 'Private', icon: 'lock' },
{ id: 'personal', label: 'Personal', icon: 'user' }
];
// Handle events from explorer
// Change active tab
const changeTab = (tabId) => {
activeTab.value = tabId;
};
// Get SVG icon function
const getSvgIcon = (iconName, size = 20) => {
const icons = {
folder: `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg>`,
unlock: `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 9.9-1"></path></svg>`,
lock: `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"></path></svg>`,
user: `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path><circle cx="12" cy="7" r="4"></circle></svg>`
};
return icons[iconName] || icons.folder;
};
// Load components dynamically
const loadComponents = async () => {
try {
isLoading.value = true;
hasError.value = false;
// Import components
const dmsStoreModule = await import('~/stores/dms');
const dmsExplorerModule = await import('~/components/dms/explorer/DMSExplorer.vue');
useDmsStore = dmsStoreModule.useDmsStore;
DMSExplorer = dmsExplorerModule.default;
componentsLoaded.value = true;
await nextTick();
} catch (error) {
console.error('Failed to load DMS components:', error);
hasError.value = true;
errorMessage.value = `Failed to load DMS components: ${error.message}`;
} finally {
isLoading.value = false;
}
};
// Check if user is superadmin
const isSuperAdmin = computed(() => {
if (!useDmsStore) return false;
const store = useDmsStore();
return store.currentUser?.role === 'superadmin';
});
// Check if user is admin or superadmin
const isAdmin = computed(() => {
if (!useDmsStore) return false;
const store = useDmsStore();
return ['admin', 'superadmin'].includes(store.currentUser?.role);
});
// Add Admin Dashboard link for admins
const showAdminDashboard = computed(() => {
if (!useDmsStore) return false;
const store = useDmsStore();
return ['admin', 'superadmin'].includes(store.currentUser?.role);
});
// Event handlers (placeholder functions for when components load)
const handleItemSelected = (item) => {
selectedItem.value = item;
console.log('Item selected:', item);
};
const handleViewModeChanged = (mode) => {
@ -140,23 +130,12 @@ const handleViewModeChanged = (mode) => {
};
const handlePathChanged = (path) => {
currentPath.value = path;
};
// Get SVG icon
const getSvgIcon = (iconType, size = 16) => {
const icons = {
folder: `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg>`,
unlock: `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 9.9-1"></path></svg>`,
lock: `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"></path></svg>`,
user: `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path><circle cx="12" cy="7" r="4"></circle></svg>`
};
return icons[iconType] || icons.folder;
console.log('Path changed to:', path);
};
// Lifecycle hooks
onMounted(() => {
// Any initialization logic
loadComponents();
});
</script>
@ -167,34 +146,195 @@ onMounted(() => {
<rs-card class="h-full">
<template #body>
<div class="dms-layout h-full flex flex-col">
<!-- Document Category Tabs -->
<div class="tabs-header border-b border-gray-200 dark:border-gray-700 p-4 bg-white dark:bg-gray-800">
<div class="flex items-center space-x-1">
<button
v-for="tab in documentTabs"
:key="tab.id"
@click="activeTab = tab.id"
class="flex items-center space-x-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors"
:class="activeTab === tab.id
? 'bg-blue-100 text-blue-700 dark:bg-blue-900/20 dark:text-blue-300'
: 'text-gray-600 hover:text-gray-900 hover:bg-gray-100 dark:text-gray-400 dark:hover:text-gray-200 dark:hover:bg-gray-700'"
>
<span v-html="getSvgIcon(tab.icon, 16)"></span>
<span>{{ tab.label }}</span>
</button>
<!-- Loading State -->
<div v-if="isLoading" class="flex items-center justify-center h-full">
<div class="text-center">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-4"></div>
<p class="text-gray-600 dark:text-gray-400">Loading Document Management System...</p>
</div>
</div>
<!-- Explorer Component -->
<div class="explorer-container flex-1 overflow-hidden">
<DMSExplorer
:initial-path="'/'"
:view-mode="'list'"
:active-document-tab="activeTab"
@item-selected="handleItemSelected"
@view-mode-changed="handleViewModeChanged"
@path-changed="handlePathChanged"
/>
<!-- Error State -->
<div v-else-if="hasError" class="flex items-center justify-center h-full">
<div class="text-center p-6">
<div class="text-red-500 text-5xl mb-4"></div>
<h2 class="text-xl font-semibold text-red-600 mb-2">Error Loading DMS</h2>
<p class="text-gray-600 dark:text-gray-400 mb-4">{{ errorMessage }}</p>
<rs-button @click="loadComponents" variant="primary">
Retry
</rs-button>
</div>
</div>
<!-- Main Content -->
<div v-else class="dms-content h-full flex flex-col">
<!-- Enhanced Access Level Tabs -->
<div class="access-tabs bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
<div class="flex flex-col sm:flex-row sm:items-center justify-between px-4 sm:px-6 py-4 gap-3">
<div class="flex flex-wrap gap-1">
<button
v-for="tab in tabs"
:key="tab.id"
@click="changeTab(tab.id)"
class="access-tab flex items-center space-x-2 px-4 py-2 rounded-lg text-sm font-medium transition-all duration-200"
:class="[
activeTab === tab.id
? `bg-${tab.color}-100 text-${tab.color}-700 border border-${tab.color}-200 shadow-sm dark:bg-${tab.color}-900/20 dark:text-${tab.color}-300 dark:border-${tab.color}-800`
: 'text-gray-600 hover:text-gray-900 hover:bg-white dark:text-gray-400 dark:hover:text-gray-200 dark:hover:bg-gray-700 border border-transparent hover:border-gray-200 dark:hover:border-gray-600'
]"
>
<span
v-html="getSvgIcon(tab.icon)"
:class="[
activeTab === tab.id
? `text-${tab.color}-600 dark:text-${tab.color}-400`
: 'text-gray-500 dark:text-gray-500'
]"
></span>
<span>{{ tab.label }}</span>
<span
v-if="activeTab === tab.id"
:class="`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-${tab.color}-100 text-${tab.color}-800 dark:bg-${tab.color}-900/30 dark:text-${tab.color}-300`"
>
Active
</span>
</button>
</div>
<!-- Access Management Button - Moved to this row -->
<div class="flex flex-wrap gap-2">
<NuxtLink
to="/dms/access-management"
class="inline-flex items-center px-3 py-1.5 border border-gray-300 dark:border-gray-600 rounded-md text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
</svg>
Access Management
</NuxtLink>
<!-- Admin Dashboard Button (for admins only) -->
<NuxtLink
v-if="showAdminDashboard"
to="/dms/admin-dashboard"
class="inline-flex items-center px-3 py-1.5 border border-purple-300 dark:border-purple-600 rounded-md text-sm font-medium text-purple-700 dark:text-purple-300 bg-white dark:bg-gray-800 hover:bg-purple-50 dark:hover:bg-purple-900/10 transition-colors"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
Admin Dashboard
</NuxtLink>
<!-- Switch Role Button -->
<NuxtLink
to="/dms/switch-roles"
class="inline-flex items-center px-3 py-1.5 border border-indigo-300 dark:border-indigo-600 rounded-md text-sm font-medium text-indigo-700 dark:text-indigo-300 bg-white dark:bg-gray-800 hover:bg-indigo-50 dark:hover:bg-indigo-900/10 transition-colors"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
Switch Role
</NuxtLink>
</div>
</div>
</div>
<!-- Superadmin KPI Dashboard -->
<div v-if="isSuperAdmin" class="kpi-dashboard bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 px-4 sm:px-6 py-3 overflow-hidden">
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<div class="bg-white dark:bg-gray-700 rounded-lg p-3 border border-gray-200 dark:border-gray-600 shadow-sm">
<div class="flex items-center">
<div class="w-8 h-8 rounded-lg bg-blue-100 dark:bg-blue-900/20 flex items-center justify-center mr-3 flex-shrink-0">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-blue-600 dark:text-blue-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
</div>
<div class="overflow-hidden">
<div class="text-xl font-bold text-gray-900 dark:text-gray-100 truncate">450</div>
<div class="text-xs text-gray-600 dark:text-gray-400 truncate">Total Documents</div>
</div>
</div>
</div>
<div class="bg-white dark:bg-gray-700 rounded-lg p-3 border border-gray-200 dark:border-gray-600 shadow-sm">
<div class="flex items-center">
<div class="w-8 h-8 rounded-lg bg-green-100 dark:bg-green-900/20 flex items-center justify-center mr-3 flex-shrink-0">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-green-600 dark:text-green-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg>
</div>
<div class="overflow-hidden">
<div class="text-xl font-bold text-gray-900 dark:text-gray-100 truncate">12</div>
<div class="text-xs text-gray-600 dark:text-gray-400 truncate">New Documents Today</div>
</div>
</div>
</div>
<div class="bg-white dark:bg-gray-700 rounded-lg p-3 border border-gray-200 dark:border-gray-600 shadow-sm">
<div class="flex items-center">
<div class="w-8 h-8 rounded-lg bg-yellow-100 dark:bg-yellow-900/20 flex items-center justify-center mr-3 flex-shrink-0">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-yellow-600 dark:text-yellow-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" />
</svg>
</div>
<div class="overflow-hidden">
<div class="text-xl font-bold text-gray-900 dark:text-gray-100 truncate">5</div>
<div class="text-xs text-gray-600 dark:text-gray-400 truncate">Pending Access Requests</div>
</div>
</div>
</div>
<div class="bg-white dark:bg-gray-700 rounded-lg p-3 border border-gray-200 dark:border-gray-600 shadow-sm">
<div class="flex items-center">
<div class="w-8 h-8 rounded-lg bg-purple-100 dark:bg-purple-900/20 flex items-center justify-center mr-3 flex-shrink-0">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-purple-600 dark:text-purple-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
</div>
<div class="overflow-hidden">
<div class="text-xl font-bold text-gray-900 dark:text-gray-100 truncate">32</div>
<div class="text-xs text-gray-600 dark:text-gray-400 truncate">Active Users</div>
</div>
</div>
</div>
</div>
</div>
<!-- Content Area -->
<div class="content-area flex-1">
<!-- Action Buttons -->
<div class="px-6 py-3 bg-white dark:bg-gray-900/10 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between">
<div class="flex space-x-2">
<!-- Placeholder for future actions -->
</div>
<div class="flex space-x-2">
<!-- Role Management button has been removed -->
</div>
</div>
<!-- DMS Explorer Component -->
<component
v-if="componentsLoaded && DMSExplorer"
:is="DMSExplorer"
:initial-path="'/'"
:view-mode="'list'"
:active-document-tab="activeTab"
@item-selected="handleItemSelected"
@view-mode-changed="handleViewModeChanged"
@path-changed="handlePathChanged"
/>
<!-- Fallback Content -->
<div v-else class="text-center py-12">
<h2 class="text-2xl font-semibold mb-4">{{ tabs.find(t => t.id === activeTab)?.label }}</h2>
<p class="text-gray-600 dark:text-gray-400">
Document explorer is loading...
</p>
</div>
</div>
</div>
</div>
</template>
@ -211,16 +351,81 @@ onMounted(() => {
height: 100%;
}
.explorer-container {
.content-area {
min-height: 0;
overflow: hidden;
}
/* Ensure smooth transitions */
.tabs-header button {
transition: all 0.2s ease;
.dms-content {
height: 100%;
}
.tabs-header button:hover {
.access-tabs {
flex-shrink: 0;
}
.access-tab {
position: relative;
overflow: hidden;
}
.access-tab:hover {
transform: translateY(-1px);
}
.access-tab.active {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
/* Dynamic color classes */
.bg-blue-100 { background-color: rgb(219 234 254); }
.text-blue-700 { color: rgb(29 78 216); }
.border-blue-200 { border-color: rgb(191 219 254); }
.text-blue-600 { color: rgb(37 99 235); }
.bg-blue-100 { background-color: rgb(219 234 254); }
.text-blue-800 { color: rgb(30 64 175); }
.bg-green-100 { background-color: rgb(220 252 231); }
.text-green-700 { color: rgb(21 128 61); }
.border-green-200 { border-color: rgb(187 247 208); }
.text-green-600 { color: rgb(22 163 74); }
.text-green-800 { color: rgb(22 101 52); }
.bg-red-100 { background-color: rgb(254 226 226); }
.text-red-700 { color: rgb(185 28 28); }
.border-red-200 { border-color: rgb(254 202 202); }
.text-red-600 { color: rgb(220 38 38); }
.text-red-800 { color: rgb(153 27 27); }
.bg-purple-100 { background-color: rgb(243 232 255); }
.text-purple-700 { color: rgb(126 34 206); }
.border-purple-200 { border-color: rgb(233 213 255); }
.text-purple-600 { color: rgb(147 51 234); }
.text-purple-800 { color: rgb(107 33 168); }
/* Dark mode colors */
.dark .bg-blue-900\/20 { background-color: rgba(30, 58, 138, 0.2); }
.dark .text-blue-300 { color: rgb(147 197 253); }
.dark .border-blue-800 { border-color: rgb(30 64 175); }
.dark .text-blue-400 { color: rgb(96 165 250); }
.dark .bg-blue-900\/30 { background-color: rgba(30, 58, 138, 0.3); }
.dark .bg-green-900\/20 { background-color: rgba(20, 83, 45, 0.2); }
.dark .text-green-300 { color: rgb(134 239 172); }
.dark .border-green-800 { border-color: rgb(22 101 52); }
.dark .text-green-400 { color: rgb(74 222 128); }
.dark .bg-green-900\/30 { background-color: rgba(20, 83, 45, 0.3); }
.dark .bg-red-900\/20 { background-color: rgba(127, 29, 29, 0.2); }
.dark .text-red-300 { color: rgb(252 165 165); }
.dark .border-red-800 { border-color: rgb(153 27 27); }
.dark .text-red-400 { color: rgb(248 113 113); }
.dark .bg-red-900\/30 { background-color: rgba(127, 29, 29, 0.3); }
.dark .bg-purple-900\/20 { background-color: rgba(88, 28, 135, 0.2); }
.dark .text-purple-300 { color: rgb(196 181 253); }
.dark .border-purple-800 { border-color: rgb(107 33 168); }
.dark .text-purple-400 { color: rgb(168 85 247); }
.dark .bg-purple-900\/30 { background-color: rgba(88, 28, 135, 0.3); }
</style>

View File

@ -0,0 +1,585 @@
<script setup>
import { ref, onMounted } from 'vue';
import { useDmsStore } from '~/stores/dms';
// Define page metadata
definePageMeta({
title: "Role Management",
middleware: ["auth"],
requiresAuth: true,
breadcrumb: [
{
name: "DMS",
path: "/dms",
},
{
name: "Role Management",
path: "/dms/role-management",
},
],
});
// Store
const dmsStore = useDmsStore();
// State variables
const isLoading = ref(true);
const users = ref([]);
const roles = ref([]);
const activeTab = ref('users');
const searchQuery = ref('');
const hasError = ref(false);
const errorMessage = ref('');
// Filtered users
const filteredUsers = computed(() => {
if (!searchQuery.value) return users.value;
const query = searchQuery.value.toLowerCase();
return users.value.filter(user =>
user.name.toLowerCase().includes(query) ||
user.email.toLowerCase().includes(query) ||
user.department.toLowerCase().includes(query) ||
user.roles.some(role => role.toLowerCase().includes(query))
);
});
// Methods
const loadUsers = async () => {
isLoading.value = true;
hasError.value = false;
try {
// This would be an actual API call in production
// For demo, we'll simulate it with mock data
await new Promise(resolve => setTimeout(resolve, 800));
users.value = [
{
id: 'user1',
name: 'Aiman Fakhrullah',
email: 'aiman@example.com',
department: 'JKR Bahagian Kejuruteraan Awam Cawangan Kota Bharu',
roles: ['engineer'],
lastLogin: '2023-12-15T08:30:00Z',
status: 'active'
},
{
id: 'user2',
name: 'Ahmad Zaki',
email: 'ahmad@example.com',
department: 'JKR Bahagian Kewangan',
roles: ['finance', 'approver'],
lastLogin: '2023-12-14T14:45:00Z',
status: 'active'
},
{
id: 'user3',
name: 'Siti Aminah',
email: 'siti@example.com',
department: 'JKR Cawangan Kuala Terengganu',
roles: ['manager', 'approver'],
lastLogin: '2023-12-13T09:15:00Z',
status: 'active'
},
{
id: 'admin1',
name: 'Admin User',
email: 'admin@example.com',
department: 'IT Department',
roles: ['admin'],
lastLogin: '2023-12-15T10:00:00Z',
status: 'active'
}
];
} catch (error) {
console.error('Failed to load users:', error);
hasError.value = true;
errorMessage.value = 'Failed to load users. Please try again.';
} finally {
isLoading.value = false;
}
};
const loadRoles = async () => {
isLoading.value = true;
hasError.value = false;
try {
// This would be an actual API call to Authentik in production
// For demo, we'll simulate it with mock data
await new Promise(resolve => setTimeout(resolve, 600));
roles.value = [
{
id: 'admin',
name: 'Administrator',
description: 'Full system access',
userCount: 1,
permissions: {
documents: {
view: true,
edit: true,
delete: true,
approve: true,
reject: true,
download: true
},
cabinets: {
view: true,
create: true,
edit: true,
delete: true
},
accessRequests: {
approve: true,
reject: true,
viewAll: true
}
}
},
{
id: 'manager',
name: 'Manager',
description: 'Department management',
userCount: 1,
permissions: {
documents: {
view: true,
edit: true,
delete: false,
approve: true,
reject: true,
download: true
},
cabinets: {
view: true,
create: true,
edit: true,
delete: false
},
accessRequests: {
approve: true,
reject: true,
viewAll: true
}
}
},
{
id: 'engineer',
name: 'Engineer',
description: 'Engineering staff',
userCount: 1,
permissions: {
documents: {
view: true,
edit: true,
delete: false,
approve: false,
reject: false,
download: true
},
cabinets: {
view: true,
create: false,
edit: false,
delete: false
},
accessRequests: {
approve: false,
reject: false,
viewAll: false
}
}
},
{
id: 'finance',
name: 'Finance',
description: 'Finance department staff',
userCount: 1,
permissions: {
documents: {
view: true,
edit: true,
delete: false,
approve: false,
reject: false,
download: true
},
cabinets: {
view: true,
create: false,
edit: false,
delete: false
},
accessRequests: {
approve: false,
reject: false,
viewAll: false
}
}
},
{
id: 'approver',
name: 'Approver',
description: 'Can approve access requests',
userCount: 2,
permissions: {
documents: {
view: true,
edit: false,
delete: false,
approve: true,
reject: true,
download: false
},
cabinets: {
view: true,
create: false,
edit: false,
delete: false
},
accessRequests: {
approve: true,
reject: true,
viewAll: true
}
}
}
];
} catch (error) {
console.error('Failed to load roles:', error);
hasError.value = true;
errorMessage.value = 'Failed to load roles. Please try again.';
} finally {
isLoading.value = false;
}
};
const switchTab = (tab) => {
activeTab.value = tab;
if (tab === 'users' && users.value.length === 0) {
loadUsers();
} else if (tab === 'roles' && roles.value.length === 0) {
loadRoles();
}
};
const formatDateTime = (dateString) => {
if (!dateString) return 'Never';
const date = new Date(dateString);
return date.toLocaleString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
};
// Icons
const getSvgIcon = (iconName) => {
const icons = {
'users': `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></svg>`,
'roles': `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"></path></svg>`,
'search': `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>`,
'add': `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>`,
'edit': `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg>`,
'trash': `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path></svg>`
};
return icons[iconName] || '';
};
// Lifecycle hooks
onMounted(() => {
loadUsers();
});
</script>
<template>
<div class="dms-role-management">
<LayoutsBreadcrumb />
<rs-card class="h-full">
<template #body>
<div class="h-full flex flex-col">
<!-- Loading State -->
<div v-if="isLoading" class="flex items-center justify-center h-full">
<div class="text-center">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-4"></div>
<p class="text-gray-600 dark:text-gray-400">Loading role management...</p>
</div>
</div>
<!-- Error State -->
<div v-else-if="hasError" class="flex items-center justify-center h-full">
<div class="text-center p-6">
<div class="text-red-500 text-5xl mb-4"></div>
<h2 class="text-xl font-semibold text-red-600 mb-2">Error Loading Data</h2>
<p class="text-gray-600 dark:text-gray-400 mb-4">{{ errorMessage }}</p>
<rs-button @click="activeTab === 'users' ? loadUsers() : loadRoles()" variant="primary">
Retry
</rs-button>
</div>
</div>
<!-- Main Content -->
<div v-else class="h-full flex flex-col">
<!-- Header with authentik info -->
<div class="bg-indigo-50 dark:bg-indigo-900/20 border-b border-indigo-200 dark:border-indigo-800">
<div class="px-6 py-4">
<div class="flex items-center">
<div class="w-10 h-10 rounded bg-indigo-100 dark:bg-indigo-900/30 flex items-center justify-center mr-3">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-indigo-600 dark:text-indigo-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
</svg>
</div>
<div>
<h2 class="text-lg font-medium text-indigo-900 dark:text-indigo-100">Authentik Integration</h2>
<p class="text-sm text-indigo-600 dark:text-indigo-300">Role-Based Access Control Management</p>
</div>
</div>
</div>
</div>
<!-- Tabs Navigation -->
<div class="border-b border-gray-200 dark:border-gray-700">
<div class="px-6">
<nav class="-mb-px flex space-x-6" aria-label="Tabs">
<button
v-for="tab in [{ id: 'users', label: 'Users', icon: 'users' }, { id: 'roles', label: 'Roles', icon: 'roles' }]"
:key="tab.id"
@click="switchTab(tab.id)"
class="py-4 px-1 border-b-2 font-medium text-sm transition-colors"
:class="[
activeTab === tab.id
? 'border-indigo-500 text-indigo-600 dark:border-indigo-400 dark:text-indigo-400'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-200 dark:hover:border-gray-600'
]"
>
<div class="flex items-center">
<span v-html="getSvgIcon(tab.icon)" class="mr-2"></span>
<span>{{ tab.label }}</span>
</div>
</button>
</nav>
</div>
</div>
<!-- Action Bar -->
<div class="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 p-4">
<div class="flex flex-wrap items-center justify-between gap-3">
<!-- Search -->
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<span v-html="getSvgIcon('search')" class="text-gray-400 dark:text-gray-500"></span>
</div>
<input
type="text"
v-model="searchQuery"
class="pl-10 pr-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 placeholder-gray-500 dark:placeholder-gray-400 focus:ring-indigo-500 focus:border-indigo-500 w-64"
placeholder="Search..."
/>
</div>
<!-- Actions -->
<div>
<rs-button variant="primary" class="flex items-center">
<span v-html="getSvgIcon('add')" class="mr-1.5"></span>
<span>{{ activeTab === 'users' ? 'Add User' : 'Add Role' }}</span>
</rs-button>
</div>
</div>
</div>
<!-- Tab Content -->
<div class="flex-1 p-4 overflow-auto">
<!-- Users Tab -->
<div v-if="activeTab === 'users'" class="h-full">
<div class="bg-white dark:bg-gray-800 shadow overflow-hidden sm:rounded-lg">
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead class="bg-gray-50 dark:bg-gray-900/10">
<tr>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
User
</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Department
</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Roles
</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Last Login
</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Status
</th>
<th scope="col" class="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Actions
</th>
</tr>
</thead>
<tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
<tr v-for="user in filteredUsers" :key="user.id" class="hover:bg-gray-50 dark:hover:bg-gray-900/10">
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<div class="flex-shrink-0 h-10 w-10 rounded-full bg-gray-100 dark:bg-gray-700 flex items-center justify-center text-gray-500 dark:text-gray-400">
{{ user.name.charAt(0) }}
</div>
<div class="ml-4">
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">
{{ user.name }}
</div>
<div class="text-sm text-gray-500 dark:text-gray-400">
{{ user.email }}
</div>
</div>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
<div class="max-w-xs truncate">{{ user.department }}</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex flex-wrap gap-1">
<span
v-for="role in user.roles"
:key="role"
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium"
:class="{
'bg-indigo-100 text-indigo-800 dark:bg-indigo-900/20 dark:text-indigo-300': role === 'admin',
'bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-300': role === 'manager',
'bg-blue-100 text-blue-800 dark:bg-blue-900/20 dark:text-blue-300': role === 'engineer',
'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/20 dark:text-yellow-300': role === 'finance',
'bg-purple-100 text-purple-800 dark:bg-purple-900/20 dark:text-purple-300': role === 'approver'
}"
>
{{ role }}
</span>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
{{ formatDateTime(user.lastLogin) }}
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium"
:class="{
'bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-300': user.status === 'active',
'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/20 dark:text-yellow-300': user.status === 'pending',
'bg-red-100 text-red-800 dark:bg-red-900/20 dark:text-red-300': user.status === 'inactive'
}"
>
{{ user.status }}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<button class="text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-300 mr-3">
<span v-html="getSvgIcon('edit')"></span>
</button>
<button class="text-red-600 hover:text-red-900 dark:text-red-400 dark:hover:text-red-300">
<span v-html="getSvgIcon('trash')"></span>
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Roles Tab -->
<div v-else-if="activeTab === 'roles'" class="h-full">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div
v-for="role in roles"
:key="role.id"
class="bg-white dark:bg-gray-800 shadow overflow-hidden rounded-lg border border-gray-200 dark:border-gray-700"
>
<div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
<div class="flex justify-between items-start">
<div>
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100">{{ role.name }}</h3>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">{{ role.description }}</p>
</div>
<div class="flex space-x-2">
<button class="text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-300">
<span v-html="getSvgIcon('edit')"></span>
</button>
</div>
</div>
</div>
<div class="px-6 py-4">
<div class="mb-4">
<div class="flex justify-between items-center mb-2">
<h4 class="text-sm font-medium text-gray-900 dark:text-gray-100">Document Permissions</h4>
</div>
<div class="flex flex-wrap gap-2">
<span
v-for="(value, key) in role.permissions.documents"
:key="`doc-${key}`"
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium"
:class="value ? 'bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-300' : 'bg-gray-100 text-gray-800 dark:bg-gray-900/20 dark:text-gray-400 line-through'"
>
{{ key }}
</span>
</div>
</div>
<div class="mb-4">
<div class="flex justify-between items-center mb-2">
<h4 class="text-sm font-medium text-gray-900 dark:text-gray-100">Cabinet Permissions</h4>
</div>
<div class="flex flex-wrap gap-2">
<span
v-for="(value, key) in role.permissions.cabinets"
:key="`cab-${key}`"
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium"
:class="value ? 'bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-300' : 'bg-gray-100 text-gray-800 dark:bg-gray-900/20 dark:text-gray-400 line-through'"
>
{{ key }}
</span>
</div>
</div>
<div>
<div class="flex justify-between items-center mb-2">
<h4 class="text-sm font-medium text-gray-900 dark:text-gray-100">Access Request Permissions</h4>
</div>
<div class="flex flex-wrap gap-2">
<span
v-for="(value, key) in role.permissions.accessRequests"
:key="`acc-${key}`"
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium"
:class="value ? 'bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-300' : 'bg-gray-100 text-gray-800 dark:bg-gray-900/20 dark:text-gray-400 line-through'"
>
{{ key }}
</span>
</div>
</div>
</div>
<div class="px-6 py-3 bg-gray-50 dark:bg-gray-900/10 text-right">
<span class="text-sm text-gray-500 dark:text-gray-400">
{{ role.userCount }} {{ role.userCount === 1 ? 'user' : 'users' }} with this role
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
</rs-card>
</div>
</template>
<style scoped>
.dms-role-management {
height: calc(100vh - 64px);
}
</style>

View File

@ -1,5 +1,5 @@
<script setup>
import { ref, reactive, computed } from 'vue';
import { ref, reactive, computed, onMounted, watch } from 'vue';
// Define page metadata
definePageMeta({
@ -18,11 +18,18 @@ definePageMeta({
],
});
// Basic loading and error states
const isLoading = ref(true);
const isSaving = ref(false);
const saveError = ref('');
const saveSuccess = ref('');
// Settings categories
const settingsCategories = [
{ id: 'access', name: 'User & Access Management', icon: '🔐' },
{ id: 'documents', name: 'Document & Folder Settings', icon: '📁' },
{ id: 'metadata', name: 'Metadata & Tagging', icon: '📝' },
{ id: 'workflow', name: 'Workflow & Automation', icon: '🔄' },
{ id: 'upload', name: 'Upload & Storage Settings', icon: '📤' },
{ id: 'system', name: 'System Settings', icon: '📅' }
];
@ -30,9 +37,8 @@ const settingsCategories = [
// Current active category
const activeCategory = ref('access');
// Settings data structure
// Local reactive settings for form manipulation with default structure
const settings = reactive({
// User & Access Management
access: {
userRoles: ['Admin', 'Editor', 'Viewer', 'Uploader'],
rbacEnabled: true,
@ -51,8 +57,6 @@ const settings = reactive({
sessionTimeout: 8
}
},
// Document & Folder Settings
documents: {
folderHierarchy: {
maxDepth: 5,
@ -66,7 +70,7 @@ const settings = reactive({
},
retention: {
enabled: true,
defaultDays: 2555, // 7 years
defaultDays: 2555,
archiveBeforeDelete: true
},
versionControl: {
@ -75,8 +79,6 @@ const settings = reactive({
autoVersioning: true
}
},
// Metadata & Tagging
metadata: {
customFields: [
{ name: 'Department', type: 'dropdown', required: true },
@ -94,28 +96,41 @@ const settings = reactive({
rules: ['confidential-keywords', 'department-based', 'file-type']
}
},
// Upload & Storage Settings
workflow: {
approvalFlows: {
enabled: true,
defaultFlow: 'department-head-approval',
customFlows: ['legal-review', 'finance-approval', 'director-sign-off']
},
notifications: {
emailNotifications: true,
inAppNotifications: true,
uploadAlerts: true,
deadlineReminders: true
},
automation: {
triggers: ['document-uploaded', 'approval-completed', 'deadline-reached'],
actions: ['move-to-folder', 'send-notification', 'create-task']
}
},
upload: {
fileTypes: {
allowed: ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'jpg', 'png'],
blocked: ['exe', 'bat', 'cmd']
},
fileSizeLimit: 100, // MB
fileSizeLimit: 100,
quotas: {
perUser: 5000, // MB
perGroup: 50000, // MB
perProject: 100000 // MB
perUser: 5000,
perGroup: 50000,
perProject: 100000
},
storage: {
type: 'local', // local, s3, azure, google
type: 'local',
path: '/var/uploads/edms',
backupEnabled: true,
compressionEnabled: false
}
},
// System Settings
system: {
timezone: 'Asia/Kuala_Lumpur',
backupSchedule: 'daily',
@ -127,67 +142,137 @@ const settings = reactive({
}
});
// Computed properties
const currentSettings = computed(() => {
return settings[activeCategory.value];
});
// Computed properties for array-to-string conversions
const predefinedTagsString = computed({
get: () => settings.metadata.tagging.predefinedTags.join(', '),
get: () => settings.metadata?.tagging?.predefinedTags?.join(', ') || '',
set: (value) => {
settings.metadata.tagging.predefinedTags = value.split(',').map(tag => tag.trim()).filter(tag => tag.length > 0);
}
});
const allowedFileTypesString = computed({
get: () => settings.upload.fileTypes.allowed.join(', '),
get: () => settings.upload?.fileTypes?.allowed?.join(', ') || '',
set: (value) => {
settings.upload.fileTypes.allowed = value.split(',').map(type => type.trim()).filter(type => type.length > 0);
}
});
const blockedFileTypesString = computed({
get: () => settings.upload.fileTypes.blocked.join(', '),
get: () => settings.upload?.fileTypes?.blocked?.join(', ') || '',
set: (value) => {
settings.upload.fileTypes.blocked = value.split(',').map(type => type.trim()).filter(type => type.length > 0);
}
});
// Methods
// Load settings from API
const loadSettings = async () => {
try {
isLoading.value = true;
saveError.value = '';
const response = await $fetch('/api/dms/settings', {
method: 'GET'
});
if (response && response.data) {
// Merge loaded settings with defaults
Object.assign(settings, response.data);
console.log('Settings loaded successfully:', response.data);
}
} catch (error) {
console.error('Error loading settings:', error);
saveError.value = 'Failed to load settings. Using defaults.';
} finally {
isLoading.value = false;
}
};
// Save settings to API
const saveSettings = async () => {
try {
// In a real app, this would make an API call
console.log('Saving settings:', settings);
isSaving.value = true;
saveError.value = '';
saveSuccess.value = '';
// Show success message
alert('Settings saved successfully!');
const response = await $fetch('/api/dms/settings', {
method: 'POST',
body: settings
});
if (response && response.statusCode === 200) {
saveSuccess.value = 'Settings saved successfully!';
setTimeout(() => {
saveSuccess.value = '';
}, 3000);
} else {
saveError.value = 'Failed to save settings. Please try again.';
}
} catch (error) {
console.error('Error saving settings:', error);
alert('Error saving settings. Please try again.');
saveError.value = 'Error saving settings. Please try again.';
} finally {
isSaving.value = false;
}
};
const resetToDefaults = () => {
// Reset to defaults
const resetToDefaultsConfirm = async () => {
if (confirm('Are you sure you want to reset all settings to defaults? This action cannot be undone.')) {
// Reset logic would go here
console.log('Resetting to defaults');
try {
isSaving.value = true;
saveError.value = '';
saveSuccess.value = '';
// Reset to default values
settings.access = {
userRoles: ['Admin', 'Editor', 'Viewer', 'Uploader'],
rbacEnabled: true,
userGroups: ['HR Department', 'Finance', 'IT', 'Legal'],
permissions: {
view: true,
edit: true,
delete: false,
download: true,
share: true
},
authentication: {
ssoEnabled: false,
mfaRequired: false,
ldapIntegration: false,
sessionTimeout: 8
}
};
// Save the reset settings
await saveSettings();
saveSuccess.value = 'Settings reset to defaults successfully!';
} catch (error) {
console.error('Error resetting settings:', error);
saveError.value = 'Error resetting settings. Please try again.';
}
}
};
const exportSettings = () => {
const dataStr = JSON.stringify(settings, null, 2);
const dataUri = 'data:application/json;charset=utf-8,'+ encodeURIComponent(dataStr);
const exportFileDefaultName = 'edms-settings.json';
const linkElement = document.createElement('a');
linkElement.setAttribute('href', dataUri);
linkElement.setAttribute('download', exportFileDefaultName);
linkElement.click();
// Export settings
const exportSettingsFile = () => {
try {
const dataStr = JSON.stringify(settings, null, 2);
const dataUri = 'data:application/json;charset=utf-8,'+ encodeURIComponent(dataStr);
const exportFileDefaultName = 'dms-settings.json';
const linkElement = document.createElement('a');
linkElement.setAttribute('href', dataUri);
linkElement.setAttribute('download', exportFileDefaultName);
linkElement.click();
} catch (error) {
console.error('Error exporting settings:', error);
saveError.value = 'Error exporting settings.';
}
};
const importSettings = (event) => {
// Import settings
const importSettingsFile = (event) => {
const file = event.target.files[0];
if (file) {
const reader = new FileReader();
@ -195,9 +280,13 @@ const importSettings = (event) => {
try {
const importedSettings = JSON.parse(e.target.result);
Object.assign(settings, importedSettings);
alert('Settings imported successfully!');
saveSuccess.value = 'Settings imported successfully!';
setTimeout(() => {
saveSuccess.value = '';
}, 3000);
} catch (error) {
alert('Error importing settings. Please check the file format.');
console.error('Error importing settings:', error);
saveError.value = 'Error importing settings. Please check the file format.';
}
};
reader.readAsText(file);
@ -219,8 +308,10 @@ const removeCustomField = (index) => {
const addUserRole = () => {
const roleName = prompt('Enter new role name:');
if (roleName && !settings.access.userRoles.includes(roleName)) {
settings.access.userRoles.push(roleName);
if (roleName && roleName.trim()) {
if (!settings.access.userRoles.includes(roleName.trim())) {
settings.access.userRoles.push(roleName.trim());
}
}
};
@ -230,6 +321,11 @@ const removeUserRole = (role) => {
settings.access.userRoles.splice(index, 1);
}
};
// Load settings on mount
onMounted(async () => {
await loadSettings();
});
</script>
<template>
@ -241,46 +337,66 @@ const removeUserRole = (role) => {
<div class="flex justify-between items-center">
<h1 class="text-2xl font-bold text-gray-900 dark:text-gray-100">DMS Settings</h1>
<div class="flex space-x-2">
<rs-button @click="exportSettings" class="!bg-gray-100 !text-gray-700 border">
<Icon name="ic:outline-download" class="mr-2" />
<rs-button @click="exportSettingsFile" variant="secondary-outline" size="sm" :disabled="isLoading || isSaving">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7,10 12,15 17,10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>
Export
</rs-button>
<label class="cursor-pointer">
<input type="file" @change="importSettings" accept=".json" class="hidden" />
<rs-button class="!bg-gray-100 !text-gray-700 border">
<Icon name="ic:outline-upload" class="mr-2" />
<input type="file" @change="importSettingsFile" accept=".json" class="hidden" :disabled="isLoading || isSaving" />
<rs-button variant="secondary-outline" size="sm" :disabled="isLoading || isSaving">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="17,8 12,3 7,8"></polyline><line x1="12" y1="3" x2="12" y2="15"></line></svg>
Import
</rs-button>
</label>
<rs-button @click="resetToDefaults" class="!bg-red-100 !text-red-700 border border-red-200">
<Icon name="ic:outline-refresh" class="mr-2" />
<rs-button @click="resetToDefaultsConfirm" variant="danger-outline" size="sm" :disabled="isLoading || isSaving">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-2"><polyline points="1,4 1,10 7,10"></polyline><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"></path></svg>
Reset
</rs-button>
<rs-button @click="saveSettings" class="!bg-blue-600 !text-white">
<Icon name="ic:outline-save" class="mr-2" />
Save Settings
<rs-button @click="saveSettings" variant="primary" size="sm" :disabled="isLoading || isSaving">
<svg v-if="isSaving" class="animate-spin mr-2 h-4 w-4" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<svg v-else xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-2"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"></path><polyline points="17,21 17,13 7,13 7,21"></polyline><polyline points="7,3 7,8 15,8"></polyline></svg>
{{ isSaving ? 'Saving...' : 'Save Settings' }}
</rs-button>
</div>
</div>
<!-- Success/Error Messages -->
<div v-if="saveSuccess" class="mt-4 p-3 bg-green-100 border border-green-400 text-green-700 rounded">
{{ saveSuccess }}
</div>
<div v-if="saveError" class="mt-4 p-3 bg-red-100 border border-red-400 text-red-700 rounded">
{{ saveError }}
</div>
</template>
<template #body>
<div class="settings-layout flex h-full">
<!-- Loading State -->
<div v-if="isLoading" class="flex items-center justify-center h-64">
<div class="text-center">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-4"></div>
<p class="text-gray-600 dark:text-gray-400">Loading DMS settings...</p>
</div>
</div>
<!-- Settings Content -->
<div v-else class="settings-layout flex h-full">
<!-- Settings Navigation -->
<div class="settings-nav w-80 border-r border-gray-200 dark:border-gray-700 p-4 bg-gray-50 dark:bg-gray-800">
<div class="space-y-2">
<button
<rs-button
v-for="category in settingsCategories"
:key="category.id"
@click="activeCategory = category.id"
class="w-full flex items-center space-x-3 px-4 py-3 text-left rounded-lg transition-colors"
:class="activeCategory === category.id
? 'bg-blue-100 text-blue-700 dark:bg-blue-900/20 dark:text-blue-300'
: 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'"
:variant="activeCategory === category.id ? 'primary' : 'secondary-text'"
size="md"
class="w-full justify-start"
>
<span class="text-lg">{{ category.icon }}</span>
<span class="text-lg mr-3">{{ category.icon }}</span>
<span class="font-medium text-sm">{{ category.name }}</span>
</button>
</rs-button>
</div>
</div>
@ -301,11 +417,14 @@ const removeUserRole = (role) => {
<div class="space-y-2">
<div v-for="role in settings.access.userRoles" :key="role" class="flex items-center justify-between bg-gray-50 dark:bg-gray-700 px-3 py-2 rounded">
<span>{{ role }}</span>
<button @click="removeUserRole(role)" class="text-red-500 hover:text-red-700">
<Icon name="ic:outline-delete" size="16" />
</button>
<rs-button @click="removeUserRole(role)" variant="danger-text" size="sm">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3,6 5,6 21,6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path><line x1="10" y1="11" x2="10" y2="17"></line><line x1="14" y1="11" x2="14" y2="17"></line></svg>
</rs-button>
</div>
<button @click="addUserRole" class="text-blue-600 hover:text-blue-800 text-sm">+ Add Role</button>
<rs-button @click="addUserRole" variant="primary-text" size="sm">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-1"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>
Add Role
</rs-button>
</div>
</div>
<div>
@ -368,31 +487,7 @@ const removeUserRole = (role) => {
<div v-if="activeCategory === 'documents'" class="space-y-8">
<div>
<h2 class="text-xl font-semibold mb-4">📁 Document & Folder Settings</h2>
<!-- Folder Hierarchy -->
<div class="bg-white dark:bg-gray-800 rounded-lg border p-6 mb-6">
<h3 class="text-lg font-medium mb-4">Folder Hierarchies</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label class="block text-sm font-medium mb-2">Maximum Folder Depth</label>
<input type="number" v-model="settings.documents.folderHierarchy.maxDepth"
class="w-full px-3 py-2 border border-gray-300 rounded-md" min="1" max="10" />
</div>
<div>
<rs-select
v-model="settings.documents.folderHierarchy.folderTemplates[0]"
:options="[
{ value: 'Standard', label: 'Standard' },
{ value: 'Project-based', label: 'Project-based' },
{ value: 'Department-based', label: 'Department-based' },
{ value: 'Custom', label: 'Custom' }
]"
label="Folder Template"
/>
</div>
</div>
</div>
<!-- Naming Conventions -->
<div class="bg-white dark:bg-gray-800 rounded-lg border p-6 mb-6">
<h3 class="text-lg font-medium mb-4">Document Naming Conventions</h3>
@ -402,11 +497,10 @@ const removeUserRole = (role) => {
Auto-generate document names
</label>
<div>
<rs-input
v-model="settings.documents.namingConventions.pattern"
label="Naming Pattern"
placeholder="{department}_{title}_{date}"
/>
<label class="block text-sm font-medium mb-2">Naming Pattern</label>
<input type="text" v-model="settings.documents.namingConventions.pattern"
class="w-full px-3 py-2 border border-gray-300 rounded-md"
placeholder="{department}_{title}_{date}" />
</div>
</div>
</div>
@ -459,13 +553,14 @@ const removeUserRole = (role) => {
<input type="checkbox" v-model="field.required" class="mr-2" />
Required
</label>
<button @click="removeCustomField(index)" class="text-red-500 hover:text-red-700">
<Icon name="ic:outline-delete" size="20" />
</button>
<rs-button @click="removeCustomField(index)" variant="danger-text" size="sm">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3,6 5,6 21,6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path><line x1="10" y1="11" x2="10" y2="17"></line><line x1="14" y1="11" x2="14" y2="17"></line></svg>
</rs-button>
</div>
<button @click="addCustomField" class="text-blue-600 hover:text-blue-800">
+ Add Custom Field
</button>
<rs-button @click="addCustomField" variant="primary-text" size="sm">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-1"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>
Add Custom Field
</rs-button>
</div>
</div>
@ -482,12 +577,10 @@ const removeUserRole = (role) => {
Enable Tag Suggestions
</label>
<div>
<rs-textarea
v-model="predefinedTagsString"
label="Predefined Tags"
placeholder="urgent, confidential, public, draft, final"
:rows="3"
/>
<label class="block text-sm font-medium mb-2">Predefined Tags</label>
<textarea v-model="predefinedTagsString"
class="w-full px-3 py-2 border border-gray-300 rounded-md h-20"
placeholder="urgent, confidential, public, draft, final"></textarea>
</div>
</div>
</div>
@ -601,18 +694,57 @@ const removeUserRole = (role) => {
</div>
</div>
<!-- Placeholder for other categories -->
<div v-if="!['access', 'documents', 'metadata', 'upload', 'system'].includes(activeCategory)" class="space-y-8">
<div class="text-center py-12">
<div class="text-6xl mb-4">
{{ settingsCategories.find(c => c.id === activeCategory)?.icon }}
<!-- Workflow Settings -->
<div v-if="activeCategory === 'workflow'" class="space-y-8">
<div>
<h2 class="text-xl font-semibold mb-4">🔄 Workflow & Automation</h2>
<!-- Approval Flows -->
<div class="bg-white dark:bg-gray-800 rounded-lg border p-6 mb-6">
<h3 class="text-lg font-medium mb-4">Approval Workflows</h3>
<div class="space-y-4">
<label class="flex items-center">
<input type="checkbox" v-model="settings.workflow.approvalFlows.enabled" class="mr-2" />
Enable Approval Workflows
</label>
<div>
<label class="block text-sm font-medium mb-2">Default Approval Flow</label>
<select v-model="settings.workflow.approvalFlows.defaultFlow" class="w-full px-3 py-2 border border-gray-300 rounded-md">
<option value="department-head-approval">Department Head Approval</option>
<option value="legal-review">Legal Review</option>
<option value="finance-approval">Finance Approval</option>
<option value="director-sign-off">Director Sign-off</option>
</select>
</div>
</div>
</div>
<!-- Notifications -->
<div class="bg-white dark:bg-gray-800 rounded-lg border p-6">
<h3 class="text-lg font-medium mb-4">Notification Settings</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="space-y-3">
<label class="flex items-center">
<input type="checkbox" v-model="settings.workflow.notifications.emailNotifications" class="mr-2" />
Email Notifications
</label>
<label class="flex items-center">
<input type="checkbox" v-model="settings.workflow.notifications.inAppNotifications" class="mr-2" />
In-App Notifications
</label>
</div>
<div class="space-y-3">
<label class="flex items-center">
<input type="checkbox" v-model="settings.workflow.notifications.uploadAlerts" class="mr-2" />
Upload Alerts
</label>
<label class="flex items-center">
<input type="checkbox" v-model="settings.workflow.notifications.deadlineReminders" class="mr-2" />
Deadline Reminders
</label>
</div>
</div>
</div>
<h2 class="text-xl font-semibold mb-2">
{{ settingsCategories.find(c => c.id === activeCategory)?.name }}
</h2>
<p class="text-gray-600 dark:text-gray-400">
Settings for this category are being developed and will be available in the next update.
</p>
</div>
</div>

270
pages/dms/switch-roles.vue Normal file
View File

@ -0,0 +1,270 @@
<script setup>
import { useDmsStore } from '~/stores/dms';
import { ref, onMounted, computed } from 'vue';
import { useRouter } from 'vue-router';
// Define page metadata
definePageMeta({
title: "Switch User Role",
middleware: ["auth"],
requiresAuth: true,
breadcrumb: [
{
name: "DMS",
path: "/dms",
},
{
name: "Switch Role",
path: "/dms/switch-roles",
},
],
});
// Get the DMS store and router
const dmsStore = useDmsStore();
const router = useRouter();
// Original user backup
const originalUser = ref(null);
const currentRole = ref('');
const message = ref('');
const isLoading = ref(false);
// Get available roles from the store
const availableRoles = computed(() => {
return dmsStore.systemRoles || [];
});
// Check current role on mount
onMounted(() => {
// Store original user for restoration later
originalUser.value = { ...dmsStore.currentUser };
currentRole.value = dmsStore.currentUser.role;
});
// Get role details by ID
const getRoleById = (roleId) => {
return availableRoles.value.find(role => role.id === roleId) || {};
};
// Switch to a specific role
const switchToRole = (roleId) => {
if (currentRole.value === roleId) return;
isLoading.value = true;
message.value = `Switching to ${getRoleById(roleId).name} role...`;
// User data based on role
const userData = {
superadmin: {
id: 'superadmin1',
name: 'Super Admin User',
email: 'superadmin@example.com',
role: 'superadmin',
department: 'IT Department'
},
admin: {
id: 'admin1',
name: 'Admin User',
email: 'admin@example.com',
role: 'admin',
department: 'IT Department'
},
user: {
id: 'user1',
name: 'Regular User',
email: 'user@example.com',
role: 'user',
department: 'General Department'
}
};
// Keep original fields that we don't want to change
const originalFields = {};
if (originalUser.value && originalUser.value.role === roleId) {
// If switching back to the original role, use all original data
dmsStore.currentUser = { ...originalUser.value };
} else {
// Otherwise use the role-specific data
dmsStore.currentUser = {
...originalFields,
...userData[roleId]
};
}
// Update current role display
currentRole.value = roleId;
setTimeout(() => {
message.value = `Successfully switched to ${getRoleById(roleId).name} role!`;
isLoading.value = false;
}, 800);
};
// Switch back to original role
const switchToOriginal = () => {
isLoading.value = true;
message.value = 'Switching back to original role...';
if (originalUser.value) {
dmsStore.currentUser = { ...originalUser.value };
currentRole.value = originalUser.value.role;
}
setTimeout(() => {
message.value = 'Successfully switched back to original role!';
isLoading.value = false;
}, 800);
};
// Navigate back to DMS homepage
const goToDms = () => {
router.push('/dms');
};
</script>
<template>
<div class="switch-role-page">
<LayoutsBreadcrumb />
<rs-card class="h-full">
<template #body>
<div class="p-6">
<h1 class="text-2xl font-semibold mb-4">Switch User Role</h1>
<div class="bg-blue-50 dark:bg-blue-900/10 rounded-lg p-5 mb-6 border border-blue-200 dark:border-blue-800">
<div class="flex items-center mb-4">
<div class="w-12 h-12 rounded-full bg-blue-100 dark:bg-blue-900/30 flex items-center justify-center mr-4">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-blue-600 dark:text-blue-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
</div>
<div>
<h2 class="text-xl font-medium text-blue-900 dark:text-blue-100">
Current Role: <span class="font-bold">{{ getRoleById(currentRole).name || currentRole }}</span>
</h2>
<p class="text-blue-700 dark:text-blue-300">
This tool allows you to temporarily switch your role to view the system from different perspectives
</p>
</div>
</div>
<!-- Animated Status Message -->
<div v-if="message" class="mb-4 p-4 rounded-lg bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700">
<div class="flex items-center">
<div v-if="isLoading" class="mr-3 h-4 w-4 rounded-full border-2 border-blue-600 border-t-transparent animate-spin"></div>
<div v-else class="mr-3 h-4 w-4 rounded-full bg-green-500"></div>
<p>{{ message }}</p>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<!-- Superadmin Role Card -->
<div class="bg-white dark:bg-gray-800 rounded-lg p-5 border border-purple-100 dark:border-purple-900/20">
<div class="flex items-center mb-3">
<div class="w-8 h-8 rounded-full bg-purple-100 dark:bg-purple-900/30 flex items-center justify-center mr-3">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-purple-600 dark:text-purple-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-9.618 5.04L12 21.012l9.618-13.028A11.955 11.955 0 0112 2.944z" />
</svg>
</div>
<h3 class="text-lg font-medium text-purple-600 dark:text-purple-400">Superadmin Role</h3>
</div>
<p class="text-gray-600 dark:text-gray-400 mb-4 text-sm h-20 overflow-auto">
Full system access with complete control over users, settings, and content. Ability to manage all aspects of the system including user roles and permissions.
</p>
<button
@click="switchToRole('superadmin')"
class="w-full px-4 py-2 bg-purple-600 hover:bg-purple-700 text-white rounded-md text-sm font-medium transition-colors"
:disabled="currentRole === 'superadmin' || isLoading"
:class="{'opacity-50': currentRole === 'superadmin' || isLoading}"
>
Switch to Superadmin
</button>
</div>
<!-- Admin Role Card -->
<div class="bg-white dark:bg-gray-800 rounded-lg p-5 border border-blue-100 dark:border-blue-900/20">
<div class="flex items-center mb-3">
<div class="w-8 h-8 rounded-full bg-blue-100 dark:bg-blue-900/30 flex items-center justify-center mr-3">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-blue-600 dark:text-blue-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
</div>
<h3 class="text-lg font-medium text-blue-600 dark:text-blue-400">Admin Role</h3>
</div>
<p class="text-gray-600 dark:text-gray-400 mb-4 text-sm h-20 overflow-auto">
Administrative access with ability to manage content, approve requests, and view performance metrics. Access to dashboards and management tools.
</p>
<button
@click="switchToRole('admin')"
class="w-full px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md text-sm font-medium transition-colors"
:disabled="currentRole === 'admin' || isLoading"
:class="{'opacity-50': currentRole === 'admin' || isLoading}"
>
Switch to Admin
</button>
</div>
<!-- User Role Card -->
<div class="bg-white dark:bg-gray-800 rounded-lg p-5 border border-green-100 dark:border-green-900/20">
<div class="flex items-center mb-3">
<div class="w-8 h-8 rounded-full bg-green-100 dark:bg-green-900/30 flex items-center justify-center mr-3">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-green-600 dark:text-green-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
</div>
<h3 class="text-lg font-medium text-green-600 dark:text-green-400">User Role</h3>
</div>
<p class="text-gray-600 dark:text-gray-400 mb-4 text-sm h-20 overflow-auto">
Standard user access with ability to view permitted documents, request access to restricted content, and perform basic operations within granted permissions.
</p>
<button
@click="switchToRole('user')"
class="w-full px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-md text-sm font-medium transition-colors"
:disabled="currentRole === 'user' || isLoading"
:class="{'opacity-50': currentRole === 'user' || isLoading}"
>
Switch to User
</button>
</div>
</div>
<!-- Original Role Button -->
<div v-if="originalUser && currentRole !== originalUser.role" class="mt-4">
<button
@click="switchToOriginal"
class="w-full px-4 py-2 bg-gray-600 hover:bg-gray-700 text-white rounded-md text-sm font-medium transition-colors"
:disabled="isLoading"
>
Restore Original Role ({{ getRoleById(originalUser.role).name || originalUser.role }})
</button>
</div>
</div>
<div class="mt-8 bg-yellow-50 dark:bg-yellow-900/10 rounded-lg p-5 border border-yellow-200 dark:border-yellow-800">
<h3 class="text-lg font-medium mb-3 text-yellow-800 dark:text-yellow-400">Important Note</h3>
<p class="text-yellow-700 dark:text-yellow-300 mb-4">
This role switch is temporary and will reset when you refresh the page. This tool is for testing and demonstration purposes only.
</p>
<div class="mt-6 flex justify-center">
<button
@click="goToDms"
class="px-6 py-3 bg-yellow-600 hover:bg-yellow-700 text-white rounded-md text-sm font-medium transition-colors"
>
Return to DMS with New Role
</button>
</div>
</div>
</div>
</template>
</rs-card>
</div>
</template>
<style scoped>
.switch-role-page {
height: calc(100vh - 64px);
}
</style>

View File

@ -41,7 +41,11 @@ model department {
org_id Int
cabinets cabinets[]
organization organization @relation(fields: [org_id], references: [org_id], onDelete: Cascade, onUpdate: NoAction, map: "department_organization_FK")
<<<<<<< HEAD
user user[]
=======
users sys_user[]
>>>>>>> d4880c491e3491be4f09fbfbc0e0a9f8b5cfb1b8
@@index([org_id], map: "department_organization_FK")
}
@ -58,6 +62,26 @@ model cabinets {
department department? @relation(fields: [dp_id], references: [dp_id], onDelete: NoAction, onUpdate: NoAction, map: "cabinets_department_FK")
@@index([dp_id], map: "cabinets_department_FK")
<<<<<<< HEAD
=======
}
model sys_user {
su_id Int @id @default(autoincrement())
su_username String @unique(map: "sys_user_unique") @db.VarChar(100)
su_name String @db.VarChar(255)
su_nric Int @unique(map: "sys_user_unique_1")
su_dob DateTime @db.Date
su_email String? @db.VarChar(255)
su_password String @db.VarChar(255)
dp_id Int
su_active Int? @default(1)
su_lock Int? @default(0)
su_org_id Int
department department @relation(fields: [dp_id], references: [dp_id], onDelete: NoAction, onUpdate: NoAction, map: "sys_user_department_FK")
@@index([dp_id], map: "sys_user_department_FK")
>>>>>>> d4880c491e3491be4f09fbfbc0e0a9f8b5cfb1b8
}
model role {
@ -71,6 +95,7 @@ model role {
}
model user {
<<<<<<< HEAD
userID Int @id @default(autoincrement())
userSecretKey String? @db.VarChar(255)
userUsername String? @db.VarChar(255)
@ -87,6 +112,20 @@ model user {
userrole userrole[]
@@index([dp_id], map: "user_department_FK")
=======
userID Int @id @default(autoincrement())
userSecretKey String? @db.VarChar(255)
userUsername String? @db.VarChar(255)
userPassword String? @db.VarChar(255)
userFullName String? @db.VarChar(255)
userEmail String? @db.VarChar(255)
userPhone String? @db.VarChar(255)
userStatus String? @db.VarChar(255)
userCreatedDate DateTime? @db.DateTime(0)
userModifiedDate DateTime? @db.DateTime(0)
audit audit[]
userrole userrole[]
>>>>>>> d4880c491e3491be4f09fbfbc0e0a9f8b5cfb1b8
}
model userrole {
@ -164,6 +203,7 @@ model dms_settings {
}
model site_settings {
<<<<<<< HEAD
settingID Int @id @default(autoincrement())
siteName String? @default("corradAF") @db.VarChar(255)
siteNameFontSize Int? @default(18)
@ -191,4 +231,107 @@ model site_settings {
seoFacebookPixel String? @db.VarChar(255)
settingCreatedDate DateTime? @default(now()) @db.DateTime(0)
settingModifiedDate DateTime? @default(now()) @db.DateTime(0)
=======
settingID Int @id @default(autoincrement())
siteName String? @default("corradAF") @db.VarChar(255)
siteNameFontSize Int? @default(18)
siteDescription String? @db.Text
siteLogo String? @db.VarChar(500)
siteLoadingLogo String? @db.VarChar(500)
siteFavicon String? @db.VarChar(500)
siteLoginLogo String? @db.VarChar(500)
showSiteNameInHeader Boolean? @default(true)
customCSS String? @db.LongText
themeMode String? @default("biasa") @db.VarChar(100)
customThemeFile String? @db.VarChar(500)
currentFont String? @db.VarChar(100)
fontSource String? @db.VarChar(100)
seoTitle String? @db.VarChar(255)
seoDescription String? @db.Text
seoKeywords String? @db.Text
seoAuthor String? @db.VarChar(255)
seoOgImage String? @db.VarChar(500)
seoTwitterCard String? @default("summary_large_image") @db.VarChar(100)
seoCanonicalUrl String? @db.VarChar(500)
seoRobots String? @default("index, follow") @db.VarChar(100)
seoGoogleAnalytics String? @db.VarChar(255)
seoGoogleTagManager String? @db.VarChar(255)
seoFacebookPixel String? @db.VarChar(255)
settingCreatedDate DateTime? @default(now()) @db.DateTime(0)
settingModifiedDate DateTime? @default(now()) @db.DateTime(0)
}
model dms_settings {
settingID Int @id @default(autoincrement())
// User & Access Management
userRoles String? @db.Text
rbacEnabled Boolean? @default(true)
userGroups String? @db.Text
permissionView Boolean? @default(true)
permissionEdit Boolean? @default(true)
permissionDelete Boolean? @default(false)
permissionDownload Boolean? @default(true)
permissionShare Boolean? @default(true)
ssoEnabled Boolean? @default(false)
mfaRequired Boolean? @default(false)
ldapIntegration Boolean? @default(false)
sessionTimeout Int? @default(8)
// Document & Folder Settings
folderMaxDepth Int? @default(5)
folderDefaultStructure String? @db.Text
folderTemplates String? @db.Text
namingAutoGenerate Boolean? @default(true)
namingMandatoryFields String? @db.Text
namingPattern String? @default("{department}_{title}_{date}") @db.VarChar(255)
retentionEnabled Boolean? @default(true)
retentionDefaultDays Int? @default(2555)
retentionArchiveBeforeDelete Boolean? @default(true)
versionControlEnabled Boolean? @default(true)
versionControlMaxVersions Int? @default(10)
versionControlAutoVersioning Boolean? @default(true)
// Metadata & Tagging
metadataCustomFields String? @db.LongText
taggingPredefinedTags String? @db.Text
taggingUserGeneratedTags Boolean? @default(true)
taggingTagSuggestions Boolean? @default(true)
classificationAutoEnabled Boolean? @default(true)
classificationRules String? @db.Text
// Workflow & Automation
workflowApprovalEnabled Boolean? @default(true)
workflowDefaultFlow String? @default("department-head-approval") @db.VarChar(255)
workflowCustomFlows String? @db.Text
notificationEmail Boolean? @default(true)
notificationInApp Boolean? @default(true)
notificationUploadAlerts Boolean? @default(true)
notificationDeadlineReminders Boolean? @default(true)
automationTriggers String? @db.Text
automationActions String? @db.Text
// Upload & Storage Settings
uploadAllowedFileTypes String? @db.Text
uploadBlockedFileTypes String? @db.Text
uploadFileSizeLimit Int? @default(100)
uploadQuotaPerUser Int? @default(5000)
uploadQuotaPerGroup Int? @default(50000)
uploadQuotaPerProject Int? @default(100000)
storageType String? @default("local") @db.VarChar(100)
storagePath String? @default("/var/uploads/edms") @db.VarChar(500)
storageBackupEnabled Boolean? @default(true)
storageCompressionEnabled Boolean? @default(false)
// System Settings
systemTimezone String? @default("Asia/Kuala_Lumpur") @db.VarChar(100)
systemBackupSchedule String? @default("daily") @db.VarChar(100)
systemLogLevel String? @default("info") @db.VarChar(100)
systemMaintenanceMode Boolean? @default(false)
systemAutoUpdates Boolean? @default(false)
systemMonitoring Boolean? @default(true)
systemPerformanceMetrics Boolean? @default(true)
settingCreatedDate DateTime? @default(now()) @db.DateTime(0)
settingModifiedDate DateTime? @default(now()) @db.DateTime(0)
>>>>>>> d4880c491e3491be4f09fbfbc0e0a9f8b5cfb1b8
}

350
server/api/dms/settings.js Normal file
View File

@ -0,0 +1,350 @@
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
export default defineEventHandler(async (event) => {
const method = getMethod(event);
try {
if (method === "GET") {
// Get DMS settings
let settings = await prisma.dms_settings.findFirst({
orderBy: { settingID: "desc" },
});
// If no settings exist, create default ones
if (!settings) {
settings = await prisma.dms_settings.create({
data: {
settingCreatedDate: new Date(),
settingModifiedDate: new Date(),
},
});
}
// Transform database fields to frontend structure
const transformedSettings = {
// User & Access Management
access: {
userRoles: settings.userRoles ? settings.userRoles.split(',') : ['Admin', 'Editor', 'Viewer', 'Uploader'],
rbacEnabled: settings.rbacEnabled ?? true,
userGroups: settings.userGroups ? settings.userGroups.split(',') : ['HR Department', 'Finance', 'IT', 'Legal'],
permissions: {
view: settings.permissionView ?? true,
edit: settings.permissionEdit ?? true,
delete: settings.permissionDelete ?? false,
download: settings.permissionDownload ?? true,
share: settings.permissionShare ?? true
},
authentication: {
ssoEnabled: settings.ssoEnabled ?? false,
mfaRequired: settings.mfaRequired ?? false,
ldapIntegration: settings.ldapIntegration ?? false,
sessionTimeout: settings.sessionTimeout ?? 8
}
},
// Document & Folder Settings
documents: {
folderHierarchy: {
maxDepth: settings.folderMaxDepth ?? 5,
defaultStructure: settings.folderDefaultStructure ? settings.folderDefaultStructure.split(',') : ['Department', 'Project', 'Category', 'Year'],
folderTemplates: settings.folderTemplates ? settings.folderTemplates.split(',') : ['Standard', 'Project-based', 'Department-based']
},
namingConventions: {
autoGenerate: settings.namingAutoGenerate ?? true,
mandatoryFields: settings.namingMandatoryFields ? settings.namingMandatoryFields.split(',') : ['title', 'department', 'date'],
pattern: settings.namingPattern ?? '{department}_{title}_{date}'
},
retention: {
enabled: settings.retentionEnabled ?? true,
defaultDays: settings.retentionDefaultDays ?? 2555,
archiveBeforeDelete: settings.retentionArchiveBeforeDelete ?? true
},
versionControl: {
enabled: settings.versionControlEnabled ?? true,
maxVersions: settings.versionControlMaxVersions ?? 10,
autoVersioning: settings.versionControlAutoVersioning ?? true
}
},
// Metadata & Tagging
metadata: {
customFields: settings.metadataCustomFields ? JSON.parse(settings.metadataCustomFields) : [
{ name: 'Department', type: 'dropdown', required: true },
{ name: 'Priority', type: 'select', required: false },
{ name: 'Project Code', type: 'text', required: true },
{ name: 'Review Date', type: 'date', required: false }
],
tagging: {
predefinedTags: settings.taggingPredefinedTags ? settings.taggingPredefinedTags.split(',') : ['urgent', 'confidential', 'public', 'draft', 'final'],
userGeneratedTags: settings.taggingUserGeneratedTags ?? true,
tagSuggestions: settings.taggingTagSuggestions ?? true
},
classification: {
autoClassification: settings.classificationAutoEnabled ?? true,
rules: settings.classificationRules ? settings.classificationRules.split(',') : ['confidential-keywords', 'department-based', 'file-type']
}
},
// Workflow & Automation
workflow: {
approvalFlows: {
enabled: settings.workflowApprovalEnabled ?? true,
defaultFlow: settings.workflowDefaultFlow ?? 'department-head-approval',
customFlows: settings.workflowCustomFlows ? settings.workflowCustomFlows.split(',') : ['legal-review', 'finance-approval', 'director-sign-off']
},
notifications: {
emailNotifications: settings.notificationEmail ?? true,
inAppNotifications: settings.notificationInApp ?? true,
uploadAlerts: settings.notificationUploadAlerts ?? true,
deadlineReminders: settings.notificationDeadlineReminders ?? true
},
automation: {
triggers: settings.automationTriggers ? settings.automationTriggers.split(',') : ['document-uploaded', 'approval-completed', 'deadline-reached'],
actions: settings.automationActions ? settings.automationActions.split(',') : ['move-to-folder', 'send-notification', 'create-task']
}
},
// Upload & Storage Settings
upload: {
fileTypes: {
allowed: settings.uploadAllowedFileTypes ? settings.uploadAllowedFileTypes.split(',') : ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'jpg', 'png'],
blocked: settings.uploadBlockedFileTypes ? settings.uploadBlockedFileTypes.split(',') : ['exe', 'bat', 'cmd']
},
fileSizeLimit: settings.uploadFileSizeLimit ?? 100,
quotas: {
perUser: settings.uploadQuotaPerUser ?? 5000,
perGroup: settings.uploadQuotaPerGroup ?? 50000,
perProject: settings.uploadQuotaPerProject ?? 100000
},
storage: {
type: settings.storageType ?? 'local',
path: settings.storagePath ?? '/var/uploads/edms',
backupEnabled: settings.storageBackupEnabled ?? true,
compressionEnabled: settings.storageCompressionEnabled ?? false
}
},
// System Settings
system: {
timezone: settings.systemTimezone ?? 'Asia/Kuala_Lumpur',
backupSchedule: settings.systemBackupSchedule ?? 'daily',
logLevel: settings.systemLogLevel ?? 'info',
maintenanceMode: settings.systemMaintenanceMode ?? false,
autoUpdates: settings.systemAutoUpdates ?? false,
systemMonitoring: settings.systemMonitoring ?? true,
performanceMetrics: settings.systemPerformanceMetrics ?? true
}
};
return {
statusCode: 200,
message: "Success",
data: transformedSettings,
};
}
if (method === "POST") {
let body;
try {
body = await readBody(event);
} catch (bodyError) {
console.error("Error reading request body:", bodyError);
return {
statusCode: 400,
message: "Invalid request body",
error: bodyError.message,
};
}
// Validate required fields
if (!body || typeof body !== 'object') {
return {
statusCode: 400,
message: "Request body must be a valid JSON object",
};
}
// Check if settings exist
const existingSettings = await prisma.dms_settings.findFirst();
// Transform frontend structure to database fields
const dbData = {
settingModifiedDate: new Date()
};
// User & Access Management
if (body.access) {
if (body.access.userRoles) dbData.userRoles = body.access.userRoles.join(',');
if (body.access.rbacEnabled !== undefined) dbData.rbacEnabled = body.access.rbacEnabled;
if (body.access.userGroups) dbData.userGroups = body.access.userGroups.join(',');
if (body.access.permissions) {
if (body.access.permissions.view !== undefined) dbData.permissionView = body.access.permissions.view;
if (body.access.permissions.edit !== undefined) dbData.permissionEdit = body.access.permissions.edit;
if (body.access.permissions.delete !== undefined) dbData.permissionDelete = body.access.permissions.delete;
if (body.access.permissions.download !== undefined) dbData.permissionDownload = body.access.permissions.download;
if (body.access.permissions.share !== undefined) dbData.permissionShare = body.access.permissions.share;
}
if (body.access.authentication) {
if (body.access.authentication.ssoEnabled !== undefined) dbData.ssoEnabled = body.access.authentication.ssoEnabled;
if (body.access.authentication.mfaRequired !== undefined) dbData.mfaRequired = body.access.authentication.mfaRequired;
if (body.access.authentication.ldapIntegration !== undefined) dbData.ldapIntegration = body.access.authentication.ldapIntegration;
if (body.access.authentication.sessionTimeout !== undefined) dbData.sessionTimeout = body.access.authentication.sessionTimeout;
}
}
// Document & Folder Settings
if (body.documents) {
if (body.documents.folderHierarchy) {
if (body.documents.folderHierarchy.maxDepth !== undefined) dbData.folderMaxDepth = body.documents.folderHierarchy.maxDepth;
if (body.documents.folderHierarchy.defaultStructure) dbData.folderDefaultStructure = body.documents.folderHierarchy.defaultStructure.join(',');
if (body.documents.folderHierarchy.folderTemplates) dbData.folderTemplates = body.documents.folderHierarchy.folderTemplates.join(',');
}
if (body.documents.namingConventions) {
if (body.documents.namingConventions.autoGenerate !== undefined) dbData.namingAutoGenerate = body.documents.namingConventions.autoGenerate;
if (body.documents.namingConventions.mandatoryFields) dbData.namingMandatoryFields = body.documents.namingConventions.mandatoryFields.join(',');
if (body.documents.namingConventions.pattern !== undefined) dbData.namingPattern = body.documents.namingConventions.pattern;
}
if (body.documents.retention) {
if (body.documents.retention.enabled !== undefined) dbData.retentionEnabled = body.documents.retention.enabled;
if (body.documents.retention.defaultDays !== undefined) dbData.retentionDefaultDays = body.documents.retention.defaultDays;
if (body.documents.retention.archiveBeforeDelete !== undefined) dbData.retentionArchiveBeforeDelete = body.documents.retention.archiveBeforeDelete;
}
if (body.documents.versionControl) {
if (body.documents.versionControl.enabled !== undefined) dbData.versionControlEnabled = body.documents.versionControl.enabled;
if (body.documents.versionControl.maxVersions !== undefined) dbData.versionControlMaxVersions = body.documents.versionControl.maxVersions;
if (body.documents.versionControl.autoVersioning !== undefined) dbData.versionControlAutoVersioning = body.documents.versionControl.autoVersioning;
}
}
// Metadata & Tagging
if (body.metadata) {
if (body.metadata.customFields) dbData.metadataCustomFields = JSON.stringify(body.metadata.customFields);
if (body.metadata.tagging) {
if (body.metadata.tagging.predefinedTags) dbData.taggingPredefinedTags = body.metadata.tagging.predefinedTags.join(',');
if (body.metadata.tagging.userGeneratedTags !== undefined) dbData.taggingUserGeneratedTags = body.metadata.tagging.userGeneratedTags;
if (body.metadata.tagging.tagSuggestions !== undefined) dbData.taggingTagSuggestions = body.metadata.tagging.tagSuggestions;
}
if (body.metadata.classification) {
if (body.metadata.classification.autoClassification !== undefined) dbData.classificationAutoEnabled = body.metadata.classification.autoClassification;
if (body.metadata.classification.rules) dbData.classificationRules = body.metadata.classification.rules.join(',');
}
}
// Workflow & Automation
if (body.workflow) {
if (body.workflow.approvalFlows) {
if (body.workflow.approvalFlows.enabled !== undefined) dbData.workflowApprovalEnabled = body.workflow.approvalFlows.enabled;
if (body.workflow.approvalFlows.defaultFlow !== undefined) dbData.workflowDefaultFlow = body.workflow.approvalFlows.defaultFlow;
if (body.workflow.approvalFlows.customFlows) dbData.workflowCustomFlows = body.workflow.approvalFlows.customFlows.join(',');
}
if (body.workflow.notifications) {
if (body.workflow.notifications.emailNotifications !== undefined) dbData.notificationEmail = body.workflow.notifications.emailNotifications;
if (body.workflow.notifications.inAppNotifications !== undefined) dbData.notificationInApp = body.workflow.notifications.inAppNotifications;
if (body.workflow.notifications.uploadAlerts !== undefined) dbData.notificationUploadAlerts = body.workflow.notifications.uploadAlerts;
if (body.workflow.notifications.deadlineReminders !== undefined) dbData.notificationDeadlineReminders = body.workflow.notifications.deadlineReminders;
}
if (body.workflow.automation) {
if (body.workflow.automation.triggers) dbData.automationTriggers = body.workflow.automation.triggers.join(',');
if (body.workflow.automation.actions) dbData.automationActions = body.workflow.automation.actions.join(',');
}
}
// Upload & Storage Settings
if (body.upload) {
if (body.upload.fileTypes) {
if (body.upload.fileTypes.allowed) dbData.uploadAllowedFileTypes = body.upload.fileTypes.allowed.join(',');
if (body.upload.fileTypes.blocked) dbData.uploadBlockedFileTypes = body.upload.fileTypes.blocked.join(',');
}
if (body.upload.fileSizeLimit !== undefined) dbData.uploadFileSizeLimit = body.upload.fileSizeLimit;
if (body.upload.quotas) {
if (body.upload.quotas.perUser !== undefined) dbData.uploadQuotaPerUser = body.upload.quotas.perUser;
if (body.upload.quotas.perGroup !== undefined) dbData.uploadQuotaPerGroup = body.upload.quotas.perGroup;
if (body.upload.quotas.perProject !== undefined) dbData.uploadQuotaPerProject = body.upload.quotas.perProject;
}
if (body.upload.storage) {
if (body.upload.storage.type !== undefined) dbData.storageType = body.upload.storage.type;
if (body.upload.storage.path !== undefined) dbData.storagePath = body.upload.storage.path;
if (body.upload.storage.backupEnabled !== undefined) dbData.storageBackupEnabled = body.upload.storage.backupEnabled;
if (body.upload.storage.compressionEnabled !== undefined) dbData.storageCompressionEnabled = body.upload.storage.compressionEnabled;
}
}
// System Settings
if (body.system) {
if (body.system.timezone !== undefined) dbData.systemTimezone = body.system.timezone;
if (body.system.backupSchedule !== undefined) dbData.systemBackupSchedule = body.system.backupSchedule;
if (body.system.logLevel !== undefined) dbData.systemLogLevel = body.system.logLevel;
if (body.system.maintenanceMode !== undefined) dbData.systemMaintenanceMode = body.system.maintenanceMode;
if (body.system.autoUpdates !== undefined) dbData.systemAutoUpdates = body.system.autoUpdates;
if (body.system.systemMonitoring !== undefined) dbData.systemMonitoring = body.system.systemMonitoring;
if (body.system.performanceMetrics !== undefined) dbData.systemPerformanceMetrics = body.system.performanceMetrics;
}
let settings;
if (existingSettings) {
// Update existing settings
settings = await prisma.dms_settings.update({
where: { settingID: existingSettings.settingID },
data: dbData,
});
} else {
// Create new settings
settings = await prisma.dms_settings.create({
data: {
...dbData,
settingCreatedDate: new Date(),
},
});
}
return {
statusCode: 200,
message: "DMS settings updated successfully",
data: { settingID: settings.settingID },
};
}
return {
statusCode: 405,
message: "Method not allowed",
};
} catch (error) {
console.error("DMS settings API error:", error);
// Provide more specific error messages
if (error.code === 'P2002') {
return {
statusCode: 400,
message: "Duplicate entry error",
error: error.message,
};
}
if (error.code === 'P2025') {
return {
statusCode: 404,
message: "Record not found",
error: error.message,
};
}
if (error.code && error.code.startsWith('P')) {
return {
statusCode: 400,
message: "Database error",
error: error.message,
code: error.code,
};
}
return {
statusCode: 500,
message: "Internal server error",
error: error.message,
};
} finally {
await prisma.$disconnect();
}
});

View File

@ -12,10 +12,32 @@ export const useDmsStore = defineStore('dms', {
id: 'user1',
name: 'Aiman Fakhrullah',
email: 'aiman@example.com',
role: 'engineer', // engineer, admin, manager, etc.
role: 'user', // Role can be 'superadmin', 'admin', or 'user'
department: 'JKR Bahagian Kejuruteraan Awam Cawangan Kota Bharu'
},
// System roles
systemRoles: [
{
id: 'superadmin',
name: 'Super Administrator',
description: 'Full system access with ability to manage all settings, users, and content',
color: 'purple'
},
{
id: 'admin',
name: 'Administrator',
description: 'Administrative access to manage content and some system settings',
color: 'blue'
},
{
id: 'user',
name: 'User',
description: 'Standard user access for viewing and interacting with content based on permissions',
color: 'green'
}
],
// Cabinet access types
cabinetAccessTypes: [
{ id: 'public', name: 'Public Access', icon: 'check-circle', color: 'green' },
@ -30,6 +52,224 @@ export const useDmsStore = defineStore('dms', {
{ id: 'rejected', name: 'Rejected', color: 'red' }
],
// Access requests tracking
accessRequests: [
{
id: 'req1',
documentId: 'private-file1',
documentName: 'Annual_Budget_2024_CONFIDENTIAL.xlsx',
documentPath: '/private-cabinet1/private-drawer1/Annual_Budget_2024_CONFIDENTIAL.xlsx',
requesterId: 'user1',
requesterName: 'Aiman Fakhrullah',
requesterEmail: 'aiman@example.com',
requesterDepartment: 'JKR Bahagian Kejuruteraan Awam Cawangan Kota Bharu',
approverId: 'user2',
approverName: 'Ahmad Zaki',
approverDepartment: 'JKR Bahagian Kewangan',
requestDate: '2023-12-01T09:30:00Z',
targetResolutionTime: '2023-12-03T09:30:00Z', // 48 hours SLA
responseDate: '2023-12-02T14:15:00Z',
status: 'approved',
accessType: 'view',
accessDuration: '7 days',
justification: 'Need to review budget allocations for the upcoming bridge construction project',
notes: 'Approved for view-only access. Please ensure confidentiality is maintained.'
},
{
id: 'req2',
documentId: 'private-drawer1',
documentName: 'Budget & Financial Reports',
documentPath: '/private-cabinet1/Budget & Financial Reports',
requesterId: 'user1',
requesterName: 'Aiman Fakhrullah',
requesterEmail: 'aiman@example.com',
requesterDepartment: 'JKR Bahagian Kejuruteraan Awam Cawangan Kota Bharu',
approverId: null,
approverName: null,
approverDepartment: null,
requestDate: '2023-12-10T11:45:00Z',
targetResolutionTime: '2023-12-12T11:45:00Z', // 48 hours SLA
responseDate: null,
status: 'pending',
accessType: 'download',
accessDuration: '30 days',
justification: 'Need to analyze financial reports for the department annual review',
notes: null
},
{
id: 'req3',
documentId: 'jkr-kuala-terengganu',
documentName: 'JKR Cawangan Kuala Terengganu, Terengganu',
documentPath: '/private-cabinets/JKR Cawangan Kuala Terengganu, Terengganu',
requesterId: 'user1',
requesterName: 'Aiman Fakhrullah',
requesterEmail: 'aiman@example.com',
requesterDepartment: 'JKR Bahagian Kejuruteraan Awam Cawangan Kota Bharu',
approverId: 'user3',
approverName: 'Siti Aminah',
approverDepartment: 'JKR Cawangan Kuala Terengganu',
requestDate: '2023-12-05T15:20:00Z',
targetResolutionTime: '2023-12-07T15:20:00Z', // 48 hours SLA
responseDate: '2023-12-08T10:05:00Z', // Overdue response
status: 'rejected',
accessType: 'full',
accessDuration: '90 days',
justification: 'Need full access for the joint infrastructure project between Kota Bharu and Kuala Terengganu branches',
notes: 'Request denied. Please have your department head contact our director for special authorization.'
},
{
id: 'req4',
documentId: 'jkr-batu-kawan',
documentName: 'JKR Cawangan Batu Kawan, Penang',
documentPath: '/private-cabinets/JKR Cawangan Batu Kawan, Penang',
requesterId: 'user1',
requesterName: 'Aiman Fakhrullah',
requesterEmail: 'aiman@example.com',
requesterDepartment: 'JKR Bahagian Kejuruteraan Awam Cawangan Kota Bharu',
approverId: null,
approverName: null,
approverDepartment: null,
requestDate: '2023-11-28T09:15:00Z',
targetResolutionTime: '2023-11-30T09:15:00Z', // 48 hours SLA
responseDate: null,
status: 'pending',
accessType: 'view',
accessDuration: '14 days',
justification: 'Need to review similar projects in Batu Kawan for reference',
notes: null
},
{
id: 'req5',
documentId: 'jkr-arkitek',
documentName: 'JKR Cawangan Arkitek',
documentPath: '/private-cabinets/JKR Cawangan Arkitek',
requesterId: 'user1',
requesterName: 'Aiman Fakhrullah',
requesterEmail: 'aiman@example.com',
requesterDepartment: 'JKR Bahagian Kejuruteraan Awam Cawangan Kota Bharu',
approverId: null,
approverName: null,
approverDepartment: null,
requestDate: '2023-12-15T14:30:00Z',
targetResolutionTime: '2023-12-17T14:30:00Z', // 48 hours SLA
responseDate: null,
status: 'pending',
accessType: 'view',
accessDuration: '30 days',
justification: 'Need to consult architectural plans for the new government complex',
notes: null
}
],
// Department access requests (for KPI tracking)
departmentAccessRequests: [
{
id: 'dept-req1',
documentId: 'finance-reports-2023',
documentName: 'Finance Reports 2023',
documentPath: '/private-cabinet1/finance-reports-2023',
requesterId: 'user4',
requesterName: 'Ibrahim Hassan',
requesterEmail: 'ibrahim@example.com',
requesterDepartment: 'JKR Bahagian Kejuruteraan Awam Cawangan Kota Bharu',
approverId: 'user5',
approverName: 'Fatimah Abdullah',
approverDepartment: 'JKR Bahagian Kewangan',
requestDate: '2023-12-02T10:30:00Z',
targetResolutionTime: '2023-12-04T10:30:00Z',
responseDate: '2023-12-03T11:45:00Z',
status: 'approved',
accessType: 'view',
accessDuration: '14 days',
justification: 'Need to review financial reports for project planning',
notes: 'Approved with standard view-only restrictions'
},
{
id: 'dept-req2',
documentId: 'hr-policies-2023',
documentName: 'HR Policies 2023',
documentPath: '/private-cabinet2/hr-policies-2023',
requesterId: 'user6',
requesterName: 'Nurul Huda',
requesterEmail: 'nurul@example.com',
requesterDepartment: 'JKR Bahagian Kejuruteraan Awam Cawangan Kota Bharu',
approverId: 'user7',
approverName: 'Omar Ali',
approverDepartment: 'JKR Bahagian HR',
requestDate: '2023-12-05T09:15:00Z',
targetResolutionTime: '2023-12-07T09:15:00Z',
responseDate: '2023-12-09T16:30:00Z', // Overdue
status: 'approved',
accessType: 'download',
accessDuration: '30 days',
justification: 'Need to implement new HR policies in our department',
notes: 'Approved with delayed response due to verification requirements'
},
{
id: 'dept-req3',
documentId: 'strategic-plan-2024',
documentName: 'Strategic Plan 2024',
documentPath: '/private-cabinet3/strategic-plan-2024',
requesterId: 'user8',
requesterName: 'Hassan Ibrahim',
requesterEmail: 'hassan@example.com',
requesterDepartment: 'JKR Bahagian Kejuruteraan Awam Cawangan Kota Bharu',
approverId: 'user9',
approverName: 'Zainab Mohamed',
approverDepartment: 'JKR Pengarah',
requestDate: '2023-12-08T11:00:00Z',
targetResolutionTime: '2023-12-10T11:00:00Z',
responseDate: '2023-12-09T09:45:00Z',
status: 'rejected',
accessType: 'full',
accessDuration: '90 days',
justification: 'Need full access to implement strategic initiatives',
notes: 'Rejected due to insufficient clearance level. Please request through department head.'
},
{
id: 'dept-req4',
documentId: 'project-tendering-guidelines',
documentName: 'Project Tendering Guidelines',
documentPath: '/private-cabinet4/project-tendering-guidelines',
requesterId: 'user10',
requesterName: 'Razak Ismail',
requesterEmail: 'razak@example.com',
requesterDepartment: 'JKR Bahagian Kejuruteraan Awam Cawangan Kota Bharu',
approverId: null,
approverName: null,
approverDepartment: null,
requestDate: '2023-12-12T14:15:00Z',
targetResolutionTime: '2023-12-14T14:15:00Z',
responseDate: null,
status: 'pending',
accessType: 'view',
accessDuration: '7 days',
justification: 'Need to review tendering guidelines for upcoming project',
notes: null
},
{
id: 'dept-req5',
documentId: 'audit-reports-2023',
documentName: 'Audit Reports 2023',
documentPath: '/private-cabinet5/audit-reports-2023',
requesterId: 'user11',
requesterName: 'Aminah Zainal',
requesterEmail: 'aminah@example.com',
requesterDepartment: 'JKR Bahagian Kejuruteraan Awam Cawangan Kota Bharu',
approverId: null,
approverName: null,
approverDepartment: null,
requestDate: '2023-12-14T10:30:00Z',
targetResolutionTime: '2023-12-16T10:30:00Z',
responseDate: null,
status: 'pending',
accessType: 'download',
accessDuration: '14 days',
justification: 'Need to review audit findings for compliance',
notes: null
}
],
// Items state - in production this would be loaded from API
cabinets: [
{
@ -268,75 +508,24 @@ export const useDmsStore = defineStore('dms', {
}
],
// Access requests
accessRequests: [
{
id: 'req1',
userId: 'user1',
userName: 'Aiman Fakhrullah',
cabinetId: 'jkr-batu-kawan',
cabinetName: 'JKR Cawangan Batu Kawan, Penang',
requestDate: '2023-06-15',
status: 'pending',
reason: 'Need access for project collaboration'
},
{
id: 'req2',
userId: 'user1',
userName: 'Aiman Fakhrullah',
cabinetId: 'jkr-kuala-terengganu',
cabinetName: 'JKR Cawangan Kuala Terengganu, Terengganu',
requestDate: '2023-06-16',
status: 'pending',
reason: 'Required for cross-department coordination'
},
{
id: 'req3',
userId: 'user1',
userName: 'Aiman Fakhrullah',
cabinetId: 'jkr-arkitek',
cabinetName: 'JKR Cawangan Arkitek',
requestDate: '2023-06-17',
status: 'pending',
reason: 'Need architectural plans for current project'
},
{
id: 'req4',
userId: 'user1',
userName: 'Aiman Fakhrullah',
cabinetId: 'jkr-putrajaya',
cabinetName: 'JKR Cawangan Putrajaya',
requestDate: '2023-06-01',
status: 'rejected',
reason: 'Need access to headquarters documents',
rejectionReason: 'Access restricted to headquarters staff only'
}
],
// Selected item
selectedItem: null,
// View settings
viewMode: 'list', // list, grid, details
sortBy: 'name',
sortDirection: 'asc',
// User permissions - would be loaded from auth service
userPermissions: {
canCreate: true,
canEdit: true,
canDelete: true,
canManageAccess: true
},
// Flags
isLoading: false,
showFileViewer: false,
viewerDocument: null,
searchQuery: '',
// Search state
searchResults: [],
searchQuery: '',
// Settings integration
// Upload state
uploadProgress: 0,
currentUploads: [],
// View state
viewMode: 'list', // list, grid, details
sortBy: 'name', // name, modified, size, type
sortDirection: 'asc', // asc, desc
// File viewer state
fileViewerOpen: false,
currentDocument: null,
// System settings
systemSettings: {
upload: {
allowedFileTypes: ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'jpg', 'png'],
@ -521,8 +710,44 @@ export const useDmsStore = defineStore('dms', {
},
// Get pending access requests
pendingAccessRequests: (state) => {
return state.accessRequests.filter(req => req.status === 'pending');
pendingAccessRequests() {
return this.accessRequests.filter(req => req.status === 'pending');
},
approvedAccessRequests() {
return this.accessRequests.filter(req => req.status === 'approved');
},
rejectedAccessRequests() {
return this.accessRequests.filter(req => req.status === 'rejected');
},
overdueAccessRequests() {
const now = new Date();
return this.accessRequests.filter(req => {
if (req.status !== 'pending') return false;
const deadline = new Date(req.targetResolutionTime);
return now > deadline;
});
},
// Getter for average response time
averageResponseTime() {
const resolvedRequests = this.accessRequests.filter(
req => req.status === 'approved' || req.status === 'rejected'
);
if (resolvedRequests.length === 0) return 0;
const totalResponseTime = resolvedRequests.reduce((total, req) => {
const requestDate = new Date(req.requestDate);
const responseDate = new Date(req.responseDate);
const responseTimeHours = (responseDate - requestDate) / (1000 * 60 * 60);
return total + responseTimeHours;
}, 0);
return totalResponseTime / resolvedRequests.length;
}
},
@ -1151,46 +1376,55 @@ export const useDmsStore = defineStore('dms', {
},
// Access request functionality
async requestAccess(itemId, accessLevel, justification, duration = '7 days') {
async requestAccess(itemId, accessType, justification, duration = '7 days') {
this.isLoading = true;
try {
// Mock API delay
await new Promise(resolve => setTimeout(resolve, 500));
await new Promise(resolve => setTimeout(resolve, 800));
// Generate a unique request ID
const requestId = `req${Date.now()}`;
// Find the item being requested
const item = this.findItemById(itemId);
if (!item) {
throw new Error(`Item with id ${itemId} not found`);
}
// Calculate target resolution time (48 hours SLA)
const requestDate = new Date();
const targetResolutionTime = new Date(requestDate);
targetResolutionTime.setHours(targetResolutionTime.getHours() + 48);
// Create new access request
const newRequest = {
id: requestId,
userId: 'current-user-id', // Would come from auth store
userName: 'Current User', // Would come from auth store
itemId: itemId,
accessLevel: accessLevel,
justification: justification,
duration: duration,
requestDate: new Date().toISOString().split('T')[0],
status: 'pending'
id: `req-${Date.now()}`,
documentId: item.id,
documentName: item.name,
documentPath: this.getItemPath(item),
requesterId: this.currentUser.id,
requesterName: this.currentUser.name,
requesterEmail: this.currentUser.email,
requesterDepartment: this.currentUser.department,
approverId: null,
approverName: null,
approverDepartment: null,
requestDate: requestDate.toISOString(),
targetResolutionTime: targetResolutionTime.toISOString(),
responseDate: null,
status: 'pending',
accessType,
accessDuration: duration,
justification,
notes: null
};
// Add to access requests
// Add the request to the store
this.accessRequests.push(newRequest);
// Update the item's access request status (for mock data)
// In production, this would be handled server-side
const updateItemStatus = (items, id) => {
for (const item of items) {
if (item.id === id) {
item.accessRequestStatus = 'pending';
return true;
}
}
return false;
};
// Try to find and update the item in the mock data arrays
// This is a simplified approach for demo purposes
// Update the item's access request status if it's a cabinet
if (item.type === 'cabinet' || item.type === 'cabinet-group') {
this.updateCabinetAccessStatus(itemId, 'pending');
}
return newRequest;
} catch (error) {
@ -1199,6 +1433,447 @@ export const useDmsStore = defineStore('dms', {
} finally {
this.isLoading = false;
}
},
// Get access requests (can filter by user)
async getAccessRequests(userId = null) {
// Simulate API delay for realistic testing
await new Promise(resolve => setTimeout(resolve, 500));
if (userId) {
return this.accessRequests.filter(req => req.requesterId === userId);
}
return this.accessRequests;
},
// Get department access requests
async getDepartmentAccessRequests() {
// Simulate API delay for realistic testing
await new Promise(resolve => setTimeout(resolve, 600));
return this.departmentAccessRequests;
},
// Approve an access request
async approveAccessRequest(requestId, notes = '') {
// Simulate API delay for realistic testing
await new Promise(resolve => setTimeout(resolve, 1000));
const requestIndex = this.accessRequests.findIndex(req => req.id === requestId);
if (requestIndex === -1) {
throw new Error(`Request with id ${requestId} not found`);
}
// Update the request
const request = this.accessRequests[requestIndex];
const updatedRequest = {
...request,
approverId: this.currentUser.id,
approverName: this.currentUser.name,
approverDepartment: this.currentUser.department,
responseDate: new Date().toISOString(),
status: 'approved',
notes
};
// Update in store
this.accessRequests[requestIndex] = updatedRequest;
// Update the item's access status if it's a cabinet
if (request.documentId) {
const item = this.findItemById(request.documentId);
if (item && (item.type === 'cabinet' || item.type === 'cabinet-group')) {
this.updateCabinetAccessStatus(request.documentId, 'approved');
}
}
return updatedRequest;
},
// Reject an access request
async rejectAccessRequest(requestId, notes = '') {
// Simulate API delay for realistic testing
await new Promise(resolve => setTimeout(resolve, 1000));
const requestIndex = this.accessRequests.findIndex(req => req.id === requestId);
if (requestIndex === -1) {
throw new Error(`Request with id ${requestId} not found`);
}
// Update the request
const request = this.accessRequests[requestIndex];
const updatedRequest = {
...request,
approverId: this.currentUser.id,
approverName: this.currentUser.name,
approverDepartment: this.currentUser.department,
responseDate: new Date().toISOString(),
status: 'rejected',
notes
};
// Update in store
this.accessRequests[requestIndex] = updatedRequest;
// Update the item's access status if it's a cabinet
if (request.documentId) {
const item = this.findItemById(request.documentId);
if (item && (item.type === 'cabinet' || item.type === 'cabinet-group')) {
this.updateCabinetAccessStatus(request.documentId, 'rejected');
}
}
return updatedRequest;
},
// Get KPI metrics for access requests
async getAccessRequestMetrics(timeRange = '30days', userId = null) {
// Simulate API delay
await new Promise(resolve => setTimeout(resolve, 700));
// Filter requests by time range
const now = new Date();
let daysToLookBack = 30;
switch (timeRange) {
case '7days':
daysToLookBack = 7;
break;
case '30days':
daysToLookBack = 30;
break;
case '90days':
daysToLookBack = 90;
break;
case '365days':
daysToLookBack = 365;
break;
}
const cutoffDate = new Date(now);
cutoffDate.setDate(cutoffDate.getDate() - daysToLookBack);
let requestsToAnalyze = this.accessRequests;
// Filter by user if specified
if (userId) {
requestsToAnalyze = requestsToAnalyze.filter(req => req.requesterId === userId);
}
// Filter by date range
requestsToAnalyze = requestsToAnalyze.filter(req => {
const requestDate = new Date(req.requestDate);
return requestDate >= cutoffDate;
});
// Calculate metrics
const totalRequests = requestsToAnalyze.length;
const pendingRequests = requestsToAnalyze.filter(req => req.status === 'pending').length;
const approvedRequests = requestsToAnalyze.filter(req => req.status === 'approved').length;
const rejectedRequests = requestsToAnalyze.filter(req => req.status === 'rejected').length;
// Calculate response times
const resolvedRequests = requestsToAnalyze.filter(
req => req.status === 'approved' || req.status === 'rejected'
);
let totalResponseTime = 0;
let fastestResponse = Infinity;
let slowestResponse = 0;
resolvedRequests.forEach(req => {
const requestDate = new Date(req.requestDate);
const responseDate = new Date(req.responseDate);
const responseTimeHours = (responseDate - requestDate) / (1000 * 60 * 60);
totalResponseTime += responseTimeHours;
fastestResponse = Math.min(fastestResponse, responseTimeHours);
slowestResponse = Math.max(slowestResponse, responseTimeHours);
});
const avgResponseTime = resolvedRequests.length > 0
? totalResponseTime / resolvedRequests.length
: 0;
// Calculate overdue metrics
const overdueRequests = requestsToAnalyze.filter(req => {
if (req.status !== 'pending') return false;
const deadline = new Date(req.targetResolutionTime);
return now > deadline;
}).length;
const overduePercentage = totalRequests > 0
? overdueRequests / totalRequests
: 0;
return {
totalRequests,
pendingRequests,
approvedRequests,
rejectedRequests,
avgResponseTime,
fastestResponse: fastestResponse === Infinity ? 0 : fastestResponse,
slowestResponse,
overdueRequests,
overduePercentage
};
},
// Helper function to get the path for an item
getItemPath(item) {
if (!item.parentPath) return `/${item.name}`;
return `${item.parentPath}/${item.name}`;
},
// Find an item by ID
findItemById(id) {
// Look through cabinets recursively
const findInCabinets = (cabinets, targetId) => {
for (const cabinet of cabinets) {
if (cabinet.id === targetId) {
return cabinet;
}
if (cabinet.children) {
const found = findInCabinets(cabinet.children, targetId);
if (found) return found;
}
}
return null;
};
// First check cabinets
const cabinetItem = findInCabinets(this.cabinets, id);
if (cabinetItem) return cabinetItem;
// Then check regular items
return this.items.find(item => item.id === id);
},
// Update cabinet access status
updateCabinetAccessStatus(cabinetId, status) {
const updateCabinet = (cabinets, id, newStatus) => {
return cabinets.map(cabinet => {
if (cabinet.id === id) {
return {
...cabinet,
accessRequestStatus: newStatus,
hasAccess: newStatus === 'approved',
isLocked: newStatus === 'rejected'
};
}
if (cabinet.children) {
return {
...cabinet,
children: updateCabinet(cabinet.children, id, newStatus)
};
}
return cabinet;
});
};
this.cabinets = updateCabinet(this.cabinets, cabinetId, status);
},
// Authentik integration placeholder - this would be replaced with actual Authentik API calls
async authenticateWithAuthentik(username, password) {
// Simulate API delay
await new Promise(resolve => setTimeout(resolve, 800));
// This is a placeholder for the actual Authentik integration
// In a real implementation, this would make API calls to Authentik
if (username === 'superadmin' && password === 'password') {
return {
user: {
id: 'superadmin1',
name: 'Super Admin User',
email: 'superadmin@example.com',
role: 'superadmin',
department: 'IT Department'
},
token: 'sample-authentik-token'
};
}
if (username === 'admin' && password === 'password') {
return {
user: {
id: 'admin1',
name: 'Admin User',
email: 'admin@example.com',
role: 'admin',
department: 'IT Department'
},
token: 'sample-authentik-token'
};
}
if (username === 'user' && password === 'password') {
return {
user: {
id: 'user1',
name: 'Aiman Fakhrullah',
email: 'aiman@example.com',
role: 'user',
department: 'General Department'
},
token: 'sample-authentik-token'
};
}
throw new Error('Authentication failed');
},
// Get RBAC permissions from Authentik
async getRbacPermissions(userId) {
// Simulate API delay
await new Promise(resolve => setTimeout(resolve, 600));
// This is a placeholder for the actual Authentik integration
// In a real implementation, this would fetch RBAC permissions from Authentik
const permissions = {
'superadmin1': {
roles: ['superadmin'],
permissions: {
documents: {
view: true,
edit: true,
delete: true,
approve: true,
reject: true,
download: true
},
cabinets: {
view: true,
create: true,
edit: true,
delete: true
},
accessRequests: {
approve: true,
reject: true,
viewAll: true
},
systemSettings: {
manage: true
},
users: {
manage: true
},
roles: {
manage: true
}
}
},
'admin1': {
roles: ['admin'],
permissions: {
documents: {
view: true,
edit: true,
delete: true,
approve: true,
reject: true,
download: true
},
cabinets: {
view: true,
create: true,
edit: true,
delete: true
},
accessRequests: {
approve: true,
reject: true,
viewAll: true
},
systemSettings: {
manage: false
},
users: {
manage: false
},
roles: {
manage: false
}
}
},
'user1': {
roles: ['user'],
permissions: {
documents: {
view: true,
edit: true,
delete: false,
approve: false,
reject: false,
download: true
},
cabinets: {
view: true,
create: false,
edit: false,
delete: false
},
accessRequests: {
approve: false,
reject: false,
viewAll: false
},
systemSettings: {
manage: false
},
users: {
manage: false
},
roles: {
manage: false
}
}
}
};
return permissions[userId] || {
roles: [],
permissions: {
documents: {
view: false,
edit: false,
delete: false,
approve: false,
reject: false,
download: false
},
cabinets: {
view: false,
create: false,
edit: false,
delete: false
},
accessRequests: {
approve: false,
reject: false,
viewAll: false
},
systemSettings: {
manage: false
},
users: {
manage: false
},
roles: {
manage: false
}
}
};
}
}
});