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 Make sure to install the dependencies:
- **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
### 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 ```bash
git clone https://github.com/your-repo/edms.git # yarn
cd edms
```
2. **Install Dependencies**
```bash
# Using npm
npm install
# Using yarn
yarn install yarn install
# Using pnpm # npm
pnpm install npm install
# pnpm
pnpm install --shamefully-hoist
``` ```
3. **Environment Configuration** ## Development Server
Create `.env` file:
```env
# Database
DATABASE_URL="mysql://username:password@localhost:3306/edms_db"
# Authentication Start the development server on http://localhost:3000
JWT_SECRET="your-jwt-secret-key-min-256-bits"
SESSION_SECRET="your-session-secret-key"
# 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 ```bash
npm run dev npm run dev
``` ```
Access the application at `http://localhost:3000` ## Production
## 🎨 Design System Build the application for production:
### 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
```bash ```bash
# Build application
npm run build 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 ```bash
- **Document-level Security**: Individual document access controls npm run preview
- **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
## 🤝 Contributing Checkout the [deployment documentation](https://v3.nuxtjs.org/guide/deploy/presets) for more information.
# corradAF
1. Fork the repository This is the base project for corradAF.
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.

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> <script setup>
import { ref, computed } from 'vue'; import { ref, computed, watch } from 'vue';
import { useDmsStore } from '~/stores/dms'; import { useDmsStore } from '~/stores/dms';
const props = defineProps({ const props = defineProps({
@ -19,211 +19,274 @@ const emit = defineEmits(['close', 'submit']);
const dmsStore = useDmsStore(); const dmsStore = useDmsStore();
// Form state // Form state
const selectedAccessType = ref('view'); const accessType = ref('view');
const accessDuration = ref('7 days');
const justification = ref(''); const justification = ref('');
const accessDuration = ref('7 days');
const isSubmitting = ref(false); const isSubmitting = ref(false);
const formError = ref(''); const formError = ref('');
// Access type options // Durations
const accessTypes = [ const durationOptions = [
{ id: 'view', label: 'View Only', description: 'Can only view the document' }, { value: '1 day', label: '1 Day' },
{ id: 'download', label: 'Download', description: 'Can view and download' }, { value: '7 days', label: '7 Days' },
{ id: 'print', label: 'Print', description: 'Can view and print' }, { value: '14 days', label: '14 Days' },
{ id: 'full', label: 'Full Access', description: 'View, download and print' } { value: '30 days', label: '30 Days' },
{ value: '90 days', label: '90 Days' }
]; ];
// Access duration options // Access type options
const durationOptions = [ const accessTypeOptions = [
'7 days', {
'14 days', value: 'view',
'30 days', label: 'View Only',
'60 days', description: 'View documents without the ability to download or edit',
'90 days', 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>`
'Permanent' },
{
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 // Computed properties
const itemTitle = computed(() => { const itemName = computed(() => {
return props.item?.name || 'Document'; return props.item?.name || 'Document';
}); });
const itemFileName = computed(() => { const itemPath = computed(() => {
if (props.item?.type === 'file') { if (!props.item) return '';
return props.item.name; 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 // Methods
const closeDialog = () => { const resetForm = () => {
// Reset form accessType.value = 'view';
selectedAccessType.value = 'view';
accessDuration.value = '7 days';
justification.value = ''; justification.value = '';
accessDuration.value = '7 days';
formError.value = ''; formError.value = '';
emit('close');
}; };
const submitRequest = async () => { const handleClose = () => {
// Validate form emit('close');
if (!selectedAccessType.value) { // Reset form after closing
formError.value = 'Please select an access type'; setTimeout(() => {
return; resetForm();
} }, 300);
};
if (!justification.value.trim()) { const handleSubmit = () => {
formError.value = 'Please provide a justification for your request'; if (!isValidForm.value) {
formError.value = 'Please complete all required fields';
return; return;
} }
isSubmitting.value = true; isSubmitting.value = true;
formError.value = '';
try { try {
// Submit the request to the store emit('submit', {
const request = await dmsStore.requestAccess( accessType: accessType.value,
props.item.id, justification: justification.value,
selectedAccessType.value, accessDuration: accessDuration.value
justification.value, });
accessDuration.value
);
// Emit success event
emit('submit', request);
// Close the dialog (this will also reset the form)
closeDialog();
} catch (error) { } catch (error) {
formError.value = 'Failed to submit access request. Please try again.'; console.error('Submit error:', error);
console.error('Access request error:', error); formError.value = error.message || 'Failed to submit request';
} finally { } finally {
isSubmitting.value = false; isSubmitting.value = false;
} }
}; };
// Watch for visibility changes
watch(() => props.visible, (newValue) => {
if (newValue) {
resetForm();
}
});
</script> </script>
<template> <template>
<rs-modal <rs-modal
:visible="visible" v-model="props.visible"
@close="closeDialog"
title="Request Document Access"
size="md" 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> <template #body>
<div class="p-6"> <div class="p-2">
<!-- Document Information Section --> <!-- Item information -->
<div class="mb-6"> <div class="mb-6 bg-gray-50 dark:bg-gray-800 p-4 rounded-lg">
<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-gray-700 dark:text-gray-300 text-sm font-medium mb-2">Requesting access to:</h3>
<h3 class="text-sm font-medium text-blue-900 dark:text-blue-100 mb-2">Document Information</h3> <div class="flex items-start">
<div class="space-y-1"> <div class="mt-1 mr-3">
<div class="flex"> <span v-if="itemType === 'cabinet'" class="text-blue-600 dark:text-blue-400">
<span class="text-sm font-medium text-gray-700 dark:text-gray-300 w-12">Title:</span> <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<span class="text-sm text-gray-900 dark:text-gray-100">{{ itemTitle }}</span> <path d="M2 6a2 2 0 012-2h5l2 2h5a2 2 0 012 2v6a2 2 0 01-2 2H4a2 2 0 01-2-2V6z" />
</div> </svg>
<div class="flex"> </span>
<span class="text-sm font-medium text-gray-700 dark:text-gray-300 w-12">File:</span> <span v-else-if="itemType === 'drawer'" class="text-blue-600 dark:text-blue-400">
<span class="text-sm text-gray-600 dark:text-gray-400">{{ itemFileName }}</span> <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>
<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> </div>
</div> </div>
<!-- Form error --> <!-- 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 }} {{ formError }}
</div> </div>
<!-- Access Type Section --> <!-- Access type -->
<div class="mb-6"> <div class="mb-6">
<h3 class="text-sm font-medium text-gray-900 dark:text-gray-100 mb-3">Access Type</h3> <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
<div class="grid grid-cols-2 gap-3"> Access Type
</label>
<div class="space-y-3">
<div <div
v-for="accessType in accessTypes" v-for="option in accessTypeOptions"
:key="accessType.id" :key="option.value"
class="relative" 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"> <div class="flex items-center h-5">
<input <input
:id="accessType.id" :id="`access-type-${option.value}`"
v-model="selectedAccessType"
:value="accessType.id"
type="radio" type="radio"
class="w-4 h-4 text-blue-600 border-gray-300 focus:ring-blue-500" 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>
<div class="ml-3"> <div class="ml-3 flex items-center">
<div class="text-sm font-medium text-gray-900 dark:text-gray-100"> <span v-html="option.icon" class="mr-2 text-gray-600 dark:text-gray-400"></span>
{{ accessType.label }} <div>
</div> <label
<div class="text-xs text-gray-500 dark:text-gray-400 mt-1"> :for="`access-type-${option.value}`"
{{ accessType.description }} class="font-medium text-gray-700 dark:text-gray-300 cursor-pointer"
</div> >
</div> {{ option.label }}
</label> </label>
<p class="text-gray-500 dark:text-gray-400 text-xs">{{ option.description }}</p>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
<!-- Access Duration Section --> <!-- Justification -->
<div class="mb-6"> <div class="mb-6">
<rs-select <label for="justification" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
v-model="accessDuration" Justification
:options="durationOptions" <span class="text-red-500">*</span>
label="Access Duration" </label>
:required="false" <textarea
/> id="justification"
</div>
<!-- Justification Section -->
<div class="mb-6">
<rs-textarea
v-model="justification" v-model="justification"
label="Justification" rows="3"
placeholder="Please explain why you need access to this document..." 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"
:rows="4" placeholder="Please explain why you need access to this resource..."
:required="true" ></textarea>
resize="none" <p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
/> Minimum 10 characters. Please provide a detailed explanation.
</p>
</div> </div>
<!-- Footer Note --> <!-- Access Duration -->
<p class="text-xs text-gray-500 dark:text-gray-400 mb-6"> <div class="mb-6">
Your request will be reviewed by the document owner or administrator. <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> </p>
</div> </div>
</div>
</template> </template>
<template #footer> <template #footer>
<div class="flex justify-end gap-3 px-6 py-4 bg-gray-50 dark:bg-gray-800"> <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 <rs-button
@click="closeDialog" variant="light"
@click="handleClose"
:disabled="isSubmitting" :disabled="isSubmitting"
variant="secondary"
> >
Cancel Cancel
</rs-button> </rs-button>
<rs-button <rs-button
@click="submitRequest"
:disabled="isSubmitting"
variant="primary" variant="primary"
class="flex items-center" @click="handleSubmit"
:disabled="!isValidForm || isSubmitting"
:loading="isSubmitting"
> >
<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 Submit Request
</rs-button> </rs-button>
</div> </div>
</div>
</template> </template>
</rs-modal> </rs-modal>
</template> </template>

View File

@ -275,9 +275,9 @@ const getFieldComponent = (fieldType) => {
<div v-else> <div v-else>
<p class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2"> <p class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">
Drag and drop files here, or 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 browse
</button> </rs-button>
</p> </p>
<p class="text-sm text-gray-500"> <p class="text-sm text-gray-500">
Supported: {{ dmsStore.systemSettings.upload.allowedFileTypes.join(', ').toUpperCase() }} 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> <p class="text-sm text-gray-500">{{ formatFileSize(file.size) }}</p>
</div> </div>
</div> </div>
<button @click="removeFile(index)" <rs-button @click="removeFile(index)"
class="text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300"> variant="danger-text"
size="sm">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <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"/> <path d="M18 6L6 18M6 6L18 18" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg> </svg>
</button> </rs-button>
</div> </div>
<!-- Validation Errors --> <!-- 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 DMSUploadDialog from '~/components/dms/dialogs/DMSUploadDialog.vue';
import DMSCreateNewDialog from '~/components/dms/dialogs/DMSCreateNewDialog.vue'; import DMSCreateNewDialog from '~/components/dms/dialogs/DMSCreateNewDialog.vue';
import DMSAccessRequestDialog from '~/components/dms/dialogs/DMSAccessRequestDialog.vue'; import DMSAccessRequestDialog from '~/components/dms/dialogs/DMSAccessRequestDialog.vue';
import { useDmsStore } from '~/stores/dms';
// Props for the explorer // Props for the explorer
const props = defineProps({ const props = defineProps({
@ -36,6 +37,9 @@ const sortOrder = ref('asc'); // asc, desc
const searchQuery = ref(''); const searchQuery = ref('');
const showAccessRequestDialog = ref(false); const showAccessRequestDialog = ref(false);
const accessRequestItem = ref(null); const accessRequestItem = ref(null);
const isRequestingAccess = ref(false);
const requestSuccess = ref(false);
const requestError = ref(null);
// Navigation history for back/forward functionality // Navigation history for back/forward functionality
const navigationHistory = ref([props.initialPath]); const navigationHistory = ref([props.initialPath]);
@ -52,6 +56,7 @@ const currentDocument = ref(null);
// Mock data - replace with actual API calls // Mock data - replace with actual API calls
const allItems = ref([]); const allItems = ref([]);
const router = useRouter(); const router = useRouter();
const dmsStore = useDmsStore();
// Methods // Methods
const loadItems = async () => { const loadItems = async () => {
@ -600,6 +605,39 @@ const closeAccessRequestDialog = () => {
accessRequestItem.value = null; 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 // Computed properties
const currentItems = computed(() => { const currentItems = computed(() => {
let items = allItems.value.filter(item => item.parentPath === currentPath.value); 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)" <span v-if="item.accessType === 'private' && !checkItemAccess(item)"
v-html="getSvgIcon('lock', 16)" 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> 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>
</div> </div>
@ -1346,20 +1411,59 @@ const parentType = computed(() => {
<!-- Create New Dialog --> <!-- Create New Dialog -->
<DMSCreateNewDialog <DMSCreateNewDialog
v-model:visible="showCreateDialog" v-if="showCreateDialog"
:current-path="currentPath"
:parent-type="parentType"
@create="handleCreateNew"
@close="showCreateDialog = false" @close="showCreateDialog = false"
@create="handleCreateNew"
/> />
<!-- Access Request Dialog --> <!-- Access Request Dialog -->
<DMSAccessRequestDialog <DMSAccessRequestDialog
:visible="showAccessRequestDialog" v-if="accessRequestItem"
:item="accessRequestItem" :item="accessRequestItem"
@submit="handleAccessRequestSubmitted" :visible="showAccessRequestDialog"
@close="closeAccessRequestDialog" @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> </div>
</template> </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 ## 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 ### 1. Basic Information
- **Site Name**: Customize the application name displayed globally in: - **Site Name**: Application name displayed globally
- Header and sidebar - **Site Description**: Meta description for SEO and social sharing
- Browser title and meta tags - **Font Configuration**: Typography settings and font selection
- SEO and Open Graph tags - **Display Options**: Show/hide site name in header and other locations
- 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
### 2. Branding ### 2. Branding
- **Site Logo**: Upload a custom logo displayed in: - **Site Logo**: Main application logo for headers and navigation
- Header (horizontal layout) - **Loading Logo**: Branded loading screen logo
- Sidebar (vertical layout) - **Favicon**: Browser tab icon and bookmark icon
- Loading screen - **Login Logo**: Dedicated branding for authentication pages
- Login page
- Any component using site settings
- **Favicon**: Upload a custom favicon displayed in:
- Browser tabs
- Bookmarks
- Mobile home screen icons
### 3. Advanced Settings ### 3. SEO & Social Media
- **Custom CSS**: Add custom CSS injected into document head - **Meta Tags**: Title, description, keywords, author information
- **Custom Theme File**: Upload CSS files saved to `/assets/style/css/` - **Open Graph**: Facebook and social media sharing optimization
- **Add Custom Theme to theme.css**: Directly add themes to the main theme.css file - **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** ## Basic Information 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
## 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 **Display Locations**:
The settings are stored in the `site_settings` table with the following fields: - Header navigation (primary logo area)
- `siteName`, `siteDescription` - Browser page titles (prefixed to page names)
- `siteLogo`, `siteFavicon` - Loading screen branding
- `selectedTheme` - Selected theme name - SEO meta tags and social sharing
- `customCSS`, `customThemeFile` - Authentication page headers
- Legacy fields maintained for backward compatibility
### API Endpoints ### Site Description
- `GET /api/devtool/config/site-settings` - Retrieve current settings - **Purpose**: Provide descriptive text for SEO and social media sharing
- `POST /api/devtool/config/site-settings` - Update settings - **SEO Impact**: Used as meta description tag for search engines
- `POST /api/devtool/config/upload-file` - Upload files (logos, themes) - **Social Sharing**: Appears in social media link previews
- `POST /api/devtool/config/add-custom-theme` - Add custom theme to theme.css - **Character Limit**: Recommended 150-160 characters for optimal SEO
- **Multi-language Support**: Single description applies globally
### File Upload Locations ### Typography Configuration
- **Logo and Favicon files**: Saved to `public/uploads/site-settings/` - **Font Selection**: Choose from available system fonts
- **Theme CSS files**: Saved to `assets/style/css/` directory - **Font Source**: Local fonts vs. web fonts (Google Fonts integration)
- **Custom themes**: Added directly to `assets/style/css/base/theme.css` - **Font Size Controls**: Configurable sizes for different elements
- **Global Application**: Typography changes apply system-wide
### Composable ## Branding & Visual Identity
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
### Global Integration ### Logo Management System
The site settings are globally integrated across:
#### Header Component #### Site Logo (Primary)
- Uses site settings for logo and name display - **Usage**: Main application logo in header and navigation
- Theme selection dropdown uses same system as site settings - **Recommended Dimensions**: 200x60px (maximum)
- Synced with site settings theme selection - **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 #### Loading Logo
- Uses site logo if available, fallback to default - **Usage**: Branded loading screens and splash pages
- Displays site name in loading screen - **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 #### Favicon Configuration
- Global meta tags updated from site settings - **Usage**: Browser tab icons, bookmarks, mobile home screen icons
- Title, description, and favicon managed globally - **Required Format**: ICO format preferred (PNG acceptable)
- Theme initialization from site settings - **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 #### Login Page Logo
- Document title updated globally - **Usage**: Dedicated branding for authentication and login pages
- Meta descriptions for SEO - **Design Consideration**: Can be different from main site logo for branding flexibility
- Open Graph tags for social sharing - **Recommended Dimensions**: 250x80px maximum
- Twitter Card tags - **Security Context**: Appears on security-sensitive pages
- Favicon and apple-touch-icon - **Brand Trust**: Important for user trust and brand recognition
### Theme System Integration ### File Upload System
- Integrates with existing theme system (themeList.js, themeList2.js) - **Secure Upload**: File validation and virus scanning
- Theme selection in header dropdown synced with site settings - **Storage Location**: `public/uploads/site-settings/` directory
- Custom themes can be added directly to theme.css - **Backup Integration**: Uploaded files included in system backups
- Backward compatibility with existing theme structure - **Version Control**: Maintain previous versions of uploaded assets
- **CDN Support**: Compatible with content delivery networks
### Custom Theme Structure ## SEO & Meta Tag Management
Custom themes added to theme.css should follow this structure:
### 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 ```css
html[data-theme="your-theme-name"] { html[data-theme="custom-theme-name"] {
--color-primary: 255, 0, 0; --color-primary: 255, 0, 0;
--color-secondary: 0, 255, 0; --color-secondary: 0, 255, 0;
--color-success: 0, 255, 0; --color-success: 0, 255, 0;
--color-info: 0, 0, 255; --color-info: 0, 0, 255;
--color-warning: 255, 255, 0; --color-warning: 255, 255, 0;
--color-danger: 255, 0, 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 #### Color Variable System
If no settings are configured, the system uses these defaults: - **RGB Values**: Colors defined as RGB triplets for alpha transparency support
- Site Name: "corradAF" - **Semantic Naming**: Colors named by purpose rather than appearance
- Site Description: "corradAF Base Project" - **Component Integration**: Variables used throughout Rs component system
- Selected Theme: "biasa" - **Dark Mode Support**: Automatic dark mode variants
- Logo: Default corradAF logo
- Favicon: Default favicon
## Migration Notes #### Theme File Management
- Legacy color fields (primaryColor, secondaryColor, etc.) are maintained for backward compatibility - **Upload Location**: Custom themes saved to `assets/style/css/`
- `themeMode` field is mapped to `selectedTheme` for compatibility - **Integration Method**: Direct injection into `theme.css` file
- Existing installations will automatically use default values - **Validation**: CSS syntax validation before integration
- Theme selection integrates with existing theme dropdown in header - **Backup**: Automatic backup before theme modifications
## Notes ### Theme Switching Mechanism
- Changes are applied immediately in the preview - **Real-time Application**: Themes applied immediately without page refresh
- Theme changes affect the entire application - **Header Integration**: Theme selector synchronized with site settings
- Custom CSS is injected into the document head - **User Preferences**: Individual user theme preferences (if enabled)
- Theme files are saved to `/assets/style/css/` for proper integration - **Default Theme**: System-wide default theme setting
- 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
### Important Notes ## Advanced Customization
- Changes are applied immediately in the preview
- Theme changes affect the entire application ### Custom CSS Injection
- Custom CSS is injected into the document head
- Theme files are saved to `/assets/style/css/` for proper integration #### Global CSS Override
- File uploads are validated for type and size - **Injection Point**: CSS inserted into document `<head>` element
- Settings persist across browser sessions - **Priority**: Custom CSS has high specificity to override defaults
- Site name and description updates are reflected globally and immediately - **Validation**: Basic CSS syntax validation
- All meta tags and SEO elements are automatically updated - **Performance**: Minification and optimization of injected CSS
- Logo changes are reflected in all components that use site settings
#### 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) 1. [Introduction](#introduction)
2. [Getting Started](#getting-started) 2. [Getting Started](#getting-started)
3. [Navigation](#navigation) 3. [Navigation](#navigation)
4. [Document Organization](#document-organization) 4. [DMS Interface](#dms-interface)
5. [Working with Documents](#working-with-documents) 5. [Access Level System](#access-level-system)
6. [Access Control](#access-control) 6. [Document Organization](#document-organization)
7. [Search and Filtering](#search-and-filtering) 7. [Working with Documents](#working-with-documents)
8. [Document Viewer](#document-viewer) 8. [DMS Settings (Administrators)](#dms-settings-administrators)
9. [My Documents](#my-documents) 9. [Search and Filtering](#search-and-filtering)
10. [Settings Administration](#settings-administration) 10. [Document Viewer](#document-viewer)
11. [Troubleshooting](#troubleshooting) 11. [Troubleshooting](#troubleshooting)
## Introduction ## 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. 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 ### 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 - **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) - **Document Viewer**: Built-in viewer supporting multiple file formats (PDF, images, text files, spreadsheets)
- **Search Functionality**: Advanced search across document titles, descriptions, and metadata - **Search Functionality**: Advanced search across document titles, descriptions, and metadata
- **Upload Management**: Drag-and-drop file upload with comprehensive metadata assignment - **Upload Management**: Drag-and-drop file upload with comprehensive metadata assignment
@ -55,27 +56,32 @@ The main dashboard provides:
### Main Navigation Menu ### Main Navigation Menu
Access the EDMS through the main navigation menu: Access the EDMS through the main navigation menu:
- **Dashboard**: System overview and quick access to recent items - **Dashboard**: System overview and quick access to recent items
- **DMS**: Main document management interface - **DMS**: Main document management interface with access level tabs
- **My Documents**: Personal document collection and workspace - **DMS Settings**: Administrative configuration panel (admin only)
- **Upload Document**: Dedicated document upload interface - **ERD**: Entity Relationship Diagram viewer for system structure
### EDMS Interface Layout ### 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 - Hierarchical view of cabinets, drawers, and folders
- Expandable/collapsible folder structure - Expandable/collapsible folder structure
- Color-coded access indicators for different permission levels - Color-coded access indicators for different permission levels
- Lock icons for restricted items requiring special access - 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) - Document listing with multiple view modes (List, Grid, Details)
- Breadcrumb navigation showing current location - Breadcrumb navigation showing current location
- Search and filter tools for finding documents - Search and filter tools for finding documents
- View mode controls and sorting options - 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 - Document properties and metadata information
- Quick actions and document tools - Quick actions and document tools
- Access control information and permissions - Access control information and permissions
@ -87,6 +93,82 @@ The EDMS interface consists of three main areas:
- Helps maintain orientation in deep folder structures - Helps maintain orientation in deep folder structures
- Displays full path from root to current location - 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 ## Document Organization
### Hierarchical Structure ### Hierarchical Structure
@ -106,8 +188,9 @@ Documents are organized in a four-level hierarchy designed for maximum flexibili
### Visual Indicators ### Visual Indicators
The system uses color coding and icons to indicate access levels: The system uses color coding and icons to indicate access levels:
- **Green**: Public documents (open access for all users) - **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) - **Red**: Private documents (restricted access)
- **Purple**: Personal documents (owned by you)
- **Lock Icon**: Documents you cannot currently access - **Lock Icon**: Documents you cannot currently access
- **Template Icon**: Document templates available for use - **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 #### Single File Upload
1. Navigate to the desired location in the hierarchy 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 3. Select file(s) using the file picker or drag-and-drop interface
4. Fill in comprehensive document metadata: 4. Fill in comprehensive document metadata:
- Document title and description - 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 5. Review and confirm successful uploads
### Document Metadata ### 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 - **Title**: Descriptive document name/title
- **Description**: Brief summary of document content and purpose - **Description**: Brief summary of document content and purpose
- **Keywords**: Search terms for improved discoverability (comma-separated) - **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.) - **Status**: Current state (Draft, Under Review, Approved, Archived, etc.)
- **Department**: Responsible department or team - **Department**: Responsible department or team
- **Access Level**: Determines who can view and interact with the document - **Access Level**: Determines who can view and interact with the document
- **Retention Period**: How long the document should be retained - **Retention Period**: How long the document should be retained
- **Version**: Document version number and change notes - **Version**: Document version number and change notes
- **Custom Fields**: Additional metadata fields as configured by administrators
### Viewing Documents ### Viewing Documents
1. Click on any document to view its details and metadata 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 - **Move Documents**: Drag and drop to reorganize into different locations
- **Delete Documents**: Remove documents with confirmation and audit trail - **Delete Documents**: Remove documents with confirmation and audit trail
- **Copy Documents**: Duplicate documents to multiple locations - **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 - **Share Documents**: Generate sharing links and manage access permissions
## Access Control ## DMS Settings (Administrators)
### 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
### Overview ### 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 ### Accessing DMS Settings
1. Navigate to **DMS****Settings** from the main navigation menu 1. Navigate to **DMS****Settings** in the main menu
2. Select a settings category from the left sidebar navigation 2. Requires administrative privileges to access
3. Configure settings within each category using standardized form components 3. Settings are organized into six main categories with tabbed navigation
4. Save changes to apply configurations across the system
### Settings Categories ### 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.)
#### 🔐 User & Access Management - **Access Permissions**: Configure granular permissions for view, edit, delete, download, and share
Configure user permissions, roles, and authentication settings: - **Authentication Settings**:
- Enable/disable Single Sign-On (SSO)
**User Roles & Permissions** - Require Multi-Factor Authentication (MFA)
- Define user roles (Admin, Editor, Viewer, Uploader) - LDAP/Active Directory integration
- 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
**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) - Session timeout configuration (1-24 hours)
- Password policies and security requirements
#### 📁 Document & Folder Settings #### 2. Document & Folder Settings 📁
Configure document organization and structure: - **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
**Folder Hierarchy** #### 3. Metadata & Tagging 📝
- Set maximum folder depth (1-10 levels) - **Custom Metadata Fields**:
- Choose default folder structure templates: - Add/remove custom fields (text, dropdown, date, number, multi-select)
- Standard: General-purpose organization - Set required fields for document uploads
- Project-based: Project-focused hierarchy - Configure field types and validation rules
- Department-based: Organizational structure alignment - **Tagging System**:
- Custom: User-defined structure - Manage predefined tags (urgent, confidential, public, draft, final, etc.)
- Configure folder creation permissions and restrictions - Enable/disable user-generated tags
- Configure tag suggestions and auto-completion
**Document Naming Conventions** #### 4. Workflow & Automation 🔄
- Enable automatic document name generation - **Approval Workflows**:
- Define mandatory metadata fields for document creation - Enable/disable approval workflows
- Set naming patterns using variables: - Configure default approval flow (department-head-approval, legal-review, etc.)
- `{department}`: Document department/owner - Set up custom approval processes
- `{title}`: Document title or name - **Notifications**:
- `{date}`: Creation or upload date - Email notifications for document activities
- `{project}`: Project code or identifier - In-app notifications and alerts
- `{category}`: Document category/type - Upload alerts and deadline reminders
**Document Retention & Version Control** #### 5. Upload & Storage Settings 📤
- Enable document retention policies with configurable retention periods - **File Type Management**:
- Set automatic archiving before deletion - Allowed file types (pdf, doc, docx, xls, xlsx, ppt, pptx, txt, jpg, png, etc.)
- Configure version control system: - Blocked file types (exe, bat, cmd, etc.)
- Maximum versions to retain (1-50) - Configure MIME type validation
- Automatic versioning on document updates - **Storage Quotas**:
- Version history tracking and comparison tools - Maximum file size limit (MB)
- Per-user storage quota
- Per-group storage quota
- Per-project storage quota
#### 📝 Metadata & Tagging #### 6. System Settings 📅
Configure document metadata and classification: - **General Configuration**:
- System timezone (Asia/Kuala_Lumpur, UTC, etc.)
**Custom Metadata Fields** - Backup schedule (hourly, daily, weekly, monthly)
Define custom fields for enhanced document organization: - System log level (debug, info, warning, error)
- Field types: Text, Dropdown, Date, Number, Multi-select - **System Maintenance**:
- Required field enforcement for document uploads - Enable/disable maintenance mode
- Department-specific metadata schemas - Configure automatic updates
- Dynamic field validation and formatting rules - System monitoring and performance metrics
**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
### Settings Management Features ### Settings Management Features
#### Import/Export Configuration #### Save & Load Settings
- **Export Settings**: Save current configuration as JSON file - **Auto-save**: Settings are saved immediately when modified
- Backup settings before major changes - **Loading States**: Visual feedback during save operations
- Transfer settings between environments - **Error Handling**: Clear error messages if save fails
- Documentation and compliance purposes - **Success Confirmation**: Confirmation messages with auto-dismiss
- **Import Settings**: Load configuration from JSON file
- Restore previous configurations #### Import/Export Functionality
- Deploy settings across multiple systems - **Export Settings**: Download complete settings as JSON file for backup
- Standardize configurations across environments - **Import Settings**: Upload JSON file to restore settings
- **Settings Migration**: Easy transfer between environments
- **Backup Management**: Regular backups of configuration
#### Reset to Defaults #### Reset to Defaults
- **Category Reset**: Reset individual setting categories to default values - **One-click Reset**: Reset all settings to system defaults
- **System Reset**: Reset entire system configuration to factory defaults - **Confirmation Dialog**: Prevents accidental resets
- **Selective Reset**: Choose specific settings to reset while preserving others - **Selective Reset**: Reset individual setting categories (future enhancement)
- **Default Values**: Sensible defaults for all configuration options
#### Settings Validation #### Dynamic Field Management
- **Real-time Validation**: Form validation prevents invalid configurations - **Add Custom Fields**: Create new metadata fields with validation
- **Dependency Checking**: Settings are validated for interdependencies - **Remove Fields**: Delete unused custom fields
- **Conflict Resolution**: System identifies and resolves setting conflicts - **Reorder Fields**: Arrange custom fields in preferred order
- **Preview Mode**: Test settings before applying system-wide - **Field Types**: Support for text, dropdown, date, number, and multi-select fields
### Best Practices for Settings Management ### Configuration Best Practices
#### Security Considerations #### Security Settings
- Regularly review user permissions and access levels - Enable MFA for sensitive environments
- Enable MFA for administrative accounts - Configure appropriate session timeouts
- Use strong session timeout values for security - Set up proper user roles and permissions
- Regularly audit user roles and group memberships - Regularly review access control settings
#### Storage Management #### Storage Management
- Monitor storage quotas and usage patterns - Set realistic file size limits based on storage capacity
- Implement retention policies for compliance - Configure appropriate quotas for users and groups
- Regular backup verification and testing - Enable compression for large files when possible
- Plan for storage scaling based on usage growth - Set up regular backup schedules
#### System Performance #### Workflow Optimization
- Monitor log levels to balance debugging needs with performance - Configure approval workflows based on organizational structure
- Schedule maintenance during off-peak hours - Enable relevant notifications to keep users informed
- Regular cleanup of temporary files and old versions - Set up appropriate retention policies
- Performance monitoring for system optimization - Configure automatic tagging rules for efficiency
#### Documentation & Compliance ## Search and Filtering
- 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
### 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 ### Search Tips
- **Permission Conflicts**: Resolve conflicting role assignments - Use quotation marks for exact phrase searches
- **Upload Failures**: Check file type restrictions and size limits - Combine multiple keywords for more specific results
- **Authentication Issues**: Verify SSO and authentication settings - Use the predefined tags for quick filtering
- **Storage Problems**: Monitor quotas and storage allocation - Take advantage of custom metadata fields for precise searches
#### Settings Recovery ## Document Viewer
- Use exported settings files to restore configurations
- Reset specific categories if issues persist ### Supported File Types
- Contact system administrator for complex configuration issues The built-in document viewer supports:
- Review audit logs for recent configuration changes - **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 ## Troubleshooting
### Common Issues and Solutions ### Common Issues
#### Cannot Access Document #### Documents Not Loading
- **Check Access Level**: Verify document permissions and look for lock icons 1. Check your internet connection
- **Request Access**: Use the access request feature for private documents 2. Verify you have permission to access the document
- **Contact Administrator**: Reach out for urgent access needs or system issues 3. Try refreshing the page or switching tabs
- **Verify Account Status**: Ensure your user account is active and properly configured 4. Contact administrator if the issue persists
#### Upload Problems #### Upload Failures
- **File Size Limits**: Check maximum file size restrictions (typically 50MB) 1. Check file size limits (configured in DMS settings)
- **File Format Support**: Ensure file format is supported by the system 2. Verify file type is allowed (check blocked file types in settings)
- **Network Connection**: Verify stable internet connection for large uploads 3. Ensure you have upload permissions in current location
- **Upload Permissions**: Confirm you have upload permissions for the target location 4. Try uploading one file at a time if bulk upload fails
- **Browser Issues**: Try clearing cache or using a different browser
#### Search Not Working Properly #### Search Not Working
- **Check Spelling**: Verify search terms are spelled correctly 1. Clear browser cache and cookies
- **Try Alternative Keywords**: Use different or broader search terms 2. Try different search terms or keywords
- **Clear Active Filters**: Remove any filters that might be limiting results 3. Check if you're searching in the correct access level tab
- **Refresh Application**: Reload the page to reset search functionality 4. Verify documents exist in the location you're searching
- **Check Indexing Status**: Some documents may take time to be indexed for search
#### Document Viewer Issues #### Access Denied Errors
- **Browser Compatibility**: Ensure you're using a supported modern browser 1. Verify you have appropriate permissions for the document/folder
- **Enable JavaScript**: Verify JavaScript is enabled in browser settings 2. Check if the document is in the correct access level category
- **Clear Browser Cache**: Clear cache and cookies to resolve display issues 3. Request access from document owner or administrator
- **Plugin Requirements**: Install any required browser plugins for specific file types 4. Contact administrator if permissions seem incorrect
- **Download Alternative**: Use download option if viewer is not functioning
### Getting Help and Support ### Performance Issues
- **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
### Best Practices for Optimal System Use #### Slow Loading
1. **Systematic Organization**: Create and maintain clear, logical folder structures 1. Check internet connection speed
2. **Descriptive Naming**: Use meaningful, descriptive document titles and filenames 2. Clear browser cache and temporary files
3. **Regular Maintenance**: Periodically review and clean up outdated documents 3. Close unnecessary browser tabs
4. **Backup Strategy**: Keep local copies of critical documents as backup 4. Contact administrator about server performance
5. **Consistent Conventions**: Follow organizational naming and filing conventions
6. **Metadata Accuracy**: Keep document metadata current and accurate #### Browser Compatibility
7. **Access Control Respect**: Follow organizational policies for document sharing and access - Use modern browsers (Chrome 70+, Firefox 65+, Safari 12+, Edge 79+)
8. **Version Management**: Use version control features to track document changes - Enable JavaScript in browser settings
9. **Security Awareness**: Be mindful of document sensitivity and appropriate access levels - Update browser to latest version
10. **Regular Updates**: Stay informed about system updates and new features - 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", "description": "Document Management System",
"child": [ "child": [
{ {
"title": "ERD", "title": "Document Explorer",
"path": "/dms/erd",
"icon": "material-symbols:database-sharp",
"child": [],
"meta": {}
},
{
"title": "Document Management",
"path": "/dms", "path": "/dms",
"icon": "ic:outline-folder", "icon": "ic:outline-folder",
"child": [], "child": [],
"meta": {} "meta": {}
}, },
{ {
"title": "Access Requests", "title": "Admin Dashboard",
"path": "/dms/access-requests", "path": "/dms/admin-dashboard",
"icon": "ic:outline-security", "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": [] "child": []
}, },
{ {
"title": "Settings", "title": "Settings",
"path": "/dms/settings", "path": "/dms/settings",
"icon": "ic:outline-settings", "icon": "ic:outline-settings",
"child": [] "child": [],
"meta": {
"auth": {
"role": ["admin", "superadmin"]
}
}
} }
], ],
"meta": {} "meta": {}
@ -99,7 +130,7 @@ export default [
"meta": { "meta": {
"auth": { "auth": {
"role": [ "role": [
"Developer" "superadmin"
] ]
} }
} }

View File

@ -8,14 +8,11 @@ import RsCollapseItem from "../../../components/RsCollapseItem.vue";
import RsDropdown from "../../../components/RsDropdown.vue"; import RsDropdown from "../../../components/RsDropdown.vue";
import RsDropdownItem from "../../../components/RsDropdownItem.vue"; import RsDropdownItem from "../../../components/RsDropdownItem.vue";
import RsFieldset from "../../../components/RsFieldset.vue"; import RsFieldset from "../../../components/RsFieldset.vue";
import RsInput from "../../../components/RsInput.vue";
import RsModal from "../../../components/RsModal.vue"; import RsModal from "../../../components/RsModal.vue";
import RsProgressBar from "../../../components/RsProgressBar.vue"; import RsProgressBar from "../../../components/RsProgressBar.vue";
import RsSelect from "../../../components/RsSelect.vue";
import RsTab from "../../../components/RsTab.vue"; import RsTab from "../../../components/RsTab.vue";
import RsTabItem from "../../../components/RsTabItem.vue"; import RsTabItem from "../../../components/RsTabItem.vue";
import RsTable from "../../../components/RsTable.vue"; import RsTable from "../../../components/RsTable.vue";
import RsTextarea from "../../../components/RsTextarea.vue";
import RsWizard from "../../../components/RsWizard.vue"; import RsWizard from "../../../components/RsWizard.vue";
export { export {
@ -29,13 +26,10 @@ export {
RsDropdown, RsDropdown,
RsDropdownItem, RsDropdownItem,
RsFieldset, RsFieldset,
RsInput,
RsModal, RsModal,
RsProgressBar, RsProgressBar,
RsSelect,
RsTab, RsTab,
RsTabItem, RsTabItem,
RsTable, RsTable,
RsTextarea,
RsWizard, 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> <script setup>
import { ref, computed, onMounted } from 'vue'; import { ref, onMounted, nextTick, computed } from 'vue';
import { useDmsStore } from '~/stores/dms'; import { useRouter } from 'vue-router';
import DMSNavigation from '~/components/dms/navigation/DMSNavigation.vue';
import DMSExplorer from '~/components/dms/explorer/DMSExplorer.vue'; // Router
const router = useRouter();
// Define page metadata // Define page metadata
definePageMeta({ definePageMeta({
@ -17,122 +18,111 @@ definePageMeta({
], ],
}); });
// Set up store // Import DMS components dynamically to handle potential import errors
const dmsStore = useDmsStore(); let DMSExplorer = null;
let useDmsStore = null;
// Local state // Basic 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);
const activeTab = ref('all'); const activeTab = ref('all');
const isLoading = ref(true);
const hasError = ref(false);
const errorMessage = ref('');
const componentsLoaded = ref(false);
// File selection state // Tab definitions with icons
const selectedFiles = ref([]); const tabs = [
const isSelecting = ref(false); {
id: 'all',
// Toggle file selection label: 'All Documents',
const toggleFileSelection = (file) => { icon: 'folder',
const index = selectedFiles.value.findIndex(f => f.id === file.id); color: 'blue'
if (index === -1) { },
selectedFiles.value.push(file); {
} else { id: 'public',
selectedFiles.value.splice(index, 1); 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) => { const handleItemSelected = (item) => {
selectedItem.value = item; console.log('Item selected:', item);
}; };
const handleViewModeChanged = (mode) => { const handleViewModeChanged = (mode) => {
@ -140,23 +130,12 @@ const handleViewModeChanged = (mode) => {
}; };
const handlePathChanged = (path) => { const handlePathChanged = (path) => {
currentPath.value = path; console.log('Path changed to:', 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;
}; };
// Lifecycle hooks // Lifecycle hooks
onMounted(() => { onMounted(() => {
// Any initialization logic loadComponents();
}); });
</script> </script>
@ -167,27 +146,179 @@ onMounted(() => {
<rs-card class="h-full"> <rs-card class="h-full">
<template #body> <template #body>
<div class="dms-layout h-full flex flex-col"> <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"> <!-- Loading State -->
<div class="flex items-center space-x-1"> <div v-if="isLoading" class="flex items-center justify-center h-full">
<button <div class="text-center">
v-for="tab in documentTabs" <div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-4"></div>
:key="tab.id" <p class="text-gray-600 dark:text-gray-400">Loading Document Management System...</p>
@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>
</div> </div>
</div> </div>
<!-- Explorer Component --> <!-- Error State -->
<div class="explorer-container flex-1 overflow-hidden"> <div v-else-if="hasError" class="flex items-center justify-center h-full">
<DMSExplorer <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="'/'" :initial-path="'/'"
:view-mode="'list'" :view-mode="'list'"
:active-document-tab="activeTab" :active-document-tab="activeTab"
@ -195,6 +326,15 @@ onMounted(() => {
@view-mode-changed="handleViewModeChanged" @view-mode-changed="handleViewModeChanged"
@path-changed="handlePathChanged" @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>
</div> </div>
</template> </template>
@ -211,16 +351,81 @@ onMounted(() => {
height: 100%; height: 100%;
} }
.explorer-container { .content-area {
min-height: 0; min-height: 0;
overflow: hidden;
} }
/* Ensure smooth transitions */ .dms-content {
.tabs-header button { height: 100%;
transition: all 0.2s ease;
} }
.tabs-header button:hover { .access-tabs {
flex-shrink: 0;
}
.access-tab {
position: relative;
overflow: hidden;
}
.access-tab:hover {
transform: translateY(-1px); 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> </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> <script setup>
import { ref, reactive, computed } from 'vue'; import { ref, reactive, computed, onMounted, watch } from 'vue';
// Define page metadata // Define page metadata
definePageMeta({ 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 // Settings categories
const settingsCategories = [ const settingsCategories = [
{ id: 'access', name: 'User & Access Management', icon: '🔐' }, { id: 'access', name: 'User & Access Management', icon: '🔐' },
{ id: 'documents', name: 'Document & Folder Settings', icon: '📁' }, { id: 'documents', name: 'Document & Folder Settings', icon: '📁' },
{ id: 'metadata', name: 'Metadata & Tagging', icon: '📝' }, { id: 'metadata', name: 'Metadata & Tagging', icon: '📝' },
{ id: 'workflow', name: 'Workflow & Automation', icon: '🔄' },
{ id: 'upload', name: 'Upload & Storage Settings', icon: '📤' }, { id: 'upload', name: 'Upload & Storage Settings', icon: '📤' },
{ id: 'system', name: 'System Settings', icon: '📅' } { id: 'system', name: 'System Settings', icon: '📅' }
]; ];
@ -30,9 +37,8 @@ const settingsCategories = [
// Current active category // Current active category
const activeCategory = ref('access'); const activeCategory = ref('access');
// Settings data structure // Local reactive settings for form manipulation with default structure
const settings = reactive({ const settings = reactive({
// User & Access Management
access: { access: {
userRoles: ['Admin', 'Editor', 'Viewer', 'Uploader'], userRoles: ['Admin', 'Editor', 'Viewer', 'Uploader'],
rbacEnabled: true, rbacEnabled: true,
@ -51,8 +57,6 @@ const settings = reactive({
sessionTimeout: 8 sessionTimeout: 8
} }
}, },
// Document & Folder Settings
documents: { documents: {
folderHierarchy: { folderHierarchy: {
maxDepth: 5, maxDepth: 5,
@ -66,7 +70,7 @@ const settings = reactive({
}, },
retention: { retention: {
enabled: true, enabled: true,
defaultDays: 2555, // 7 years defaultDays: 2555,
archiveBeforeDelete: true archiveBeforeDelete: true
}, },
versionControl: { versionControl: {
@ -75,8 +79,6 @@ const settings = reactive({
autoVersioning: true autoVersioning: true
} }
}, },
// Metadata & Tagging
metadata: { metadata: {
customFields: [ customFields: [
{ name: 'Department', type: 'dropdown', required: true }, { name: 'Department', type: 'dropdown', required: true },
@ -94,28 +96,41 @@ const settings = reactive({
rules: ['confidential-keywords', 'department-based', 'file-type'] rules: ['confidential-keywords', 'department-based', 'file-type']
} }
}, },
workflow: {
// Upload & Storage Settings 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: { upload: {
fileTypes: { fileTypes: {
allowed: ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'jpg', 'png'], allowed: ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'jpg', 'png'],
blocked: ['exe', 'bat', 'cmd'] blocked: ['exe', 'bat', 'cmd']
}, },
fileSizeLimit: 100, // MB fileSizeLimit: 100,
quotas: { quotas: {
perUser: 5000, // MB perUser: 5000,
perGroup: 50000, // MB perGroup: 50000,
perProject: 100000 // MB perProject: 100000
}, },
storage: { storage: {
type: 'local', // local, s3, azure, google type: 'local',
path: '/var/uploads/edms', path: '/var/uploads/edms',
backupEnabled: true, backupEnabled: true,
compressionEnabled: false compressionEnabled: false
} }
}, },
// System Settings
system: { system: {
timezone: 'Asia/Kuala_Lumpur', timezone: 'Asia/Kuala_Lumpur',
backupSchedule: 'daily', 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 // Computed properties for array-to-string conversions
const predefinedTagsString = computed({ const predefinedTagsString = computed({
get: () => settings.metadata.tagging.predefinedTags.join(', '), get: () => settings.metadata?.tagging?.predefinedTags?.join(', ') || '',
set: (value) => { set: (value) => {
settings.metadata.tagging.predefinedTags = value.split(',').map(tag => tag.trim()).filter(tag => tag.length > 0); settings.metadata.tagging.predefinedTags = value.split(',').map(tag => tag.trim()).filter(tag => tag.length > 0);
} }
}); });
const allowedFileTypesString = computed({ const allowedFileTypesString = computed({
get: () => settings.upload.fileTypes.allowed.join(', '), get: () => settings.upload?.fileTypes?.allowed?.join(', ') || '',
set: (value) => { set: (value) => {
settings.upload.fileTypes.allowed = value.split(',').map(type => type.trim()).filter(type => type.length > 0); settings.upload.fileTypes.allowed = value.split(',').map(type => type.trim()).filter(type => type.length > 0);
} }
}); });
const blockedFileTypesString = computed({ const blockedFileTypesString = computed({
get: () => settings.upload.fileTypes.blocked.join(', '), get: () => settings.upload?.fileTypes?.blocked?.join(', ') || '',
set: (value) => { set: (value) => {
settings.upload.fileTypes.blocked = value.split(',').map(type => type.trim()).filter(type => type.length > 0); 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 () => { const saveSettings = async () => {
try { try {
// In a real app, this would make an API call isSaving.value = true;
console.log('Saving settings:', settings); saveError.value = '';
saveSuccess.value = '';
// Show success message const response = await $fetch('/api/dms/settings', {
alert('Settings saved successfully!'); 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) { } catch (error) {
console.error('Error saving settings:', 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.')) { if (confirm('Are you sure you want to reset all settings to defaults? This action cannot be undone.')) {
// Reset logic would go here try {
console.log('Resetting to defaults'); 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
} }
}; };
const exportSettings = () => { // 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.';
}
}
};
// Export settings
const exportSettingsFile = () => {
try {
const dataStr = JSON.stringify(settings, null, 2); const dataStr = JSON.stringify(settings, null, 2);
const dataUri = 'data:application/json;charset=utf-8,'+ encodeURIComponent(dataStr); const dataUri = 'data:application/json;charset=utf-8,'+ encodeURIComponent(dataStr);
const exportFileDefaultName = 'edms-settings.json'; const exportFileDefaultName = 'dms-settings.json';
const linkElement = document.createElement('a'); const linkElement = document.createElement('a');
linkElement.setAttribute('href', dataUri); linkElement.setAttribute('href', dataUri);
linkElement.setAttribute('download', exportFileDefaultName); linkElement.setAttribute('download', exportFileDefaultName);
linkElement.click(); 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]; const file = event.target.files[0];
if (file) { if (file) {
const reader = new FileReader(); const reader = new FileReader();
@ -195,9 +280,13 @@ const importSettings = (event) => {
try { try {
const importedSettings = JSON.parse(e.target.result); const importedSettings = JSON.parse(e.target.result);
Object.assign(settings, importedSettings); Object.assign(settings, importedSettings);
alert('Settings imported successfully!'); saveSuccess.value = 'Settings imported successfully!';
setTimeout(() => {
saveSuccess.value = '';
}, 3000);
} catch (error) { } 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); reader.readAsText(file);
@ -219,8 +308,10 @@ const removeCustomField = (index) => {
const addUserRole = () => { const addUserRole = () => {
const roleName = prompt('Enter new role name:'); const roleName = prompt('Enter new role name:');
if (roleName && !settings.access.userRoles.includes(roleName)) { if (roleName && roleName.trim()) {
settings.access.userRoles.push(roleName); 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); settings.access.userRoles.splice(index, 1);
} }
}; };
// Load settings on mount
onMounted(async () => {
await loadSettings();
});
</script> </script>
<template> <template>
@ -241,46 +337,66 @@ const removeUserRole = (role) => {
<div class="flex justify-between items-center"> <div class="flex justify-between items-center">
<h1 class="text-2xl font-bold text-gray-900 dark:text-gray-100">DMS Settings</h1> <h1 class="text-2xl font-bold text-gray-900 dark:text-gray-100">DMS Settings</h1>
<div class="flex space-x-2"> <div class="flex space-x-2">
<rs-button @click="exportSettings" class="!bg-gray-100 !text-gray-700 border"> <rs-button @click="exportSettingsFile" variant="secondary-outline" size="sm" :disabled="isLoading || isSaving">
<Icon name="ic:outline-download" class="mr-2" /> <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 Export
</rs-button> </rs-button>
<label class="cursor-pointer"> <label class="cursor-pointer">
<input type="file" @change="importSettings" accept=".json" class="hidden" /> <input type="file" @change="importSettingsFile" accept=".json" class="hidden" :disabled="isLoading || isSaving" />
<rs-button class="!bg-gray-100 !text-gray-700 border"> <rs-button variant="secondary-outline" size="sm" :disabled="isLoading || isSaving">
<Icon name="ic:outline-upload" class="mr-2" /> <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 Import
</rs-button> </rs-button>
</label> </label>
<rs-button @click="resetToDefaults" class="!bg-red-100 !text-red-700 border border-red-200"> <rs-button @click="resetToDefaultsConfirm" variant="danger-outline" size="sm" :disabled="isLoading || isSaving">
<Icon name="ic:outline-refresh" class="mr-2" /> <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 Reset
</rs-button> </rs-button>
<rs-button @click="saveSettings" class="!bg-blue-600 !text-white"> <rs-button @click="saveSettings" variant="primary" size="sm" :disabled="isLoading || isSaving">
<Icon name="ic:outline-save" class="mr-2" /> <svg v-if="isSaving" class="animate-spin mr-2 h-4 w-4" viewBox="0 0 24 24">
Save Settings <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> </rs-button>
</div> </div>
</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>
<template #body> <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 --> <!-- 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="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"> <div class="space-y-2">
<button <rs-button
v-for="category in settingsCategories" v-for="category in settingsCategories"
:key="category.id" :key="category.id"
@click="activeCategory = 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" :variant="activeCategory === category.id ? 'primary' : 'secondary-text'"
:class="activeCategory === category.id size="md"
? 'bg-blue-100 text-blue-700 dark:bg-blue-900/20 dark:text-blue-300' class="w-full justify-start"
: 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'"
> >
<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> <span class="font-medium text-sm">{{ category.name }}</span>
</button> </rs-button>
</div> </div>
</div> </div>
@ -301,11 +417,14 @@ const removeUserRole = (role) => {
<div class="space-y-2"> <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"> <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> <span>{{ role }}</span>
<button @click="removeUserRole(role)" class="text-red-500 hover:text-red-700"> <rs-button @click="removeUserRole(role)" variant="danger-text" size="sm">
<Icon name="ic:outline-delete" size="16" /> <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>
</button> </rs-button>
</div> </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> </div>
<div> <div>
@ -369,30 +488,6 @@ const removeUserRole = (role) => {
<div> <div>
<h2 class="text-xl font-semibold mb-4">📁 Document & Folder Settings</h2> <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 --> <!-- Naming Conventions -->
<div class="bg-white dark:bg-gray-800 rounded-lg border p-6 mb-6"> <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> <h3 class="text-lg font-medium mb-4">Document Naming Conventions</h3>
@ -402,11 +497,10 @@ const removeUserRole = (role) => {
Auto-generate document names Auto-generate document names
</label> </label>
<div> <div>
<rs-input <label class="block text-sm font-medium mb-2">Naming Pattern</label>
v-model="settings.documents.namingConventions.pattern" <input type="text" v-model="settings.documents.namingConventions.pattern"
label="Naming Pattern" class="w-full px-3 py-2 border border-gray-300 rounded-md"
placeholder="{department}_{title}_{date}" placeholder="{department}_{title}_{date}" />
/>
</div> </div>
</div> </div>
</div> </div>
@ -459,13 +553,14 @@ const removeUserRole = (role) => {
<input type="checkbox" v-model="field.required" class="mr-2" /> <input type="checkbox" v-model="field.required" class="mr-2" />
Required Required
</label> </label>
<button @click="removeCustomField(index)" class="text-red-500 hover:text-red-700"> <rs-button @click="removeCustomField(index)" variant="danger-text" size="sm">
<Icon name="ic:outline-delete" size="20" /> <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>
</button> </rs-button>
</div> </div>
<button @click="addCustomField" class="text-blue-600 hover:text-blue-800"> <rs-button @click="addCustomField" variant="primary-text" size="sm">
+ Add Custom Field <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>
</button> Add Custom Field
</rs-button>
</div> </div>
</div> </div>
@ -482,12 +577,10 @@ const removeUserRole = (role) => {
Enable Tag Suggestions Enable Tag Suggestions
</label> </label>
<div> <div>
<rs-textarea <label class="block text-sm font-medium mb-2">Predefined Tags</label>
v-model="predefinedTagsString" <textarea v-model="predefinedTagsString"
label="Predefined Tags" class="w-full px-3 py-2 border border-gray-300 rounded-md h-20"
placeholder="urgent, confidential, public, draft, final" placeholder="urgent, confidential, public, draft, final"></textarea>
:rows="3"
/>
</div> </div>
</div> </div>
</div> </div>
@ -601,18 +694,57 @@ const removeUserRole = (role) => {
</div> </div>
</div> </div>
<!-- Placeholder for other categories --> <!-- Workflow Settings -->
<div v-if="!['access', 'documents', 'metadata', 'upload', 'system'].includes(activeCategory)" class="space-y-8"> <div v-if="activeCategory === 'workflow'" class="space-y-8">
<div class="text-center py-12"> <div>
<div class="text-6xl mb-4"> <h2 class="text-xl font-semibold mb-4">🔄 Workflow & Automation</h2>
{{ settingsCategories.find(c => c.id === activeCategory)?.icon }}
<!-- 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> </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>
</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 org_id Int
cabinets cabinets[] cabinets cabinets[]
organization organization @relation(fields: [org_id], references: [org_id], onDelete: Cascade, onUpdate: NoAction, map: "department_organization_FK") organization organization @relation(fields: [org_id], references: [org_id], onDelete: Cascade, onUpdate: NoAction, map: "department_organization_FK")
<<<<<<< HEAD
user user[] user user[]
=======
users sys_user[]
>>>>>>> d4880c491e3491be4f09fbfbc0e0a9f8b5cfb1b8
@@index([org_id], map: "department_organization_FK") @@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") department department? @relation(fields: [dp_id], references: [dp_id], onDelete: NoAction, onUpdate: NoAction, map: "cabinets_department_FK")
@@index([dp_id], 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 { model role {
@ -71,6 +95,7 @@ model role {
} }
model user { model user {
<<<<<<< HEAD
userID Int @id @default(autoincrement()) userID Int @id @default(autoincrement())
userSecretKey String? @db.VarChar(255) userSecretKey String? @db.VarChar(255)
userUsername String? @db.VarChar(255) userUsername String? @db.VarChar(255)
@ -87,6 +112,20 @@ model user {
userrole userrole[] userrole userrole[]
@@index([dp_id], map: "user_department_FK") @@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 { model userrole {
@ -164,6 +203,35 @@ model dms_settings {
} }
model site_settings { model site_settings {
<<<<<<< HEAD
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)
=======
settingID Int @id @default(autoincrement()) settingID Int @id @default(autoincrement())
siteName String? @default("corradAF") @db.VarChar(255) siteName String? @default("corradAF") @db.VarChar(255)
siteNameFontSize Int? @default(18) siteNameFontSize Int? @default(18)
@ -192,3 +260,78 @@ model site_settings {
settingCreatedDate DateTime? @default(now()) @db.DateTime(0) settingCreatedDate DateTime? @default(now()) @db.DateTime(0)
settingModifiedDate 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', id: 'user1',
name: 'Aiman Fakhrullah', name: 'Aiman Fakhrullah',
email: 'aiman@example.com', 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' 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 // Cabinet access types
cabinetAccessTypes: [ cabinetAccessTypes: [
{ id: 'public', name: 'Public Access', icon: 'check-circle', color: 'green' }, { 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' } { 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 // Items state - in production this would be loaded from API
cabinets: [ cabinets: [
{ {
@ -268,75 +508,24 @@ export const useDmsStore = defineStore('dms', {
} }
], ],
// Access requests // Search state
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: '',
searchResults: [], 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: { systemSettings: {
upload: { upload: {
allowedFileTypes: ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'jpg', 'png'], allowedFileTypes: ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'jpg', 'png'],
@ -521,8 +710,44 @@ export const useDmsStore = defineStore('dms', {
}, },
// Get pending access requests // Get pending access requests
pendingAccessRequests: (state) => { pendingAccessRequests() {
return state.accessRequests.filter(req => req.status === 'pending'); 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 // Access request functionality
async requestAccess(itemId, accessLevel, justification, duration = '7 days') { async requestAccess(itemId, accessType, justification, duration = '7 days') {
this.isLoading = true; this.isLoading = true;
try { try {
// Mock API delay // Mock API delay
await new Promise(resolve => setTimeout(resolve, 500)); await new Promise(resolve => setTimeout(resolve, 800));
// Generate a unique request ID // Find the item being requested
const requestId = `req${Date.now()}`; 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 // Create new access request
const newRequest = { const newRequest = {
id: requestId, id: `req-${Date.now()}`,
userId: 'current-user-id', // Would come from auth store documentId: item.id,
userName: 'Current User', // Would come from auth store documentName: item.name,
itemId: itemId, documentPath: this.getItemPath(item),
accessLevel: accessLevel, requesterId: this.currentUser.id,
justification: justification, requesterName: this.currentUser.name,
duration: duration, requesterEmail: this.currentUser.email,
requestDate: new Date().toISOString().split('T')[0], requesterDepartment: this.currentUser.department,
status: 'pending' 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); this.accessRequests.push(newRequest);
// Update the item's access request status (for mock data) // Update the item's access request status if it's a cabinet
// In production, this would be handled server-side if (item.type === 'cabinet' || item.type === 'cabinet-group') {
const updateItemStatus = (items, id) => { this.updateCabinetAccessStatus(itemId, 'pending');
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
return newRequest; return newRequest;
} catch (error) { } catch (error) {
@ -1199,6 +1433,447 @@ export const useDmsStore = defineStore('dms', {
} finally { } finally {
this.isLoading = false; 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
}
}
};
} }
} }
}); });