generated from corrad-software/corrad-af-2024
Merge branch 'development' of https://git.sena.my/corrad-software/EDMS into development
This commit is contained in:
commit
1c0afe2b8a
204
CHANGELOG.md
Normal file
204
CHANGELOG.md
Normal file
@ -0,0 +1,204 @@
|
||||
# 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
|
257
README.md
257
README.md
@ -1,45 +1,256 @@
|
||||
# Nuxt 3 Minimal Starter
|
||||
# Electronic Document Management System (EDMS)
|
||||
|
||||
Look at the [nuxt 3 documentation](https://v3.nuxtjs.org) to learn more.
|
||||
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.
|
||||
|
||||
## Setup
|
||||
## 🚀 Features
|
||||
|
||||
Make sure to install the dependencies:
|
||||
### Core Functionality
|
||||
- **Hierarchical Organization**: Cabinet → Drawer → Folder → Subfolder structure
|
||||
- **Advanced Document Management**: Upload, version control, metadata management
|
||||
- **Role-Based Access Control**: Granular permissions with access request workflows
|
||||
- **Multi-format Document Viewer**: Built-in viewer for PDF, images, Office documents
|
||||
- **Advanced Search**: Full-text search with metadata and tag filtering
|
||||
- **Responsive Design**: Works seamlessly across desktop, tablet, and mobile
|
||||
|
||||
### 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
|
||||
# yarn
|
||||
yarn install
|
||||
|
||||
# npm
|
||||
npm install
|
||||
|
||||
# pnpm
|
||||
pnpm install --shamefully-hoist
|
||||
git clone https://github.com/your-repo/edms.git
|
||||
cd edms
|
||||
```
|
||||
|
||||
## Development Server
|
||||
2. **Install Dependencies**
|
||||
```bash
|
||||
# Using npm
|
||||
npm install
|
||||
|
||||
Start the development server on http://localhost:3000
|
||||
# Using yarn
|
||||
yarn install
|
||||
|
||||
# Using pnpm
|
||||
pnpm install
|
||||
```
|
||||
|
||||
3. **Environment Configuration**
|
||||
Create `.env` file:
|
||||
```env
|
||||
# Database
|
||||
DATABASE_URL="mysql://username:password@localhost:3306/edms_db"
|
||||
|
||||
# Authentication
|
||||
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
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## Production
|
||||
Access the application at `http://localhost:3000`
|
||||
|
||||
Build the application for production:
|
||||
## 🎨 Design System
|
||||
|
||||
### Component Library
|
||||
The EDMS uses a standardized component library with the "Rs" prefix:
|
||||
|
||||
#### Form Components
|
||||
- **RsInput**: Standardized input fields with validation
|
||||
- **RsSelect**: Dropdown/select with options support
|
||||
- **RsTextarea**: Multi-line text input with resize controls
|
||||
- **RsButton**: Buttons with multiple variants and sizes
|
||||
|
||||
#### UI Components
|
||||
- **RsCard**: Container component with header/body/footer
|
||||
- **RsModal**: Modal dialogs with standardized structure
|
||||
- **RsTable**: Data tables with sorting and filtering
|
||||
- **RsDropdown**: Dropdown menus and navigation
|
||||
|
||||
### Usage Examples
|
||||
|
||||
```vue
|
||||
<!-- Input with validation -->
|
||||
<rs-input
|
||||
v-model="email"
|
||||
label="Email Address"
|
||||
type="email"
|
||||
:required="true"
|
||||
:error="emailError"
|
||||
/>
|
||||
|
||||
<!-- Button with variants -->
|
||||
<rs-button variant="primary" size="md" @click="save">
|
||||
Save Changes
|
||||
</rs-button>
|
||||
|
||||
<!-- Modal with standardized structure -->
|
||||
<rs-modal :visible="showModal" @close="closeModal">
|
||||
<template #header>Modal Title</template>
|
||||
<template #body>Modal content</template>
|
||||
<template #footer>
|
||||
<rs-button variant="secondary" @click="closeModal">Cancel</rs-button>
|
||||
<rs-button variant="primary" @click="confirm">Confirm</rs-button>
|
||||
</template>
|
||||
</rs-modal>
|
||||
```
|
||||
|
||||
### Design Principles
|
||||
- **🎯 Consistency**: Unified patterns across all components
|
||||
- **🔧 Modularity**: Reusable and composable components
|
||||
- **🌙 Dark Mode**: Universal theme support
|
||||
- **📱 Responsive**: Mobile-first approach
|
||||
- **♿ Accessibility**: WCAG-compliant with keyboard navigation
|
||||
|
||||
## 📖 Documentation
|
||||
|
||||
### Available Guides
|
||||
- **[Technical Guide](docs/Technical_Guide.md)**: Comprehensive technical documentation
|
||||
- **[User Guide](docs/User_Guide.md)**: End-user instructions and features
|
||||
- **[Site Settings](docs/SITE_SETTINGS.md)**: Configuration and customization guide
|
||||
|
||||
### Interactive Documentation
|
||||
- **Design System**: Visit `/dms/design-system` for component examples
|
||||
- **API Documentation**: Available in development mode
|
||||
- **Component Playground**: Test components with live examples
|
||||
|
||||
## 🔧 Development
|
||||
|
||||
### Project Structure
|
||||
```
|
||||
edms/
|
||||
├── components/ # Vue components
|
||||
│ ├── dms/ # DMS-specific components
|
||||
│ │ ├── dialogs/ # Modal dialogs
|
||||
│ │ ├── explorer/ # Document browser
|
||||
│ │ ├── search/ # Search functionality
|
||||
│ │ └── viewers/ # Document preview
|
||||
│ └── Rs*.vue # Design system components
|
||||
├── pages/ # File-based routing
|
||||
│ ├── dms/ # DMS pages
|
||||
│ └── devtool/ # Admin tools
|
||||
├── stores/ # Pinia state management
|
||||
├── server/ # API routes and middleware
|
||||
├── prisma/ # Database schema and migrations
|
||||
├── assets/ # Stylesheets and assets
|
||||
└── docs/ # Documentation
|
||||
```
|
||||
|
||||
### Building for Production
|
||||
|
||||
```bash
|
||||
# Build application
|
||||
npm run build
|
||||
```
|
||||
|
||||
Locally preview production build:
|
||||
|
||||
```bash
|
||||
# Preview production build
|
||||
npm run preview
|
||||
|
||||
# Generate static site (if applicable)
|
||||
npm run generate
|
||||
```
|
||||
|
||||
Checkout the [deployment documentation](https://v3.nuxtjs.org/guide/deploy/presets) for more information.
|
||||
# corradAF
|
||||
## 🔐 Security Features
|
||||
|
||||
This is the base project for corradAF.
|
||||
- **Role-Based Access Control**: Granular permission system
|
||||
- **Document-level Security**: Individual document access controls
|
||||
- **Access Request Workflow**: Approval system for restricted documents
|
||||
- **Audit Trail**: Comprehensive activity logging
|
||||
- **File Type Validation**: Security through file type restrictions
|
||||
- **Session Management**: Secure token-based authentication
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
|
||||
3. Follow the design system guidelines
|
||||
4. Use standardized components (Rs library)
|
||||
5. Add tests for new functionality
|
||||
6. Commit changes (`git commit -m 'Add amazing feature'`)
|
||||
7. Push to branch (`git push origin feature/amazing-feature`)
|
||||
8. Open a Pull Request
|
||||
|
||||
### Development Guidelines
|
||||
- Use Rs components instead of custom styling
|
||||
- Follow semantic variant naming (primary, secondary, danger)
|
||||
- Test in both light and dark modes
|
||||
- Maintain accessibility standards
|
||||
- Document new features in user/technical guides
|
||||
|
||||
## 📄 License
|
||||
|
||||
This project is part of the corradAF base project. See the license file for details.
|
||||
|
||||
## 🆘 Support
|
||||
|
||||
- **Technical Issues**: Check the [Technical Guide](docs/Technical_Guide.md)
|
||||
- **User Questions**: Refer to the [User Guide](docs/User_Guide.md)
|
||||
- **Component Usage**: Visit `/dms/design-system` for examples
|
||||
- **Bug Reports**: Create an issue with detailed reproduction steps
|
||||
|
||||
---
|
||||
|
||||
Built with ❤️ using Nuxt.js 3, Vue.js 3, and modern web technologies.
|
||||
|
115
components/RsInput.vue
Normal file
115
components/RsInput.vue
Normal file
@ -0,0 +1,115 @@
|
||||
<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>
|
143
components/RsSelect.vue
Normal file
143
components/RsSelect.vue
Normal file
@ -0,0 +1,143 @@
|
||||
<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>
|
105
components/RsTextarea.vue
Normal file
105
components/RsTextarea.vue
Normal file
@ -0,0 +1,105 @@
|
||||
<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>
|
@ -175,29 +175,24 @@ const submitRequest = async () => {
|
||||
|
||||
<!-- Access Duration Section -->
|
||||
<div class="mb-6">
|
||||
<label class="block text-sm font-medium text-gray-900 dark:text-gray-100 mb-2">Access Duration</label>
|
||||
<select
|
||||
<rs-select
|
||||
v-model="accessDuration"
|
||||
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-700 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"
|
||||
>
|
||||
<option v-for="duration in durationOptions" :key="duration" :value="duration">
|
||||
{{ duration }}
|
||||
</option>
|
||||
</select>
|
||||
:options="durationOptions"
|
||||
label="Access Duration"
|
||||
:required="false"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Justification Section -->
|
||||
<div class="mb-6">
|
||||
<label class="block text-sm font-medium text-gray-900 dark:text-gray-100 mb-2">
|
||||
Justification
|
||||
<span class="text-red-500">*</span>
|
||||
</label>
|
||||
<textarea
|
||||
<rs-textarea
|
||||
v-model="justification"
|
||||
rows="4"
|
||||
label="Justification"
|
||||
placeholder="Please explain why you need access to this document..."
|
||||
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-700 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 resize-none"
|
||||
></textarea>
|
||||
:rows="4"
|
||||
:required="true"
|
||||
resize="none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Footer Note -->
|
||||
@ -209,24 +204,25 @@ const submitRequest = async () => {
|
||||
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-3 px-6 py-4 bg-gray-50 dark:bg-gray-800">
|
||||
<button
|
||||
<rs-button
|
||||
@click="closeDialog"
|
||||
:disabled="isSubmitting"
|
||||
class="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-600 focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
variant="secondary"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
</rs-button>
|
||||
<rs-button
|
||||
@click="submitRequest"
|
||||
:disabled="isSubmitting"
|
||||
class="px-4 py-2 text-sm font-medium text-white bg-gray-900 dark:bg-gray-800 border border-transparent rounded-lg hover:bg-gray-800 dark:hover:bg-gray-700 focus:ring-2 focus:ring-gray-900 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed transition-colors flex items-center"
|
||||
variant="primary"
|
||||
class="flex items-center"
|
||||
>
|
||||
<svg v-if="isSubmitting" class="animate-spin -ml-1 mr-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
Submit Request
|
||||
</button>
|
||||
</rs-button>
|
||||
</div>
|
||||
</template>
|
||||
</rs-modal>
|
||||
|
@ -1,5 +1,6 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { ref, computed, watch, onMounted } from 'vue';
|
||||
import { useDmsStore } from '~/stores/dms';
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
@ -12,472 +13,467 @@ const props = defineProps({
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['close', 'upload', 'update:visible']);
|
||||
const emit = defineEmits(['close', 'upload']);
|
||||
|
||||
// Store
|
||||
const dmsStore = useDmsStore();
|
||||
|
||||
// Local state
|
||||
const isDragging = ref(false);
|
||||
const uploadFiles = ref([]);
|
||||
const isUploading = ref(false);
|
||||
const uploadProgress = ref({});
|
||||
const globalTags = ref(['Urgent', 'Confidential', 'Draft', 'Final', 'Review', 'Archive', 'Public']);
|
||||
const selectedTags = ref({});
|
||||
const customMetadata = ref({});
|
||||
const uploadProgress = ref(0);
|
||||
const selectedFiles = ref([]);
|
||||
const dragCounter = ref(0);
|
||||
const isDragOver = ref(false);
|
||||
const metadataTemplate = ref('standard');
|
||||
const validationErrors = ref({});
|
||||
|
||||
// Computed
|
||||
const hasFiles = computed(() => uploadFiles.value.length > 0);
|
||||
// File metadata - using enhanced system
|
||||
const fileMetadata = ref({});
|
||||
|
||||
// Get metadata template options
|
||||
const templateOptions = computed(() => [
|
||||
{ value: 'standard', label: 'Standard Document' },
|
||||
{ value: 'contract', label: 'Contract/Agreement' },
|
||||
{ value: 'report', label: 'Report/Analysis' }
|
||||
]);
|
||||
|
||||
// Get custom fields from settings
|
||||
const customFields = computed(() => {
|
||||
return dmsStore.systemSettings.metadata.customFields || [];
|
||||
});
|
||||
|
||||
// Get tag suggestions
|
||||
const tagSuggestions = ref([]);
|
||||
|
||||
// Methods
|
||||
const openFileDialog = () => {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.multiple = true;
|
||||
input.accept = dmsStore.systemSettings.upload.allowedFileTypes.map(ext => `.${ext}`).join(',');
|
||||
input.onchange = (e) => handleFiles(Array.from(e.target.files));
|
||||
input.click();
|
||||
};
|
||||
|
||||
const handleFiles = (files) => {
|
||||
const validFiles = [];
|
||||
const errors = {};
|
||||
|
||||
files.forEach(file => {
|
||||
const validation = dmsStore.validateFile(file);
|
||||
if (validation.isValid) {
|
||||
validFiles.push(file);
|
||||
|
||||
// Initialize metadata for each file
|
||||
const template = dmsStore.metadataTemplates[metadataTemplate.value];
|
||||
fileMetadata.value[file.name] = {
|
||||
...template,
|
||||
title: file.name.split('.')[0],
|
||||
author: 'Current User' // Get from auth store
|
||||
};
|
||||
} else {
|
||||
errors[file.name] = validation.errors;
|
||||
}
|
||||
});
|
||||
|
||||
selectedFiles.value = [...selectedFiles.value, ...validFiles];
|
||||
validationErrors.value = { ...validationErrors.value, ...errors };
|
||||
};
|
||||
|
||||
const removeFile = (index) => {
|
||||
const file = selectedFiles.value[index];
|
||||
delete fileMetadata.value[file.name];
|
||||
delete validationErrors.value[file.name];
|
||||
selectedFiles.value.splice(index, 1);
|
||||
};
|
||||
|
||||
const validateMetadata = () => {
|
||||
const errors = {};
|
||||
|
||||
selectedFiles.value.forEach(file => {
|
||||
const metadata = fileMetadata.value[file.name];
|
||||
const validation = dmsStore.validateMetadata(metadata, metadataTemplate.value);
|
||||
|
||||
if (!validation.isValid) {
|
||||
errors[file.name] = validation.errors;
|
||||
}
|
||||
});
|
||||
|
||||
validationErrors.value = { ...validationErrors.value, ...errors };
|
||||
return Object.keys(errors).length === 0;
|
||||
};
|
||||
|
||||
const uploadFiles = async () => {
|
||||
if (!validateMetadata()) {
|
||||
return;
|
||||
}
|
||||
|
||||
isUploading.value = true;
|
||||
uploadProgress.value = 0;
|
||||
|
||||
try {
|
||||
for (let i = 0; i < selectedFiles.value.length; i++) {
|
||||
const file = selectedFiles.value[i];
|
||||
const metadata = fileMetadata.value[file.name];
|
||||
|
||||
// Upload with enhanced metadata
|
||||
await dmsStore.uploadFileWithMetadata(file, metadata, props.currentPath);
|
||||
|
||||
uploadProgress.value = Math.round(((i + 1) / selectedFiles.value.length) * 100);
|
||||
}
|
||||
|
||||
emit('upload', selectedFiles.value);
|
||||
closeDialog();
|
||||
} catch (error) {
|
||||
console.error('Upload failed:', error);
|
||||
// Show error to user
|
||||
} finally {
|
||||
isUploading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const closeDialog = () => {
|
||||
emit('update:visible', false);
|
||||
selectedFiles.value = [];
|
||||
fileMetadata.value = {};
|
||||
validationErrors.value = {};
|
||||
uploadProgress.value = 0;
|
||||
emit('close');
|
||||
};
|
||||
|
||||
const handleDrop = (event) => {
|
||||
event.preventDefault();
|
||||
isDragging.value = false;
|
||||
// Drag and drop handlers
|
||||
const handleDragEnter = (e) => {
|
||||
e.preventDefault();
|
||||
dragCounter.value++;
|
||||
isDragOver.value = true;
|
||||
};
|
||||
|
||||
const handleDragLeave = (e) => {
|
||||
e.preventDefault();
|
||||
dragCounter.value--;
|
||||
if (dragCounter.value === 0) {
|
||||
isDragOver.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleDragOver = (e) => {
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
const handleDrop = (e) => {
|
||||
e.preventDefault();
|
||||
dragCounter.value = 0;
|
||||
isDragOver.value = false;
|
||||
|
||||
const files = Array.from(event.dataTransfer.files);
|
||||
addFiles(files);
|
||||
const files = Array.from(e.dataTransfer.files);
|
||||
handleFiles(files);
|
||||
};
|
||||
|
||||
const handleFileInput = (event) => {
|
||||
const files = Array.from(event.target.files);
|
||||
addFiles(files);
|
||||
// Reset input
|
||||
event.target.value = '';
|
||||
// Tag input handling
|
||||
const handleTagInput = (fileName, input) => {
|
||||
if (input.length > 2) {
|
||||
tagSuggestions.value = dmsStore.getTagSuggestions(input);
|
||||
} else {
|
||||
tagSuggestions.value = [];
|
||||
}
|
||||
};
|
||||
|
||||
const addFiles = (files) => {
|
||||
files.forEach(file => {
|
||||
const fileObj = {
|
||||
id: generateId(),
|
||||
file,
|
||||
name: file.name,
|
||||
size: formatFileSize(file.size),
|
||||
type: file.type,
|
||||
extension: getFileExtension(file.name),
|
||||
progress: 0,
|
||||
status: 'pending', // pending, uploading, completed, error
|
||||
tags: [],
|
||||
metadata: {}
|
||||
};
|
||||
const addTag = (fileName, tag) => {
|
||||
if (!fileMetadata.value[fileName].tags.includes(tag)) {
|
||||
fileMetadata.value[fileName].tags.push(tag);
|
||||
}
|
||||
tagSuggestions.value = [];
|
||||
};
|
||||
|
||||
const removeTag = (fileName, tagIndex) => {
|
||||
fileMetadata.value[fileName].tags.splice(tagIndex, 1);
|
||||
};
|
||||
|
||||
// Watch for template changes
|
||||
watch(metadataTemplate, (newTemplate) => {
|
||||
selectedFiles.value.forEach(file => {
|
||||
const currentMetadata = fileMetadata.value[file.name] || {};
|
||||
const template = dmsStore.metadataTemplates[newTemplate];
|
||||
|
||||
uploadFiles.value.push(fileObj);
|
||||
selectedTags.value[fileObj.id] = [];
|
||||
customMetadata.value[fileObj.id] = {
|
||||
title: '',
|
||||
description: '',
|
||||
category: '',
|
||||
author: '',
|
||||
department: ''
|
||||
// Merge current values with new template
|
||||
fileMetadata.value[file.name] = {
|
||||
...template,
|
||||
...currentMetadata,
|
||||
title: currentMetadata.title || file.name.split('.')[0]
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const removeFile = (fileId) => {
|
||||
const index = uploadFiles.value.findIndex(f => f.id === fileId);
|
||||
if (index !== -1) {
|
||||
uploadFiles.value.splice(index, 1);
|
||||
delete selectedTags.value[fileId];
|
||||
delete customMetadata.value[fileId];
|
||||
delete uploadProgress.value[fileId];
|
||||
}
|
||||
};
|
||||
|
||||
const addTag = (fileId, tag) => {
|
||||
if (!selectedTags.value[fileId].includes(tag)) {
|
||||
selectedTags.value[fileId].push(tag);
|
||||
}
|
||||
};
|
||||
|
||||
const removeTag = (fileId, tag) => {
|
||||
const index = selectedTags.value[fileId].indexOf(tag);
|
||||
if (index !== -1) {
|
||||
selectedTags.value[fileId].splice(index, 1);
|
||||
}
|
||||
};
|
||||
|
||||
const addCustomTag = (fileId, tagInput) => {
|
||||
const tag = tagInput.trim();
|
||||
if (tag && !selectedTags.value[fileId].includes(tag)) {
|
||||
selectedTags.value[fileId].push(tag);
|
||||
}
|
||||
};
|
||||
|
||||
const startUpload = async () => {
|
||||
if (!hasFiles.value || isUploading.value) return;
|
||||
|
||||
isUploading.value = true;
|
||||
|
||||
for (const fileObj of uploadFiles.value) {
|
||||
fileObj.status = 'uploading';
|
||||
|
||||
// Simulate upload progress
|
||||
for (let progress = 0; progress <= 100; progress += 10) {
|
||||
fileObj.progress = progress;
|
||||
uploadProgress.value[fileObj.id] = progress;
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
|
||||
// Prepare file data with metadata and tags
|
||||
const fileData = {
|
||||
file: fileObj.file,
|
||||
name: fileObj.name,
|
||||
path: props.currentPath,
|
||||
tags: selectedTags.value[fileObj.id],
|
||||
metadata: customMetadata.value[fileObj.id]
|
||||
};
|
||||
|
||||
try {
|
||||
// Emit upload event for parent to handle
|
||||
emit('upload', fileData);
|
||||
fileObj.status = 'completed';
|
||||
} catch (error) {
|
||||
fileObj.status = 'error';
|
||||
console.error('Upload failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
isUploading.value = false;
|
||||
|
||||
// Close dialog after successful upload
|
||||
setTimeout(() => {
|
||||
closeDialog();
|
||||
clearFiles();
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
const clearFiles = () => {
|
||||
uploadFiles.value = [];
|
||||
selectedTags.value = {};
|
||||
customMetadata.value = {};
|
||||
uploadProgress.value = {};
|
||||
};
|
||||
|
||||
// Utility functions
|
||||
const generateId = () => {
|
||||
return Date.now().toString(36) + Math.random().toString(36).substr(2);
|
||||
};
|
||||
});
|
||||
|
||||
// Format file size
|
||||
const formatFileSize = (bytes) => {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
return dmsStore.formatFileSize(bytes);
|
||||
};
|
||||
|
||||
const getFileExtension = (filename) => {
|
||||
return filename.split('.').pop().toLowerCase();
|
||||
};
|
||||
|
||||
const getFileIcon = (extension) => {
|
||||
const iconMap = {
|
||||
pdf: 'text-red-500',
|
||||
doc: 'text-blue-500',
|
||||
docx: 'text-blue-500',
|
||||
xls: 'text-green-500',
|
||||
xlsx: 'text-green-500',
|
||||
ppt: 'text-orange-500',
|
||||
pptx: 'text-orange-500',
|
||||
jpg: 'text-purple-500',
|
||||
jpeg: 'text-purple-500',
|
||||
png: 'text-purple-500',
|
||||
gif: 'text-purple-500',
|
||||
txt: 'text-gray-500'
|
||||
};
|
||||
return iconMap[extension] || 'text-gray-500';
|
||||
};
|
||||
|
||||
const getSvgIcon = (iconType, size = 24) => {
|
||||
const icons = {
|
||||
upload: `<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="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>`,
|
||||
file: `<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="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline></svg>`,
|
||||
close: `<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"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>`,
|
||||
tag: `<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.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></svg>`,
|
||||
trash: `<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"><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>`,
|
||||
plus: `<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"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>`
|
||||
};
|
||||
return icons[iconType] || icons.file;
|
||||
// Get field component type
|
||||
const getFieldComponent = (fieldType) => {
|
||||
switch (fieldType) {
|
||||
case 'dropdown':
|
||||
case 'select':
|
||||
return 'select';
|
||||
case 'date':
|
||||
return 'date';
|
||||
case 'number':
|
||||
return 'number';
|
||||
default:
|
||||
return 'text';
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="visible" class="upload-dialog fixed inset-0 z-50 bg-black bg-opacity-50 flex items-center justify-center">
|
||||
<div class="dialog-container bg-white dark:bg-gray-900 rounded-lg shadow-2xl max-w-4xl w-full mx-4 max-h-[90vh] flex flex-col">
|
||||
<!-- Header -->
|
||||
<div class="dialog-header border-b border-gray-200 dark:border-gray-700 p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-xl font-semibold">Upload Documents</h2>
|
||||
<button @click="closeDialog" class="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded">
|
||||
<span v-html="getSvgIcon('close', 20)"></span>
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-sm text-gray-500 mt-1">Upload to: {{ currentPath }}</p>
|
||||
<rs-modal :visible="visible" @close="closeDialog" size="4xl">
|
||||
<template #header>
|
||||
<div class="flex items-center space-x-2">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14 2H6C4.9 2 4 2.9 4 4V20C4 21.1 4.9 22 6 22H18C19.1 22 20 21.1 20 20V8L14 2Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<polyline points="14,2 14,8 20,8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<line x1="16" y1="13" x2="8" y2="13" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<line x1="16" y1="17" x2="8" y2="17" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<polyline points="10,9 9,9 8,9" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">Upload Documents</h3>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="dialog-content flex-1 p-4 overflow-y-auto">
|
||||
<!-- Drop Zone -->
|
||||
<div
|
||||
v-if="!hasFiles"
|
||||
@drop="handleDrop"
|
||||
@dragover.prevent
|
||||
@dragenter="isDragging = true"
|
||||
@dragleave="isDragging = false"
|
||||
class="drop-zone border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg p-8 text-center"
|
||||
:class="{ 'border-blue-500 bg-blue-50 dark:bg-blue-900/20': isDragging }"
|
||||
>
|
||||
<span v-html="getSvgIcon('upload', 48)" class="text-gray-400 mb-4 block mx-auto"></span>
|
||||
<h3 class="text-lg font-semibold mb-2">Drop files here to upload</h3>
|
||||
<p class="text-gray-500 mb-4">or</p>
|
||||
<label for="file-input" class="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 cursor-pointer">
|
||||
Choose Files
|
||||
</template>
|
||||
|
||||
<template #body>
|
||||
<div class="space-y-6">
|
||||
<!-- Template Selection -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Document Template
|
||||
</label>
|
||||
<input
|
||||
id="file-input"
|
||||
type="file"
|
||||
multiple
|
||||
@change="handleFileInput"
|
||||
class="hidden"
|
||||
/>
|
||||
<p class="text-xs text-gray-500 mt-2">Supports all document types, images, and more</p>
|
||||
<select v-model="metadataTemplate"
|
||||
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100">
|
||||
<option v-for="template in templateOptions" :key="template.value" :value="template.value">
|
||||
{{ template.label }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- File List -->
|
||||
<div v-else class="file-list space-y-4">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-semibold">Files to Upload ({{ uploadFiles.length }})</h3>
|
||||
<label for="add-more-files" class="inline-flex items-center px-3 py-1 border border-gray-300 dark:border-gray-600 rounded-md text-sm hover:bg-gray-50 dark:hover:bg-gray-800 cursor-pointer">
|
||||
<span v-html="getSvgIcon('plus', 16)" class="mr-1"></span>
|
||||
Add More
|
||||
</label>
|
||||
<input
|
||||
id="add-more-files"
|
||||
type="file"
|
||||
multiple
|
||||
@change="handleFileInput"
|
||||
class="hidden"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- File Upload Area -->
|
||||
<div class="border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg p-8 text-center"
|
||||
:class="{
|
||||
'border-blue-500 bg-blue-50 dark:bg-blue-900/10': isDragOver,
|
||||
'hover:border-gray-400': !isDragOver
|
||||
}"
|
||||
@dragenter="handleDragEnter"
|
||||
@dragleave="handleDragLeave"
|
||||
@dragover="handleDragOver"
|
||||
@drop="handleDrop">
|
||||
|
||||
<div v-for="fileObj in uploadFiles" :key="fileObj.id" class="file-item bg-gray-50 dark:bg-gray-800 rounded-lg p-4">
|
||||
<div class="flex items-start space-x-3">
|
||||
<!-- File Icon -->
|
||||
<div class="flex-shrink-0">
|
||||
<span v-html="getSvgIcon('file', 24)" :class="getFileIcon(fileObj.extension)"></span>
|
||||
</div>
|
||||
<svg class="mx-auto h-12 w-12 text-gray-400 mb-4" stroke="currentColor" fill="none" viewBox="0 0 48 48">
|
||||
<path d="M28 8H12a4 4 0 00-4 4v20m32-12v8m0 0v8a4 4 0 01-4 4H12a4 4 0 01-4-4v-4m32-4l-3.172-3.172a4 4 0 00-5.656 0L28 28M8 32l9.172-9.172a4 4 0 015.656 0L28 28m0 0l4 4m4-24h8m-4-4v8m-12 4h.02" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
|
||||
<div v-if="isDragOver" class="text-blue-600 dark:text-blue-400">
|
||||
<p class="text-lg font-medium">Drop files here</p>
|
||||
</div>
|
||||
<div v-else>
|
||||
<p class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">
|
||||
Drag and drop files here, or
|
||||
<button @click="openFileDialog" class="text-blue-600 hover:text-blue-500 dark:text-blue-400 dark:hover:text-blue-300">
|
||||
browse
|
||||
</button>
|
||||
</p>
|
||||
<p class="text-sm text-gray-500">
|
||||
Supported: {{ dmsStore.systemSettings.upload.allowedFileTypes.join(', ').toUpperCase() }}
|
||||
</p>
|
||||
<p class="text-sm text-gray-500">
|
||||
Max size: {{ Math.round(dmsStore.systemSettings.upload.maxFileSize / (1024 * 1024)) }}MB per file
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Selected Files -->
|
||||
<div v-if="selectedFiles.length > 0" class="space-y-4">
|
||||
<h4 class="text-md font-semibold text-gray-900 dark:text-gray-100">
|
||||
Selected Files ({{ selectedFiles.length }})
|
||||
</h4>
|
||||
|
||||
<div class="space-y-4 max-h-96 overflow-y-auto">
|
||||
<div v-for="(file, index) in selectedFiles" :key="file.name"
|
||||
class="bg-gray-50 dark:bg-gray-800 rounded-lg p-4 border border-gray-200 dark:border-gray-700">
|
||||
|
||||
<!-- File Info and Metadata -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<div class="flex-1">
|
||||
<h4 class="text-sm font-medium truncate">{{ fileObj.name }}</h4>
|
||||
<p class="text-xs text-gray-500">{{ fileObj.size }} • {{ fileObj.extension.toUpperCase() }}</p>
|
||||
</div>
|
||||
<button @click="removeFile(fileObj.id)" class="ml-2 p-1 hover:bg-gray-200 dark:hover:bg-gray-700 rounded">
|
||||
<span v-html="getSvgIcon('trash', 16)" class="text-red-500"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Progress Bar -->
|
||||
<div v-if="fileObj.status === 'uploading'" class="mb-3">
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<span class="text-xs text-gray-500">Uploading...</span>
|
||||
<span class="text-xs text-gray-500">{{ fileObj.progress }}%</span>
|
||||
</div>
|
||||
<div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
|
||||
<div
|
||||
class="bg-blue-600 h-2 rounded-full transition-all duration-300"
|
||||
:style="{ width: `${fileObj.progress}%` }"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status -->
|
||||
<div v-if="fileObj.status === 'completed'" class="mb-3">
|
||||
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800">
|
||||
✓ Uploaded
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-if="fileObj.status === 'error'" class="mb-3">
|
||||
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-red-100 text-red-800">
|
||||
✗ Error
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Metadata Form -->
|
||||
<div v-if="fileObj.status === 'pending'" class="space-y-3">
|
||||
<!-- Basic Metadata -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">Title</label>
|
||||
<input
|
||||
v-model="customMetadata[fileObj.id].title"
|
||||
type="text"
|
||||
class="w-full px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded focus:ring-1 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700"
|
||||
:placeholder="fileObj.name"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">Category</label>
|
||||
<select
|
||||
v-model="customMetadata[fileObj.id].category"
|
||||
class="w-full px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded focus:ring-1 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700"
|
||||
>
|
||||
<option value="">Select category</option>
|
||||
<option value="contract">Contract</option>
|
||||
<option value="report">Report</option>
|
||||
<option value="invoice">Invoice</option>
|
||||
<option value="specification">Specification</option>
|
||||
<option value="manual">Manual</option>
|
||||
<option value="other">Other</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">Author</label>
|
||||
<input
|
||||
v-model="customMetadata[fileObj.id].author"
|
||||
type="text"
|
||||
class="w-full px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded focus:ring-1 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700"
|
||||
placeholder="Document author"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">Department</label>
|
||||
<input
|
||||
v-model="customMetadata[fileObj.id].department"
|
||||
type="text"
|
||||
class="w-full px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded focus:ring-1 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700"
|
||||
placeholder="Responsible department"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- File Header -->
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="flex items-center space-x-3">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14 2H6C4.9 2 4 2.9 4 4V20C4 21.1 4.9 22 6 22H18C19.1 22 20 21.1 20 20V8L14 2Z" stroke="currentColor" stroke-width="2"/>
|
||||
<polyline points="14,2 14,8 20,8" stroke="currentColor" stroke-width="2"/>
|
||||
</svg>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">Description</label>
|
||||
<textarea
|
||||
v-model="customMetadata[fileObj.id].description"
|
||||
rows="2"
|
||||
class="w-full px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded focus:ring-1 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700"
|
||||
placeholder="Brief description of the document"
|
||||
></textarea>
|
||||
<p class="font-medium text-gray-900 dark:text-gray-100">{{ file.name }}</p>
|
||||
<p class="text-sm text-gray-500">{{ formatFileSize(file.size) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<button @click="removeFile(index)"
|
||||
class="text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M18 6L6 18M6 6L18 18" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Validation Errors -->
|
||||
<div v-if="validationErrors[file.name]" class="mb-4 p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded">
|
||||
<p class="text-sm font-medium text-red-800 dark:text-red-200 mb-1">Validation Errors:</p>
|
||||
<ul class="text-sm text-red-700 dark:text-red-300 list-disc list-inside">
|
||||
<li v-for="error in validationErrors[file.name]" :key="error">{{ error }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Metadata Form -->
|
||||
<div v-if="fileMetadata[file.name]" class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
|
||||
<!-- Dynamic Custom Fields -->
|
||||
<div v-for="field in customFields" :key="field.name"
|
||||
:class="field.type === 'text' && field.name === 'Description' ? 'md:col-span-2' : ''">
|
||||
<label class="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
{{ field.name }}
|
||||
<span v-if="field.required" class="text-red-500">*</span>
|
||||
</label>
|
||||
|
||||
<!-- Tags -->
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">Tags</label>
|
||||
<div class="flex flex-wrap gap-1 mb-2">
|
||||
<button
|
||||
v-for="tag in globalTags"
|
||||
:key="tag"
|
||||
@click="addTag(fileObj.id, tag)"
|
||||
class="inline-flex items-center px-2 py-1 rounded-full text-xs border"
|
||||
:class="selectedTags[fileObj.id]?.includes(tag)
|
||||
? 'bg-blue-100 text-blue-800 border-blue-200'
|
||||
: 'bg-gray-100 text-gray-800 border-gray-200 hover:bg-gray-200'"
|
||||
>
|
||||
<span v-html="getSvgIcon('tag', 12)" class="mr-1"></span>
|
||||
{{ tag }}
|
||||
</button>
|
||||
<!-- Dropdown/Select Field -->
|
||||
<select v-if="field.type === 'dropdown' || field.type === 'select'"
|
||||
v-model="fileMetadata[file.name][field.name]"
|
||||
class="w-full px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded focus:ring-1 focus:ring-blue-500 dark:bg-gray-700">
|
||||
<option value="">Select {{ field.name }}</option>
|
||||
<option v-for="option in field.options" :key="option" :value="option">
|
||||
{{ option }}
|
||||
</option>
|
||||
</select>
|
||||
|
||||
<!-- Date Field -->
|
||||
<input v-else-if="field.type === 'date'"
|
||||
v-model="fileMetadata[file.name][field.name]"
|
||||
type="date"
|
||||
class="w-full px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded focus:ring-1 focus:ring-blue-500 dark:bg-gray-700">
|
||||
|
||||
<!-- Number Field -->
|
||||
<input v-else-if="field.type === 'number'"
|
||||
v-model="fileMetadata[file.name][field.name]"
|
||||
type="number"
|
||||
class="w-full px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded focus:ring-1 focus:ring-blue-500 dark:bg-gray-700">
|
||||
|
||||
<!-- Text Field -->
|
||||
<input v-else
|
||||
v-model="fileMetadata[file.name][field.name]"
|
||||
type="text"
|
||||
class="w-full px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded focus:ring-1 focus:ring-blue-500 dark:bg-gray-700"
|
||||
:placeholder="`Enter ${field.name.toLowerCase()}`">
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<div class="md:col-span-2">
|
||||
<label class="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">Description</label>
|
||||
<textarea v-model="fileMetadata[file.name].description"
|
||||
rows="2"
|
||||
class="w-full px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded focus:ring-1 focus:ring-blue-500 dark:bg-gray-700"
|
||||
placeholder="Brief description of the document"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Tags -->
|
||||
<div class="md:col-span-2">
|
||||
<label class="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">Tags</label>
|
||||
<div class="space-y-2">
|
||||
<!-- Tag Input -->
|
||||
<div class="relative">
|
||||
<input type="text"
|
||||
@input="handleTagInput(file.name, $event.target.value)"
|
||||
@keyup.enter="addTag(file.name, $event.target.value); $event.target.value = ''"
|
||||
class="w-full px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded focus:ring-1 focus:ring-blue-500 dark:bg-gray-700"
|
||||
placeholder="Type and press Enter to add tags">
|
||||
|
||||
<!-- Tag Suggestions -->
|
||||
<div v-if="tagSuggestions.length > 0"
|
||||
class="absolute z-10 w-full mt-1 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-md shadow-lg max-h-40 overflow-y-auto">
|
||||
<button v-for="suggestion in tagSuggestions" :key="suggestion"
|
||||
@click="addTag(file.name, suggestion)"
|
||||
class="w-full px-3 py-2 text-left text-sm hover:bg-gray-100 dark:hover:bg-gray-700 focus:bg-gray-100 dark:focus:bg-gray-700">
|
||||
{{ suggestion }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Selected Tags -->
|
||||
<div v-if="selectedTags[fileObj.id]?.length" class="flex flex-wrap gap-1 mb-2">
|
||||
<span
|
||||
v-for="tag in selectedTags[fileObj.id]"
|
||||
:key="tag"
|
||||
class="inline-flex items-center px-2 py-1 rounded-full text-xs bg-blue-100 text-blue-800"
|
||||
>
|
||||
<div v-if="fileMetadata[file.name].tags && fileMetadata[file.name].tags.length > 0"
|
||||
class="flex flex-wrap gap-2">
|
||||
<span v-for="(tag, tagIndex) in fileMetadata[file.name].tags" :key="tagIndex"
|
||||
class="inline-flex items-center px-2 py-1 text-xs bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300 rounded-full">
|
||||
{{ tag }}
|
||||
<button @click="removeTag(fileObj.id, tag)" class="ml-1 hover:text-blue-600">
|
||||
<span v-html="getSvgIcon('close', 12)"></span>
|
||||
<button @click="removeTag(file.name, tagIndex)"
|
||||
class="ml-1 text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-200">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M18 6L6 18M6 6L18 18" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Custom Tag Input -->
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
@keyup.enter="addCustomTag(fileObj.id, $event.target.value); $event.target.value = ''"
|
||||
type="text"
|
||||
class="flex-1 px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded focus:ring-1 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700"
|
||||
placeholder="Add custom tag and press Enter"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div v-if="hasFiles" class="dialog-footer border-t border-gray-200 dark:border-gray-700 p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<button @click="clearFiles" class="text-sm text-gray-500 hover:text-gray-700">
|
||||
Clear All
|
||||
</button>
|
||||
<div class="flex space-x-2">
|
||||
<rs-button variant="secondary" @click="closeDialog">Cancel</rs-button>
|
||||
<rs-button
|
||||
@click="startUpload"
|
||||
:disabled="isUploading"
|
||||
class="inline-flex items-center"
|
||||
>
|
||||
<span v-if="isUploading" class="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></span>
|
||||
<span v-html="getSvgIcon('upload', 16)" class="mr-2" v-else></span>
|
||||
{{ isUploading ? 'Uploading...' : 'Upload Files' }}
|
||||
</rs-button>
|
||||
|
||||
<!-- Upload Progress -->
|
||||
<div v-if="isUploading" class="space-y-2">
|
||||
<div class="flex justify-between text-sm text-gray-600 dark:text-gray-400">
|
||||
<span>Uploading...</span>
|
||||
<span>{{ uploadProgress }}%</span>
|
||||
</div>
|
||||
<div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
|
||||
<div class="bg-blue-600 h-2 rounded-full transition-all duration-300"
|
||||
:style="{ width: `${uploadProgress}%` }"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="text-sm text-gray-500">
|
||||
Storage: {{ Math.round(dmsStore.userQuotas.used / (1024 * 1024)) }}MB /
|
||||
{{ Math.round(dmsStore.userQuotas.limit / (1024 * 1024)) }}MB used
|
||||
</div>
|
||||
<div class="flex space-x-3">
|
||||
<rs-button @click="closeDialog" variant="secondary" :disabled="isUploading">
|
||||
Cancel
|
||||
</rs-button>
|
||||
<rs-button @click="uploadFiles"
|
||||
:disabled="selectedFiles.length === 0 || isUploading || Object.keys(validationErrors).length > 0">
|
||||
<svg v-if="isUploading" 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>
|
||||
{{ isUploading ? 'Uploading...' : `Upload ${selectedFiles.length} File${selectedFiles.length !== 1 ? 's' : ''}` }}
|
||||
</rs-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</rs-modal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.upload-dialog {
|
||||
backdrop-filter: blur(4px);
|
||||
/* Enhanced scrollbar styling */
|
||||
.overflow-y-auto::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.dialog-container {
|
||||
max-height: 90vh;
|
||||
}
|
||||
|
||||
.dialog-content {
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.drop-zone {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.drop-zone:hover {
|
||||
border-color: #3B82F6;
|
||||
background-color: rgba(59, 130, 246, 0.05);
|
||||
}
|
||||
|
||||
/* Custom scrollbar */
|
||||
.dialog-content::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.dialog-content::-webkit-scrollbar-track {
|
||||
.overflow-y-auto::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.dialog-content::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(156, 163, 175, 0.5);
|
||||
border-radius: 4px;
|
||||
.overflow-y-auto::-webkit-scrollbar-thumb {
|
||||
background: #d1d5db;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.dialog-content::-webkit-scrollbar-thumb:hover {
|
||||
background-color: rgba(156, 163, 175, 0.7);
|
||||
.overflow-y-auto::-webkit-scrollbar-thumb:hover {
|
||||
background: #9ca3af;
|
||||
}
|
||||
</style>
|
543
components/dms/search/DMSAdvancedSearch.vue
Normal file
543
components/dms/search/DMSAdvancedSearch.vue
Normal file
@ -0,0 +1,543 @@
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import { useDmsStore } from '~/stores/dms';
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['close', 'search']);
|
||||
|
||||
// Store
|
||||
const dmsStore = useDmsStore();
|
||||
|
||||
// Search state
|
||||
const searchQuery = ref('');
|
||||
const isLoading = ref(false);
|
||||
const searchResults = ref([]);
|
||||
const hasSearched = ref(false);
|
||||
|
||||
// Advanced filters
|
||||
const filters = ref({
|
||||
fileType: [],
|
||||
department: [],
|
||||
tags: [],
|
||||
priority: [],
|
||||
author: [],
|
||||
dateRange: { start: null, end: null },
|
||||
hasVersions: false,
|
||||
accessType: []
|
||||
});
|
||||
|
||||
// Filter options
|
||||
const fileTypeOptions = computed(() => {
|
||||
const types = new Set();
|
||||
dmsStore.items.filter(item => item.type === 'file').forEach(item => {
|
||||
if (item.extension) types.add(item.extension);
|
||||
});
|
||||
return Array.from(types).map(type => ({ value: type, label: type.toUpperCase() }));
|
||||
});
|
||||
|
||||
const departmentOptions = computed(() => {
|
||||
const departments = new Set();
|
||||
dmsStore.items.forEach(item => {
|
||||
if (item.department) departments.add(item.department);
|
||||
});
|
||||
return Array.from(departments).map(dept => ({ value: dept, label: dept }));
|
||||
});
|
||||
|
||||
const priorityOptions = computed(() => [
|
||||
{ value: 'Low', label: 'Low' },
|
||||
{ value: 'Medium', label: 'Medium' },
|
||||
{ value: 'High', label: 'High' },
|
||||
{ value: 'Critical', label: 'Critical' }
|
||||
]);
|
||||
|
||||
const authorOptions = computed(() => {
|
||||
const authors = new Set();
|
||||
dmsStore.items.forEach(item => {
|
||||
if (item.author) authors.add(item.author);
|
||||
});
|
||||
return Array.from(authors).map(author => ({ value: author, label: author }));
|
||||
});
|
||||
|
||||
const accessTypeOptions = computed(() => [
|
||||
{ value: 'public', label: 'Public' },
|
||||
{ value: 'private', label: 'Private' },
|
||||
{ value: 'personal', label: 'Personal' }
|
||||
]);
|
||||
|
||||
const availableTags = computed(() => {
|
||||
const tags = new Set();
|
||||
dmsStore.items.forEach(item => {
|
||||
if (item.tags) {
|
||||
item.tags.forEach(tag => tags.add(tag));
|
||||
}
|
||||
});
|
||||
return Array.from(tags);
|
||||
});
|
||||
|
||||
// Methods
|
||||
const performSearch = async () => {
|
||||
isLoading.value = true;
|
||||
hasSearched.value = true;
|
||||
|
||||
try {
|
||||
const results = await dmsStore.advancedSearch(searchQuery.value, filters.value);
|
||||
searchResults.value = results;
|
||||
emit('search', { query: searchQuery.value, filters: filters.value, results });
|
||||
} catch (error) {
|
||||
console.error('Search failed:', error);
|
||||
searchResults.value = [];
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const clearSearch = () => {
|
||||
searchQuery.value = '';
|
||||
filters.value = {
|
||||
fileType: [],
|
||||
department: [],
|
||||
tags: [],
|
||||
priority: [],
|
||||
author: [],
|
||||
dateRange: { start: null, end: null },
|
||||
hasVersions: false,
|
||||
accessType: []
|
||||
};
|
||||
searchResults.value = [];
|
||||
hasSearched.value = false;
|
||||
dmsStore.clearSearch();
|
||||
};
|
||||
|
||||
const closeDialog = () => {
|
||||
emit('close');
|
||||
};
|
||||
|
||||
const selectDocument = (document) => {
|
||||
dmsStore.selectItem(document);
|
||||
closeDialog();
|
||||
};
|
||||
|
||||
const formatFileSize = (size) => {
|
||||
return dmsStore.formatFileSize(size);
|
||||
};
|
||||
|
||||
const getFileIcon = (extension) => {
|
||||
const iconMap = {
|
||||
pdf: 'text-red-500',
|
||||
doc: 'text-blue-500',
|
||||
docx: 'text-blue-500',
|
||||
xls: 'text-green-500',
|
||||
xlsx: 'text-green-500',
|
||||
ppt: 'text-orange-500',
|
||||
pptx: 'text-orange-500',
|
||||
jpg: 'text-purple-500',
|
||||
jpeg: 'text-purple-500',
|
||||
png: 'text-purple-500'
|
||||
};
|
||||
return iconMap[extension] || 'text-gray-500';
|
||||
};
|
||||
|
||||
// Tag input handling
|
||||
const tagInput = ref('');
|
||||
const addTag = () => {
|
||||
const tag = tagInput.value.trim();
|
||||
if (tag && !filters.value.tags.includes(tag)) {
|
||||
filters.value.tags.push(tag);
|
||||
tagInput.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
const removeTag = (index) => {
|
||||
filters.value.tags.splice(index, 1);
|
||||
};
|
||||
|
||||
// Quick search presets
|
||||
const applyQuickFilter = (type) => {
|
||||
clearSearch();
|
||||
|
||||
switch (type) {
|
||||
case 'recent':
|
||||
// Documents modified in last 7 days
|
||||
const lastWeek = new Date();
|
||||
lastWeek.setDate(lastWeek.getDate() - 7);
|
||||
filters.value.dateRange.start = lastWeek.toISOString().split('T')[0];
|
||||
break;
|
||||
case 'confidential':
|
||||
filters.value.tags = ['confidential'];
|
||||
break;
|
||||
case 'contracts':
|
||||
filters.value.tags = ['contract'];
|
||||
filters.value.department = ['Legal'];
|
||||
break;
|
||||
case 'versioned':
|
||||
filters.value.hasVersions = true;
|
||||
break;
|
||||
}
|
||||
|
||||
performSearch();
|
||||
};
|
||||
|
||||
// Auto-search when Enter is pressed
|
||||
const handleKeyup = (event) => {
|
||||
if (event.key === 'Enter') {
|
||||
performSearch();
|
||||
}
|
||||
};
|
||||
|
||||
// File size conversion helper
|
||||
const fileSizeToNumber = (sizeStr) => {
|
||||
if (typeof sizeStr === 'number') return sizeStr;
|
||||
const match = sizeStr.match(/^([\d.]+)\s*(B|KB|MB|GB)$/i);
|
||||
if (!match) return 0;
|
||||
|
||||
const value = parseFloat(match[1]);
|
||||
const unit = match[2].toUpperCase();
|
||||
|
||||
switch (unit) {
|
||||
case 'GB': return value * 1024 * 1024 * 1024;
|
||||
case 'MB': return value * 1024 * 1024;
|
||||
case 'KB': return value * 1024;
|
||||
default: return value;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<rs-modal :visible="visible" @close="closeDialog" size="5xl">
|
||||
<template #header>
|
||||
<div class="flex items-center space-x-2">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="11" cy="11" r="8" stroke="currentColor" stroke-width="2"/>
|
||||
<path d="M21 21l-4.35-4.35" stroke="currentColor" stroke-width="2"/>
|
||||
</svg>
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">Advanced Search</h3>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #body>
|
||||
<div class="space-y-6">
|
||||
<!-- Search Input -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Search Query
|
||||
</label>
|
||||
<div class="relative">
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
@keyup="handleKeyup"
|
||||
type="text"
|
||||
class="w-full pl-10 pr-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
|
||||
placeholder="Search documents, descriptions, authors..."
|
||||
/>
|
||||
<svg class="absolute left-3 top-2.5 h-5 w-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Filters -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Quick Filters
|
||||
</label>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button @click="applyQuickFilter('recent')"
|
||||
class="px-3 py-1 text-sm bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300 rounded-full hover:bg-blue-200 dark:hover:bg-blue-900/50">
|
||||
Recent (7 days)
|
||||
</button>
|
||||
<button @click="applyQuickFilter('confidential')"
|
||||
class="px-3 py-1 text-sm bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300 rounded-full hover:bg-red-200 dark:hover:bg-red-900/50">
|
||||
Confidential
|
||||
</button>
|
||||
<button @click="applyQuickFilter('contracts')"
|
||||
class="px-3 py-1 text-sm bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300 rounded-full hover:bg-green-200 dark:hover:bg-green-900/50">
|
||||
Contracts
|
||||
</button>
|
||||
<button @click="applyQuickFilter('versioned')"
|
||||
class="px-3 py-1 text-sm bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-300 rounded-full hover:bg-purple-200 dark:hover:bg-purple-900/50">
|
||||
Versioned Documents
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Advanced Filters -->
|
||||
<div class="bg-gray-50 dark:bg-gray-800 rounded-lg p-4 space-y-4">
|
||||
<h4 class="text-md font-semibold text-gray-900 dark:text-gray-100">Advanced Filters</h4>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<!-- File Type Filter -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
File Type
|
||||
</label>
|
||||
<select v-model="filters.fileType" multiple
|
||||
class="w-full px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700">
|
||||
<option v-for="option in fileTypeOptions" :key="option.value" :value="option.value">
|
||||
{{ option.label }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Department Filter -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Department
|
||||
</label>
|
||||
<select v-model="filters.department" multiple
|
||||
class="w-full px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700">
|
||||
<option v-for="option in departmentOptions" :key="option.value" :value="option.value">
|
||||
{{ option.label }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Priority Filter -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Priority
|
||||
</label>
|
||||
<select v-model="filters.priority" multiple
|
||||
class="w-full px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700">
|
||||
<option v-for="option in priorityOptions" :key="option.value" :value="option.value">
|
||||
{{ option.label }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Author Filter -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Author
|
||||
</label>
|
||||
<select v-model="filters.author" multiple
|
||||
class="w-full px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700">
|
||||
<option v-for="option in authorOptions" :key="option.value" :value="option.value">
|
||||
{{ option.label }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Access Type Filter -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Access Type
|
||||
</label>
|
||||
<select v-model="filters.accessType" multiple
|
||||
class="w-full px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700">
|
||||
<option v-for="option in accessTypeOptions" :key="option.value" :value="option.value">
|
||||
{{ option.label }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Has Versions Filter -->
|
||||
<div class="flex items-center">
|
||||
<label class="flex items-center space-x-2 text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
<input type="checkbox" v-model="filters.hasVersions"
|
||||
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500">
|
||||
<span>Has Multiple Versions</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
From Date
|
||||
</label>
|
||||
<input v-model="filters.dateRange.start" type="date"
|
||||
class="w-full px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
To Date
|
||||
</label>
|
||||
<input v-model="filters.dateRange.end" type="date"
|
||||
class="w-full px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tags Filter -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Tags
|
||||
</label>
|
||||
<div class="space-y-2">
|
||||
<!-- Tag Input -->
|
||||
<div class="flex space-x-2">
|
||||
<input v-model="tagInput"
|
||||
@keyup.enter="addTag"
|
||||
type="text"
|
||||
class="flex-1 px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700"
|
||||
placeholder="Type tag and press Enter">
|
||||
<button @click="addTag"
|
||||
class="px-3 py-1 text-sm bg-blue-600 text-white rounded hover:bg-blue-700">
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Selected Tags -->
|
||||
<div v-if="filters.tags.length > 0" class="flex flex-wrap gap-2">
|
||||
<span v-for="(tag, index) in filters.tags" :key="index"
|
||||
class="inline-flex items-center px-2 py-1 text-xs bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300 rounded-full">
|
||||
{{ tag }}
|
||||
<button @click="removeTag(index)"
|
||||
class="ml-1 text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-200">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M18 6L6 18M6 6L18 18" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Available Tags -->
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">
|
||||
Available tags:
|
||||
<button v-for="tag in availableTags.slice(0, 10)" :key="tag"
|
||||
@click="filters.tags.includes(tag) ? null : filters.tags.push(tag)"
|
||||
class="mx-1 px-1 hover:text-blue-600 dark:hover:text-blue-400"
|
||||
:class="filters.tags.includes(tag) ? 'text-blue-600 dark:text-blue-400' : ''">
|
||||
{{ tag }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search Results -->
|
||||
<div v-if="hasSearched" class="space-y-4">
|
||||
<div class="flex justify-between items-center">
|
||||
<h4 class="text-md font-semibold text-gray-900 dark:text-gray-100">
|
||||
Search Results ({{ searchResults.length }})
|
||||
</h4>
|
||||
<button @click="clearSearch"
|
||||
class="text-sm text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
|
||||
Clear Results
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Loading State -->
|
||||
<div v-if="isLoading" class="flex items-center justify-center py-8">
|
||||
<div class="flex items-center space-x-2">
|
||||
<div class="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-600"></div>
|
||||
<span class="text-gray-500">Searching...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Results List -->
|
||||
<div v-else-if="searchResults.length > 0" class="max-h-96 overflow-y-auto space-y-2">
|
||||
<div v-for="document in searchResults" :key="document.id"
|
||||
@click="selectDocument(document)"
|
||||
class="p-3 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 cursor-pointer">
|
||||
<div class="flex items-center space-x-3">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"
|
||||
:class="getFileIcon(document.extension)">
|
||||
<path d="M14 2H6C4.9 2 4 2.9 4 4V20C4 21.1 4.9 22 6 22H18C19.1 22 20 21.1 20 20V8L14 2Z" stroke="currentColor" stroke-width="2"/>
|
||||
<polyline points="14,2 14,8 20,8" stroke="currentColor" stroke-width="2"/>
|
||||
</svg>
|
||||
|
||||
<div class="flex-1 min-w-0">
|
||||
<h5 class="text-sm font-medium text-gray-900 dark:text-gray-100 truncate">
|
||||
{{ document.name }}
|
||||
</h5>
|
||||
<div class="flex items-center space-x-4 text-xs text-gray-500 dark:text-gray-400">
|
||||
<span>{{ document.size }}</span>
|
||||
<span v-if="document.department">{{ document.department }}</span>
|
||||
<span v-if="document.author">{{ document.author }}</span>
|
||||
<span>{{ document.modified }}</span>
|
||||
<span v-if="document.version > 1" class="bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300 px-1 rounded">
|
||||
v{{ document.version }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="document.description" class="text-xs text-gray-600 dark:text-gray-400 mt-1 truncate">
|
||||
{{ document.description }}
|
||||
</div>
|
||||
<div v-if="document.tags && document.tags.length > 0" class="flex flex-wrap gap-1 mt-1">
|
||||
<span v-for="tag in document.tags.slice(0, 3)" :key="tag"
|
||||
class="inline-block px-1 py-0.5 text-xs bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300 rounded">
|
||||
{{ tag }}
|
||||
</span>
|
||||
<span v-if="document.tags.length > 3" class="text-xs text-gray-500">
|
||||
+{{ document.tags.length - 3 }} more
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- No Results -->
|
||||
<div v-else class="text-center py-8">
|
||||
<svg class="mx-auto h-12 w-12 text-gray-400 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-sm font-medium text-gray-900 dark:text-gray-100 mb-1">No documents found</h3>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">Try adjusting your search criteria</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ dmsStore.items.filter(item => item.type === 'file').length }} total documents
|
||||
</div>
|
||||
<div class="flex space-x-3">
|
||||
<rs-button @click="clearSearch" variant="secondary">
|
||||
Clear
|
||||
</rs-button>
|
||||
<rs-button @click="performSearch" :disabled="isLoading">
|
||||
<svg v-if="isLoading" 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>
|
||||
{{ isLoading ? 'Searching...' : 'Search' }}
|
||||
</rs-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</rs-modal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* Multi-select styling */
|
||||
select[multiple] {
|
||||
height: auto;
|
||||
min-height: 2.5rem;
|
||||
}
|
||||
|
||||
select[multiple] option {
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
|
||||
select[multiple] option:checked {
|
||||
background: linear-gradient(#3b82f6, #3b82f6);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Enhanced scrollbar styling */
|
||||
.overflow-y-auto::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.overflow-y-auto::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.overflow-y-auto::-webkit-scrollbar-thumb {
|
||||
background: #d1d5db;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.overflow-y-auto::-webkit-scrollbar-thumb:hover {
|
||||
background: #9ca3af;
|
||||
}
|
||||
</style>
|
@ -427,153 +427,302 @@ edms/
|
||||
- **RsButton.vue**: Standardized button component with variants
|
||||
- **RsCard.vue**: Container component with consistent styling
|
||||
- **RsModal.vue**: Modal dialog base component
|
||||
- **RsInput.vue**: Standardized input field component
|
||||
- **RsSelect.vue**: Standardized dropdown/select component
|
||||
- **RsTextarea.vue**: Standardized textarea component
|
||||
- **RsTable.vue**: Data table with sorting and filtering
|
||||
- **RsDropdown.vue**: Dropdown menu component
|
||||
|
||||
### State Management (Pinia)
|
||||
## Design System
|
||||
|
||||
#### DMS Store (`stores/dms.js`)
|
||||
```javascript
|
||||
export const useDmsStore = defineStore('dms', {
|
||||
state: () => ({
|
||||
// Navigation state
|
||||
currentPath: '/',
|
||||
pathHistory: [],
|
||||
currentItems: [],
|
||||
selectedItem: null,
|
||||
|
||||
// UI state
|
||||
treeExpanded: {},
|
||||
searchQuery: '',
|
||||
viewMode: 'list', // list, grid, details
|
||||
sortOrder: 'asc',
|
||||
sortField: 'name',
|
||||
activeTab: 'all', // all, public, private, personal
|
||||
|
||||
// Loading states
|
||||
isLoading: false,
|
||||
isUploading: false,
|
||||
uploadProgress: 0,
|
||||
|
||||
// Dialog states
|
||||
showUploadDialog: false,
|
||||
showAccessRequestDialog: false,
|
||||
accessRequestItem: null
|
||||
}),
|
||||
|
||||
actions: {
|
||||
// Navigation actions
|
||||
async navigateTo(path) {
|
||||
this.isLoading = true
|
||||
try {
|
||||
this.pathHistory.push(this.currentPath)
|
||||
this.currentPath = path
|
||||
await this.loadItems()
|
||||
} finally {
|
||||
this.isLoading = false
|
||||
}
|
||||
},
|
||||
|
||||
async loadItems() {
|
||||
const response = await $fetch('/api/dms/items', {
|
||||
query: {
|
||||
path: this.currentPath,
|
||||
search: this.searchQuery,
|
||||
tab: this.activeTab
|
||||
}
|
||||
})
|
||||
this.currentItems = response.items
|
||||
},
|
||||
|
||||
// File operations
|
||||
async uploadFile(file, metadata) {
|
||||
this.isUploading = true
|
||||
try {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
formData.append('metadata', JSON.stringify(metadata))
|
||||
formData.append('path', this.currentPath)
|
||||
|
||||
const response = await $fetch('/api/dms/upload', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
onUploadProgress: (progress) => {
|
||||
this.uploadProgress = Math.round((progress.loaded / progress.total) * 100)
|
||||
}
|
||||
})
|
||||
|
||||
await this.loadItems()
|
||||
return response
|
||||
} finally {
|
||||
this.isUploading = false
|
||||
this.uploadProgress = 0
|
||||
}
|
||||
},
|
||||
|
||||
async requestAccess(item, requestData) {
|
||||
return await $fetch('/api/dms/access-request', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
documentId: item.id,
|
||||
requestedLevel: requestData.accessType,
|
||||
duration: requestData.duration,
|
||||
justification: requestData.justification
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
async searchDocuments(query) {
|
||||
this.searchQuery = query
|
||||
await this.loadItems()
|
||||
}
|
||||
},
|
||||
|
||||
getters: {
|
||||
filteredItems: (state) => {
|
||||
let items = state.currentItems
|
||||
|
||||
// Apply tab filter
|
||||
if (state.activeTab !== 'all') {
|
||||
items = items.filter(item => item.accessType === state.activeTab)
|
||||
}
|
||||
|
||||
// Apply search filter
|
||||
if (state.searchQuery) {
|
||||
items = items.filter(item =>
|
||||
item.name.toLowerCase().includes(state.searchQuery.toLowerCase()) ||
|
||||
item.description?.toLowerCase().includes(state.searchQuery.toLowerCase())
|
||||
)
|
||||
}
|
||||
|
||||
// Apply sorting
|
||||
items.sort((a, b) => {
|
||||
const aVal = a[state.sortField]
|
||||
const bVal = b[state.sortField]
|
||||
const modifier = state.sortOrder === 'asc' ? 1 : -1
|
||||
return aVal < bVal ? -modifier : aVal > bVal ? modifier : 0
|
||||
})
|
||||
|
||||
return items
|
||||
},
|
||||
|
||||
currentBreadcrumbs: (state) => {
|
||||
const parts = state.currentPath.split('/').filter(Boolean)
|
||||
return parts.map((part, index) => ({
|
||||
name: part,
|
||||
path: '/' + parts.slice(0, index + 1).join('/')
|
||||
}))
|
||||
},
|
||||
|
||||
canNavigateBack: (state) => state.pathHistory.length > 0
|
||||
},
|
||||
|
||||
persist: {
|
||||
storage: persistedState.localStorage,
|
||||
pick: ['treeExpanded', 'viewMode', 'sortOrder', 'sortField']
|
||||
}
|
||||
})
|
||||
### Overview
|
||||
The EDMS implements a comprehensive design system built on a standardized component library with the "Rs" prefix (Reusable System). This ensures consistency, maintainability, and accessibility across the entire application.
|
||||
|
||||
### Design Principles
|
||||
- **🎯 Consistency**: All components follow unified design patterns and naming conventions
|
||||
- **🔧 Modularity**: Components are reusable and composable for complex interfaces
|
||||
- **🌙 Dark Mode**: Universal support for both light and dark themes via CSS variables
|
||||
- **📱 Responsive**: Mobile-first approach with adaptive layouts
|
||||
- **♿ Accessibility**: WCAG-compliant with proper ARIA attributes and keyboard navigation
|
||||
|
||||
### Component Library
|
||||
|
||||
#### Form Components
|
||||
|
||||
##### RsInput.vue
|
||||
```vue
|
||||
<rs-input
|
||||
v-model="value"
|
||||
label="Field Label"
|
||||
placeholder="Enter text..."
|
||||
:required="true"
|
||||
:error="validationError"
|
||||
size="md"
|
||||
:disabled="false"
|
||||
/>
|
||||
```
|
||||
|
||||
**Props:**
|
||||
- `modelValue`: String/Number - Input value
|
||||
- `label`: String - Field label
|
||||
- `placeholder`: String - Placeholder text
|
||||
- `type`: String - Input type (text, email, password, etc.)
|
||||
- `required`: Boolean - Required field indicator
|
||||
- `error`: String - Error message
|
||||
- `size`: String - Size variant (sm, md, lg)
|
||||
- `disabled`: Boolean - Disabled state
|
||||
|
||||
##### RsSelect.vue
|
||||
```vue
|
||||
<rs-select
|
||||
v-model="selectedValue"
|
||||
:options="optionsList"
|
||||
label="Select Option"
|
||||
placeholder="Choose..."
|
||||
:multiple="false"
|
||||
:required="true"
|
||||
/>
|
||||
```
|
||||
|
||||
**Props:**
|
||||
- `modelValue`: String/Number/Array - Selected value(s)
|
||||
- `options`: Array - Options list (objects with value/label or simple strings)
|
||||
- `label`: String - Field label
|
||||
- `placeholder`: String - Placeholder text
|
||||
- `multiple`: Boolean - Multiple selection support
|
||||
- `required`: Boolean - Required field indicator
|
||||
- `error`: String - Error message
|
||||
- `disabled`: Boolean - Disabled state
|
||||
|
||||
##### RsTextarea.vue
|
||||
```vue
|
||||
<rs-textarea
|
||||
v-model="content"
|
||||
label="Message"
|
||||
placeholder="Enter message..."
|
||||
:rows="4"
|
||||
resize="vertical"
|
||||
:required="true"
|
||||
/>
|
||||
```
|
||||
|
||||
**Props:**
|
||||
- `modelValue`: String - Textarea content
|
||||
- `label`: String - Field label
|
||||
- `placeholder`: String - Placeholder text
|
||||
- `rows`: Number - Number of visible rows
|
||||
- `resize`: String - Resize behavior (none, both, horizontal, vertical)
|
||||
- `required`: Boolean - Required field indicator
|
||||
- `error`: String - Error message
|
||||
- `disabled`: Boolean - Disabled state
|
||||
|
||||
#### UI Components
|
||||
|
||||
##### RsButton.vue
|
||||
```vue
|
||||
<rs-button
|
||||
variant="primary"
|
||||
size="md"
|
||||
:disabled="false"
|
||||
@click="handleClick"
|
||||
>
|
||||
Button Text
|
||||
</rs-button>
|
||||
```
|
||||
|
||||
**Variants:**
|
||||
- `primary`: Blue primary button
|
||||
- `secondary`: Gray secondary button
|
||||
- `info`: Blue info button
|
||||
- `success`: Green success button
|
||||
- `warning`: Yellow warning button
|
||||
- `danger`: Red danger button
|
||||
- `primary-outline`: Outlined primary button
|
||||
- `secondary-outline`: Outlined secondary button
|
||||
- `primary-text`: Text-only primary button
|
||||
|
||||
**Sizes:**
|
||||
- `sm`: Small button (padding: 0.25rem 0.75rem)
|
||||
- `md`: Medium button (padding: 0.5rem 1rem)
|
||||
- `lg`: Large button (padding: 0.75rem 1.25rem)
|
||||
|
||||
##### RsCard.vue
|
||||
```vue
|
||||
<rs-card>
|
||||
<template #header>
|
||||
Card Header
|
||||
</template>
|
||||
<template #body>
|
||||
Card content goes here
|
||||
</template>
|
||||
<template #footer>
|
||||
Card footer with actions
|
||||
</template>
|
||||
</rs-card>
|
||||
```
|
||||
|
||||
##### RsModal.vue
|
||||
```vue
|
||||
<rs-modal :visible="showModal" @close="closeModal" size="md">
|
||||
<template #header>
|
||||
Modal Title
|
||||
</template>
|
||||
<template #body>
|
||||
Modal content
|
||||
</template>
|
||||
<template #footer>
|
||||
<rs-button variant="secondary" @click="closeModal">Cancel</rs-button>
|
||||
<rs-button variant="primary" @click="confirm">Confirm</rs-button>
|
||||
</template>
|
||||
</rs-modal>
|
||||
```
|
||||
|
||||
**Sizes:**
|
||||
- `sm`: 300px width
|
||||
- `md`: 500px width
|
||||
- `lg`: 800px width
|
||||
- `xl`: 1000px width
|
||||
- `full`: Full screen width
|
||||
|
||||
### Color System
|
||||
|
||||
The design system uses CSS custom properties for consistent theming:
|
||||
|
||||
```css
|
||||
:root {
|
||||
/* Primary Colors */
|
||||
--color-primary: 59 130 246; /* Blue */
|
||||
--color-secondary: 100 116 139; /* Gray */
|
||||
--color-info: 14 165 233; /* Sky Blue */
|
||||
--color-success: 34 197 94; /* Green */
|
||||
--color-warning: 251 191 36; /* Yellow */
|
||||
--color-danger: 239 68 68; /* Red */
|
||||
|
||||
/* Background Colors */
|
||||
--bg-1: 248 250 252; /* Light Gray */
|
||||
--bg-2: 255 255 255; /* White */
|
||||
|
||||
/* Text Colors */
|
||||
--text-color: 15 23 42; /* Slate 900 */
|
||||
--text-muted: 100 116 139; /* Slate 500 */
|
||||
|
||||
/* Border Colors */
|
||||
--border-color: 226 232 240; /* Slate 200 */
|
||||
--fk-border-color: 209 213 219; /* Gray 300 */
|
||||
}
|
||||
```
|
||||
|
||||
### Typography Scale
|
||||
|
||||
```css
|
||||
/* Font Sizes */
|
||||
.text-xs { font-size: 0.75rem; } /* 12px */
|
||||
.text-sm { font-size: 0.875rem; } /* 14px */
|
||||
.text-base { font-size: 1rem; } /* 16px */
|
||||
.text-lg { font-size: 1.125rem; } /* 18px */
|
||||
.text-xl { font-size: 1.25rem; } /* 20px */
|
||||
.text-2xl { font-size: 1.5rem; } /* 24px */
|
||||
.text-3xl { font-size: 1.875rem; } /* 30px */
|
||||
|
||||
/* Font Weights */
|
||||
.font-light { font-weight: 300; }
|
||||
.font-normal { font-weight: 400; }
|
||||
.font-medium { font-weight: 500; }
|
||||
.font-semibold { font-weight: 600; }
|
||||
.font-bold { font-weight: 700; }
|
||||
```
|
||||
|
||||
### Spacing Scale
|
||||
|
||||
```css
|
||||
/* Spacing (padding/margin) */
|
||||
.p-1 { padding: 0.25rem; } /* 4px */
|
||||
.p-2 { padding: 0.5rem; } /* 8px */
|
||||
.p-3 { padding: 0.75rem; } /* 12px */
|
||||
.p-4 { padding: 1rem; } /* 16px */
|
||||
.p-6 { padding: 1.5rem; } /* 24px */
|
||||
.p-8 { padding: 2rem; } /* 32px */
|
||||
```
|
||||
|
||||
### Usage Guidelines
|
||||
|
||||
#### ✅ Best Practices
|
||||
1. **Always use Rs components** for UI elements instead of custom styling
|
||||
2. **Use semantic variants** (primary, secondary, danger) rather than color names
|
||||
3. **Follow consistent spacing** using the predefined scale
|
||||
4. **Use CSS variables** for colors instead of hardcoded values
|
||||
5. **Test in both themes** (light and dark mode)
|
||||
6. **Maintain accessibility** with proper labels and ARIA attributes
|
||||
|
||||
#### ❌ Anti-patterns
|
||||
1. **Don't use manual Tailwind classes** for buttons (e.g., `px-4 py-2 bg-blue-500`)
|
||||
2. **Don't create custom input styling** without extending Rs components
|
||||
3. **Don't hardcode colors** or spacing values
|
||||
4. **Don't mix different component styling approaches**
|
||||
5. **Don't forget dark mode** support in custom components
|
||||
|
||||
### FormKit Integration
|
||||
|
||||
The design system integrates with FormKit for advanced form handling:
|
||||
|
||||
```javascript
|
||||
// FormKit theme configuration
|
||||
export default {
|
||||
global: {
|
||||
label: "formkit-label-global",
|
||||
outer: "formkit-outer-global",
|
||||
wrapper: "formkit-wrapper-global",
|
||||
},
|
||||
button: {
|
||||
wrapper: "formkit-wrapper-button",
|
||||
input: "formkit-input-button",
|
||||
},
|
||||
text: {
|
||||
label: "formkit-outer-text",
|
||||
inner: "formkit-inner-text",
|
||||
input: "formkit-input-text",
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Component Registration
|
||||
|
||||
All Rs components are globally registered via the component index:
|
||||
|
||||
```javascript
|
||||
// pages/devtool/code-playground/index.js
|
||||
export {
|
||||
RsAlert,
|
||||
RsBadge,
|
||||
RsButton,
|
||||
RsCard,
|
||||
RsInput,
|
||||
RsModal,
|
||||
RsSelect,
|
||||
RsTextarea,
|
||||
RsTable,
|
||||
// ... other components
|
||||
};
|
||||
```
|
||||
|
||||
### Design System Documentation
|
||||
|
||||
Access the interactive design system documentation at `/dms/design-system` to:
|
||||
- View all components with live examples
|
||||
- Test different variants and states
|
||||
- Copy code snippets for implementation
|
||||
- Understand usage guidelines and best practices
|
||||
|
||||
### Future Enhancements
|
||||
|
||||
Planned design system improvements:
|
||||
- **Animation Library**: Consistent micro-interactions and transitions
|
||||
- **Icon System**: Standardized icon library with consistent sizing
|
||||
- **Layout Components**: Grid and flexbox utilities
|
||||
- **Advanced Form Components**: Date pickers, multi-select, autocomplete
|
||||
- **Data Visualization**: Chart and graph components
|
||||
- **Notification System**: Toast and alert components
|
||||
|
||||
## API & Data Management
|
||||
|
||||
### Server API Routes
|
||||
|
@ -10,7 +10,8 @@
|
||||
7. [Search and Filtering](#search-and-filtering)
|
||||
8. [Document Viewer](#document-viewer)
|
||||
9. [My Documents](#my-documents)
|
||||
10. [Troubleshooting](#troubleshooting)
|
||||
10. [Settings Administration](#settings-administration)
|
||||
11. [Troubleshooting](#troubleshooting)
|
||||
|
||||
## Introduction
|
||||
|
||||
@ -290,6 +291,210 @@ View comprehensive information about your document usage:
|
||||
- Document categories and type distribution
|
||||
- Most accessed documents and usage patterns
|
||||
|
||||
## Settings Administration
|
||||
|
||||
### 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.
|
||||
|
||||
### Accessing Settings
|
||||
1. Navigate to **DMS** → **Settings** from the main navigation menu
|
||||
2. Select a settings category from the left sidebar navigation
|
||||
3. Configure settings within each category using standardized form components
|
||||
4. Save changes to apply configurations across the system
|
||||
|
||||
### Settings Categories
|
||||
|
||||
The EDMS settings are organized into 5 core categories for streamlined administration:
|
||||
|
||||
#### 🔐 User & Access Management
|
||||
Configure user permissions, roles, and authentication settings:
|
||||
|
||||
**User Roles & Permissions**
|
||||
- Define user roles (Admin, Editor, Viewer, Uploader)
|
||||
- Set granular permissions for each role:
|
||||
- View Documents: Read-only access to document content
|
||||
- Edit Documents: Modify document metadata and properties
|
||||
- Delete Documents: Remove documents with audit trail
|
||||
- Download Documents: Save documents for offline access
|
||||
- Share Documents: Generate sharing links and manage access
|
||||
- Manage user groups by department or project
|
||||
- Enable Role-Based Access Control (RBAC) system-wide
|
||||
|
||||
**Authentication Settings**
|
||||
- Single Sign-On (SSO) integration with external providers
|
||||
- Multi-Factor Authentication (MFA) enforcement
|
||||
- LDAP/Active Directory integration for enterprise environments
|
||||
- Session timeout configuration (1-24 hours)
|
||||
- Password policies and security requirements
|
||||
|
||||
#### 📁 Document & Folder Settings
|
||||
Configure document organization and structure:
|
||||
|
||||
**Folder Hierarchy**
|
||||
- Set maximum folder depth (1-10 levels)
|
||||
- Choose default folder structure templates:
|
||||
- Standard: General-purpose organization
|
||||
- Project-based: Project-focused hierarchy
|
||||
- Department-based: Organizational structure alignment
|
||||
- Custom: User-defined structure
|
||||
- Configure folder creation permissions and restrictions
|
||||
|
||||
**Document Naming Conventions**
|
||||
- Enable automatic document name generation
|
||||
- Define mandatory metadata fields for document creation
|
||||
- Set naming patterns using variables:
|
||||
- `{department}`: Document department/owner
|
||||
- `{title}`: Document title or name
|
||||
- `{date}`: Creation or upload date
|
||||
- `{project}`: Project code or identifier
|
||||
- `{category}`: Document category/type
|
||||
|
||||
**Document Retention & Version Control**
|
||||
- Enable document retention policies with configurable retention periods
|
||||
- Set automatic archiving before deletion
|
||||
- Configure version control system:
|
||||
- Maximum versions to retain (1-50)
|
||||
- Automatic versioning on document updates
|
||||
- Version history tracking and comparison tools
|
||||
|
||||
#### 📝 Metadata & Tagging
|
||||
Configure document metadata and classification:
|
||||
|
||||
**Custom Metadata Fields**
|
||||
Define custom fields for enhanced document organization:
|
||||
- Field types: Text, Dropdown, Date, Number, Multi-select
|
||||
- Required field enforcement for document uploads
|
||||
- Department-specific metadata schemas
|
||||
- Dynamic field validation and formatting rules
|
||||
|
||||
**Predefined Metadata Fields**
|
||||
- Department: Organizational unit assignment
|
||||
- Priority: Document importance level (Low, Medium, High, Critical)
|
||||
- Project Code: Project or initiative identifier
|
||||
- Review Date: Scheduled review or expiration date
|
||||
|
||||
**Tagging System**
|
||||
- Configure predefined tags: urgent, confidential, public, draft, final
|
||||
- Enable user-generated tags for flexible categorization
|
||||
- Tag suggestion system for consistent tagging
|
||||
- Auto-classification based on:
|
||||
- Keyword recognition in document content
|
||||
- Department-based classification rules
|
||||
- File type and format analysis
|
||||
|
||||
#### 📤 Upload & Storage Settings
|
||||
Configure file upload restrictions and storage management:
|
||||
|
||||
**File Type Management**
|
||||
- **Allowed File Types**: Specify permitted file formats
|
||||
- Default: pdf, doc, docx, xls, xlsx, ppt, pptx, txt, jpg, png
|
||||
- Configurable list for organizational requirements
|
||||
- **Blocked File Types**: Prevent potentially harmful files
|
||||
- Default: exe, bat, cmd (executable files)
|
||||
- Customizable blacklist for security compliance
|
||||
|
||||
**File Size & Storage Quotas**
|
||||
- Maximum file size per upload (typically 100MB)
|
||||
- User storage quotas (default: 5GB per user)
|
||||
- Group storage quotas (default: 50GB per group)
|
||||
- Project storage quotas (default: 100GB per project)
|
||||
- System-wide storage monitoring and alerts
|
||||
|
||||
**Storage Configuration**
|
||||
- Storage type selection (Local, AWS S3, Azure, Google Cloud)
|
||||
- Storage path configuration for file organization
|
||||
- Backup settings and automated backup scheduling
|
||||
- Compression settings for storage optimization
|
||||
|
||||
#### 📅 System Settings
|
||||
Configure global system behavior and preferences:
|
||||
|
||||
**General System Configuration**
|
||||
- **System Timezone**: Set default timezone for timestamps
|
||||
- Options: Asia/Kuala_Lumpur, UTC, America/New_York, Europe/London
|
||||
- Affects document timestamps, scheduling, and user interface
|
||||
- **Backup Schedule**: Configure automated system backups
|
||||
- Options: Hourly, Daily, Weekly, Monthly
|
||||
- Affects data protection and recovery capabilities
|
||||
- **Log Level**: Set system logging verbosity
|
||||
- Options: Debug, Info, Warning, Error
|
||||
- Affects troubleshooting and system monitoring
|
||||
|
||||
**System Monitoring & Maintenance**
|
||||
- **Maintenance Mode**: Enable system-wide maintenance mode
|
||||
- Displays maintenance message to users
|
||||
- Restricts access during system updates
|
||||
- **Automatic Updates**: Configure automatic system updates
|
||||
- Security patches and feature updates
|
||||
- Scheduled during low-usage periods
|
||||
- **System Monitoring**: Enable performance monitoring
|
||||
- Track system performance metrics
|
||||
- Monitor user activity and system health
|
||||
- Generate usage reports and analytics
|
||||
|
||||
### Settings Management Features
|
||||
|
||||
#### Import/Export Configuration
|
||||
- **Export Settings**: Save current configuration as JSON file
|
||||
- Backup settings before major changes
|
||||
- Transfer settings between environments
|
||||
- Documentation and compliance purposes
|
||||
- **Import Settings**: Load configuration from JSON file
|
||||
- Restore previous configurations
|
||||
- Deploy settings across multiple systems
|
||||
- Standardize configurations across environments
|
||||
|
||||
#### Reset to Defaults
|
||||
- **Category Reset**: Reset individual setting categories to default values
|
||||
- **System Reset**: Reset entire system configuration to factory defaults
|
||||
- **Selective Reset**: Choose specific settings to reset while preserving others
|
||||
|
||||
#### Settings Validation
|
||||
- **Real-time Validation**: Form validation prevents invalid configurations
|
||||
- **Dependency Checking**: Settings are validated for interdependencies
|
||||
- **Conflict Resolution**: System identifies and resolves setting conflicts
|
||||
- **Preview Mode**: Test settings before applying system-wide
|
||||
|
||||
### Best Practices for Settings Management
|
||||
|
||||
#### Security Considerations
|
||||
- Regularly review user permissions and access levels
|
||||
- Enable MFA for administrative accounts
|
||||
- Use strong session timeout values for security
|
||||
- Regularly audit user roles and group memberships
|
||||
|
||||
#### Storage Management
|
||||
- Monitor storage quotas and usage patterns
|
||||
- Implement retention policies for compliance
|
||||
- Regular backup verification and testing
|
||||
- Plan for storage scaling based on usage growth
|
||||
|
||||
#### System Performance
|
||||
- Monitor log levels to balance debugging needs with performance
|
||||
- Schedule maintenance during off-peak hours
|
||||
- Regular cleanup of temporary files and old versions
|
||||
- Performance monitoring for system optimization
|
||||
|
||||
#### Documentation & Compliance
|
||||
- Document all configuration changes with justification
|
||||
- Maintain change logs for audit purposes
|
||||
- Regular export of settings for backup and documentation
|
||||
- Review settings for compliance with organizational policies
|
||||
|
||||
### Troubleshooting Settings Issues
|
||||
|
||||
#### Common Settings Problems
|
||||
- **Permission Conflicts**: Resolve conflicting role assignments
|
||||
- **Upload Failures**: Check file type restrictions and size limits
|
||||
- **Authentication Issues**: Verify SSO and authentication settings
|
||||
- **Storage Problems**: Monitor quotas and storage allocation
|
||||
|
||||
#### Settings Recovery
|
||||
- Use exported settings files to restore configurations
|
||||
- Reset specific categories if issues persist
|
||||
- Contact system administrator for complex configuration issues
|
||||
- Review audit logs for recent configuration changes
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues and Solutions
|
||||
|
@ -7,5 +7,5 @@ export default defineNuxtRouteMiddleware(async (to, from) => {
|
||||
// If user is not logged in, redirect to logout page
|
||||
if (validateUser.value.statusCode === 401) return true;
|
||||
|
||||
return navigateTo("/dashboard");
|
||||
return navigateTo("/dms");
|
||||
});
|
||||
|
@ -1,3 +1,14 @@
|
||||
export default defineNuxtRouteMiddleware((to, from) => {
|
||||
export default defineNuxtRouteMiddleware(async (to, from) => {
|
||||
// Check if user is authenticated
|
||||
const { data: validateUser } = await useFetch("/api/auth/validate", {
|
||||
method: "GET",
|
||||
});
|
||||
|
||||
// If user is authenticated, redirect to DMS
|
||||
if (validateUser.value.statusCode === 200) {
|
||||
return navigateTo("/dms");
|
||||
}
|
||||
|
||||
// If user is not authenticated, redirect to login
|
||||
return navigateTo("/login");
|
||||
});
|
@ -1,19 +1,4 @@
|
||||
export default [
|
||||
{
|
||||
"header": "Utama",
|
||||
"description": "",
|
||||
"child": [
|
||||
{
|
||||
"title": "Dashboard",
|
||||
"path": "/dashboard",
|
||||
"icon": "ic:outline-dashboard",
|
||||
"child": [],
|
||||
"meta": {}
|
||||
},
|
||||
|
||||
],
|
||||
"meta": {}
|
||||
},
|
||||
{
|
||||
"header": "DMS",
|
||||
"description": "Document Management System",
|
||||
|
@ -8,11 +8,14 @@ import RsCollapseItem from "../../../components/RsCollapseItem.vue";
|
||||
import RsDropdown from "../../../components/RsDropdown.vue";
|
||||
import RsDropdownItem from "../../../components/RsDropdownItem.vue";
|
||||
import RsFieldset from "../../../components/RsFieldset.vue";
|
||||
import RsInput from "../../../components/RsInput.vue";
|
||||
import RsModal from "../../../components/RsModal.vue";
|
||||
import RsProgressBar from "../../../components/RsProgressBar.vue";
|
||||
import RsSelect from "../../../components/RsSelect.vue";
|
||||
import RsTab from "../../../components/RsTab.vue";
|
||||
import RsTabItem from "../../../components/RsTabItem.vue";
|
||||
import RsTable from "../../../components/RsTable.vue";
|
||||
import RsTextarea from "../../../components/RsTextarea.vue";
|
||||
import RsWizard from "../../../components/RsWizard.vue";
|
||||
|
||||
export {
|
||||
@ -26,10 +29,13 @@ export {
|
||||
RsDropdown,
|
||||
RsDropdownItem,
|
||||
RsFieldset,
|
||||
RsInput,
|
||||
RsModal,
|
||||
RsProgressBar,
|
||||
RsSelect,
|
||||
RsTab,
|
||||
RsTabItem,
|
||||
RsTable,
|
||||
RsTextarea,
|
||||
RsWizard,
|
||||
};
|
||||
|
277
pages/dms/design-system.vue
Normal file
277
pages/dms/design-system.vue
Normal file
@ -0,0 +1,277 @@
|
||||
<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><rs-modal :visible="showModal" @close="closeModal">
|
||||
<template #header>
|
||||
Modal Title
|
||||
</template>
|
||||
<template #body>
|
||||
Modal content here
|
||||
</template>
|
||||
<template #footer>
|
||||
<rs-button variant="secondary">Cancel</rs-button>
|
||||
<rs-button variant="primary">Confirm</rs-button>
|
||||
</template>
|
||||
</rs-modal></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>
|
@ -1,364 +0,0 @@
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useDmsStore } from '~/stores/dms';
|
||||
import DMSNavigation from '~/components/dms/navigation/DMSNavigation.vue';
|
||||
|
||||
// Define page metadata
|
||||
definePageMeta({
|
||||
title: "Document Properties",
|
||||
middleware: ["auth"],
|
||||
requiresAuth: true,
|
||||
breadcrumb: [
|
||||
{
|
||||
name: "Dashboard",
|
||||
path: "/dashboard",
|
||||
},
|
||||
{
|
||||
name: "DMS",
|
||||
path: "/dms",
|
||||
},
|
||||
{
|
||||
name: "Document Properties",
|
||||
path: "/dms/document-properties",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Set up store
|
||||
const dmsStore = useDmsStore();
|
||||
|
||||
// Local state
|
||||
const currentDocument = ref(null);
|
||||
const isEditing = ref(false);
|
||||
const documentForm = ref({
|
||||
title: '',
|
||||
description: '',
|
||||
keywords: '',
|
||||
category: '',
|
||||
status: '',
|
||||
retention: '',
|
||||
owner: '',
|
||||
department: '',
|
||||
created: '',
|
||||
modified: '',
|
||||
fileSize: '',
|
||||
version: '',
|
||||
versionHistory: []
|
||||
});
|
||||
|
||||
// Mock document data
|
||||
const documentData = {
|
||||
id: 'doc123',
|
||||
title: 'Pembangunan Sistem IT 2021',
|
||||
description: 'Spesifikasi teknikal untuk pembangunan sistem IT baru di JKR Kota Bharu',
|
||||
keywords: 'IT, sistem, JKR, spesifikasi, pembangunan',
|
||||
category: 'Technical Specification',
|
||||
status: 'Approved',
|
||||
retention: '7 years',
|
||||
owner: 'Mohd Faizal bin Abdullah',
|
||||
department: 'IT Department',
|
||||
created: '2021-05-20',
|
||||
modified: '2021-05-25',
|
||||
fileSize: '4MB',
|
||||
filePath: '/JKR Cawangan Kota Bharu, Kelantan/Technical Documents/',
|
||||
fileName: 'Pembangunan_Sistem_IT_2021.pdf',
|
||||
fileType: 'PDF',
|
||||
version: '1.2',
|
||||
versionHistory: [
|
||||
{ version: '1.2', date: '2021-05-25', user: 'Mohd Faizal bin Abdullah', notes: 'Final approved version' },
|
||||
{ version: '1.1', date: '2021-05-22', user: 'Ahmad bin Ishak', notes: 'Updated requirements section' },
|
||||
{ version: '1.0', date: '2021-05-20', user: 'Mohd Faizal bin Abdullah', notes: 'Initial draft' }
|
||||
]
|
||||
};
|
||||
|
||||
// Enable editing
|
||||
const startEditing = () => {
|
||||
// Copy current document values to form
|
||||
documentForm.value = { ...currentDocument.value };
|
||||
isEditing.value = true;
|
||||
};
|
||||
|
||||
// Save changes
|
||||
const saveChanges = () => {
|
||||
// In a real app, we would send the updated data to the server
|
||||
// For now, we'll just update our local copy
|
||||
Object.assign(currentDocument.value, documentForm.value);
|
||||
isEditing.value = false;
|
||||
};
|
||||
|
||||
// Cancel editing
|
||||
const cancelEditing = () => {
|
||||
isEditing.value = false;
|
||||
};
|
||||
|
||||
// Load document on mount
|
||||
onMounted(() => {
|
||||
// In a real app, we would fetch the document from an API
|
||||
// For now, we'll use our mock data
|
||||
currentDocument.value = documentData;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="dms-page">
|
||||
<LayoutsBreadcrumb />
|
||||
|
||||
<rs-card class="h-full">
|
||||
<template #header>
|
||||
<div class="flex flex-wrap items-center justify-between gap-4">
|
||||
<h1 class="text-xl font-bold text-primary">Document Properties</h1>
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
<rs-button v-if="!isEditing" color="primary" @click="startEditing">
|
||||
<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="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>
|
||||
Edit Properties
|
||||
</rs-button>
|
||||
<div v-else class="flex gap-2">
|
||||
<rs-button color="primary" @click="saveChanges">
|
||||
<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="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>
|
||||
Save
|
||||
</rs-button>
|
||||
<rs-button color="secondary" @click="cancelEditing">
|
||||
<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"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
|
||||
Cancel
|
||||
</rs-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #body>
|
||||
<div class="explorer-layout h-full flex overflow-hidden">
|
||||
<!-- Left sidebar navigation -->
|
||||
<DMSNavigation />
|
||||
|
||||
<!-- Main content area -->
|
||||
<div class="flex-1 overflow-y-auto p-6">
|
||||
<div v-if="currentDocument" class="max-w-4xl mx-auto">
|
||||
<div class="bg-white dark:bg-gray-800 shadow-sm rounded-lg overflow-hidden mb-6">
|
||||
<div class="bg-gray-50 dark:bg-gray-700 px-6 py-4 border-b border-gray-200 dark:border-gray-600">
|
||||
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100 flex items-center">
|
||||
<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" class="mr-2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><line x1="16" y1="13" x2="8" y2="13"></line><line x1="16" y1="17" x2="8" y2="17"></line><polyline points="10 9 9 9 8 9"></polyline></svg>
|
||||
File Information
|
||||
</h2>
|
||||
</div>
|
||||
<div class="px-6 py-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">File Name</label>
|
||||
<div class="text-gray-900 dark:text-gray-100">{{ currentDocument.fileName }}</div>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">File Type</label>
|
||||
<div class="text-gray-900 dark:text-gray-100">{{ currentDocument.fileType }}</div>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">File Size</label>
|
||||
<div class="text-gray-900 dark:text-gray-100">{{ currentDocument.fileSize }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">File Path</label>
|
||||
<div class="text-gray-900 dark:text-gray-100">{{ currentDocument.filePath }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Created Date</label>
|
||||
<div class="text-gray-900 dark:text-gray-100">{{ currentDocument.created }}</div>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Modified Date</label>
|
||||
<div class="text-gray-900 dark:text-gray-100">{{ currentDocument.modified }}</div>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Current Version</label>
|
||||
<div class="text-gray-900 dark:text-gray-100">{{ currentDocument.version }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white dark:bg-gray-800 shadow-sm rounded-lg overflow-hidden mb-6">
|
||||
<div class="bg-gray-50 dark:bg-gray-700 px-6 py-4 border-b border-gray-200 dark:border-gray-600">
|
||||
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100 flex items-center">
|
||||
<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" class="mr-2"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg>
|
||||
Document Metadata
|
||||
</h2>
|
||||
</div>
|
||||
<div class="px-6 py-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Title</label>
|
||||
<div v-if="!isEditing" class="text-gray-900 dark:text-gray-100">{{ currentDocument.title }}</div>
|
||||
<input
|
||||
v-else
|
||||
v-model="documentForm.title"
|
||||
type="text"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-primary focus:ring focus:ring-primary focus:ring-opacity-50"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Description</label>
|
||||
<div v-if="!isEditing" class="text-gray-900 dark:text-gray-100">{{ currentDocument.description }}</div>
|
||||
<textarea
|
||||
v-else
|
||||
v-model="documentForm.description"
|
||||
rows="3"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-primary focus:ring focus:ring-primary focus:ring-opacity-50"
|
||||
></textarea>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Keywords</label>
|
||||
<div v-if="!isEditing" class="text-gray-900 dark:text-gray-100">{{ currentDocument.keywords }}</div>
|
||||
<input
|
||||
v-else
|
||||
v-model="documentForm.keywords"
|
||||
type="text"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-primary focus:ring focus:ring-primary focus:ring-opacity-50"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Category</label>
|
||||
<div v-if="!isEditing" class="text-gray-900 dark:text-gray-100">{{ currentDocument.category }}</div>
|
||||
<select
|
||||
v-else
|
||||
v-model="documentForm.category"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-primary focus:ring focus:ring-primary focus:ring-opacity-50"
|
||||
>
|
||||
<option value="Technical Specification">Technical Specification</option>
|
||||
<option value="Project Proposal">Project Proposal</option>
|
||||
<option value="Contract">Contract</option>
|
||||
<option value="Report">Report</option>
|
||||
<option value="Other">Other</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Status</label>
|
||||
<div v-if="!isEditing" class="text-gray-900 dark:text-gray-100">{{ currentDocument.status }}</div>
|
||||
<select
|
||||
v-else
|
||||
v-model="documentForm.status"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-primary focus:ring focus:ring-primary focus:ring-opacity-50"
|
||||
>
|
||||
<option value="Draft">Draft</option>
|
||||
<option value="Under Review">Under Review</option>
|
||||
<option value="Approved">Approved</option>
|
||||
<option value="Rejected">Rejected</option>
|
||||
<option value="Archived">Archived</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Retention Period</label>
|
||||
<div v-if="!isEditing" class="text-gray-900 dark:text-gray-100">{{ currentDocument.retention }}</div>
|
||||
<select
|
||||
v-else
|
||||
v-model="documentForm.retention"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-primary focus:ring focus:ring-primary focus:ring-opacity-50"
|
||||
>
|
||||
<option value="1 year">1 year</option>
|
||||
<option value="3 years">3 years</option>
|
||||
<option value="5 years">5 years</option>
|
||||
<option value="7 years">7 years</option>
|
||||
<option value="10 years">10 years</option>
|
||||
<option value="Permanent">Permanent</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white dark:bg-gray-800 shadow-sm rounded-lg overflow-hidden mb-6">
|
||||
<div class="bg-gray-50 dark:bg-gray-700 px-6 py-4 border-b border-gray-200 dark:border-gray-600">
|
||||
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100 flex items-center">
|
||||
<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" class="mr-2"><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>
|
||||
Ownership Information
|
||||
</h2>
|
||||
</div>
|
||||
<div class="px-6 py-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Owner</label>
|
||||
<div v-if="!isEditing" class="text-gray-900 dark:text-gray-100">{{ currentDocument.owner }}</div>
|
||||
<input
|
||||
v-else
|
||||
v-model="documentForm.owner"
|
||||
type="text"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-primary focus:ring focus:ring-primary focus:ring-opacity-50"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Department</label>
|
||||
<div v-if="!isEditing" class="text-gray-900 dark:text-gray-100">{{ currentDocument.department }}</div>
|
||||
<input
|
||||
v-else
|
||||
v-model="documentForm.department"
|
||||
type="text"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-primary focus:ring focus:ring-primary focus:ring-opacity-50"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white dark:bg-gray-800 shadow-sm rounded-lg overflow-hidden">
|
||||
<div class="bg-gray-50 dark:bg-gray-700 px-6 py-4 border-b border-gray-200 dark:border-gray-600">
|
||||
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100 flex items-center">
|
||||
<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" class="mr-2"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg>
|
||||
Version History
|
||||
</h2>
|
||||
</div>
|
||||
<div class="px-6 py-4">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" class="px-6 py-3 bg-gray-50 dark:bg-gray-700 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Version</th>
|
||||
<th scope="col" class="px-6 py-3 bg-gray-50 dark:bg-gray-700 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Date</th>
|
||||
<th scope="col" class="px-6 py-3 bg-gray-50 dark:bg-gray-700 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">User</th>
|
||||
<th scope="col" class="px-6 py-3 bg-gray-50 dark:bg-gray-700 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Notes</th>
|
||||
<th scope="col" class="px-6 py-3 bg-gray-50 dark:bg-gray-700 text-right text-xs font-medium text-gray-500 dark:text-gray-300 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="(version, index) in currentDocument.versionHistory" :key="index">
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">{{ version.version }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">{{ version.date }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">{{ version.user }}</td>
|
||||
<td class="px-6 py-4 text-sm text-gray-900 dark:text-gray-100">{{ version.notes }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||
<rs-button size="xs" color="secondary">View</rs-button>
|
||||
<rs-button v-if="index > 0" size="xs" color="primary" class="ml-2">Restore</rs-button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</rs-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.dms-page {
|
||||
height: calc(100vh - 64px - 48px - 32px); /* Adjust based on your layout */
|
||||
}
|
||||
|
||||
.explorer-layout {
|
||||
height: calc(100vh - 200px); /* Adjust based on your layout */
|
||||
}
|
||||
</style>
|
@ -10,10 +10,6 @@ definePageMeta({
|
||||
middleware: ["auth"],
|
||||
requiresAuth: true,
|
||||
breadcrumb: [
|
||||
{
|
||||
name: "Dashboard",
|
||||
path: "/dashboard",
|
||||
},
|
||||
{
|
||||
name: "DMS",
|
||||
path: "/dms",
|
||||
|
@ -1,371 +0,0 @@
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useDmsStore } from '~/stores/dms';
|
||||
import DMSNavigation from '~/components/dms/navigation/DMSNavigation.vue';
|
||||
|
||||
// Define page metadata
|
||||
definePageMeta({
|
||||
title: "My Documents",
|
||||
middleware: ["auth"],
|
||||
requiresAuth: true,
|
||||
breadcrumb: [
|
||||
{
|
||||
name: "Dashboard",
|
||||
path: "/dashboard",
|
||||
},
|
||||
{
|
||||
name: "DMS",
|
||||
path: "/dms",
|
||||
},
|
||||
{
|
||||
name: "My Documents",
|
||||
path: "/dms/my-documents",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Set up store
|
||||
const dmsStore = useDmsStore();
|
||||
|
||||
// Local state
|
||||
const searchQuery = ref('');
|
||||
const isSearching = ref(false);
|
||||
const currentPath = ref('My Documents');
|
||||
const viewMode = ref('explorer'); // explorer, cabinets, list
|
||||
const selectedFiles = ref([]);
|
||||
const isSelecting = ref(false);
|
||||
const currentDocument = ref(null);
|
||||
const showFileViewer = ref(false);
|
||||
|
||||
// Toggle file selection
|
||||
const toggleFileSelection = (file) => {
|
||||
const index = selectedFiles.value.findIndex(f => f.id === file.id);
|
||||
if (index === -1) {
|
||||
selectedFiles.value.push(file);
|
||||
} else {
|
||||
selectedFiles.value.splice(index, 1);
|
||||
}
|
||||
};
|
||||
|
||||
// Clear selection
|
||||
const clearSelection = () => {
|
||||
selectedFiles.value = [];
|
||||
isSelecting.value = false;
|
||||
};
|
||||
|
||||
// Select all files
|
||||
const selectAllFiles = () => {
|
||||
selectedFiles.value = [...myFiles];
|
||||
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;
|
||||
};
|
||||
|
||||
// Search functionality
|
||||
const handleSearch = async () => {
|
||||
if (!searchQuery.value.trim()) return;
|
||||
|
||||
isSearching.value = true;
|
||||
// In a real app, we would call an API to search
|
||||
setTimeout(() => {
|
||||
isSearching.value = false;
|
||||
}, 800);
|
||||
};
|
||||
|
||||
// Clear search
|
||||
const clearSearch = () => {
|
||||
searchQuery.value = '';
|
||||
};
|
||||
|
||||
// Mock data for the example
|
||||
const myFiles = [
|
||||
{
|
||||
id: 'file1',
|
||||
name: 'Project_Proposal_2023.pdf',
|
||||
type: 'file',
|
||||
extension: 'pdf',
|
||||
size: '2.4MB',
|
||||
modified: '2023-01-15',
|
||||
status: 'unlocked',
|
||||
createdBy: 'Current User'
|
||||
},
|
||||
{
|
||||
id: 'file2',
|
||||
name: 'Budget_Estimation_Q1.xlsx',
|
||||
type: 'file',
|
||||
extension: 'xlsx',
|
||||
size: '1.8MB',
|
||||
modified: '2023-02-10',
|
||||
status: 'unlocked',
|
||||
createdBy: 'Current User'
|
||||
},
|
||||
{
|
||||
id: 'file3',
|
||||
name: 'Meeting_Minutes_March.docx',
|
||||
type: 'file',
|
||||
extension: 'docx',
|
||||
size: '578KB',
|
||||
modified: '2023-03-22',
|
||||
status: 'locked',
|
||||
createdBy: 'Current User'
|
||||
},
|
||||
{
|
||||
id: 'file4',
|
||||
name: 'Technical_Specifications.pdf',
|
||||
type: 'file',
|
||||
extension: 'pdf',
|
||||
size: '3.2MB',
|
||||
modified: '2023-02-28',
|
||||
status: 'unlocked',
|
||||
createdBy: 'Current User'
|
||||
}
|
||||
];
|
||||
|
||||
// Lifecycle hooks
|
||||
onMounted(() => {
|
||||
// In a real app, we would fetch the user's documents here
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="dms-page">
|
||||
<LayoutsBreadcrumb />
|
||||
|
||||
<rs-card class="h-full">
|
||||
<template #header>
|
||||
<div class="flex flex-wrap items-center justify-between gap-4">
|
||||
<h1 class="text-xl font-bold text-primary">My Documents</h1>
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
<!-- Search box -->
|
||||
<div class="relative">
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
@keyup.enter="handleSearch"
|
||||
type="text"
|
||||
placeholder="Search my documents..."
|
||||
class="pl-10 pr-4 py-2 rounded-md border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 w-64"
|
||||
/>
|
||||
<span
|
||||
class="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-500"
|
||||
>
|
||||
<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"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
|
||||
</span>
|
||||
<button
|
||||
v-if="searchQuery"
|
||||
@click="clearSearch"
|
||||
class="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-500 hover:text-gray-700"
|
||||
>
|
||||
<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"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #body>
|
||||
<div class="explorer-layout h-full flex flex-col">
|
||||
<!-- Address bar (Windows Explorer style) -->
|
||||
<div class="address-bar border-b border-gray-200 dark:border-gray-700 p-2 flex items-center gap-2">
|
||||
<button class="p-2 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800">
|
||||
<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="M19 12H5M12 19l-7-7 7-7"/></svg>
|
||||
</button>
|
||||
|
||||
<button class="p-2 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800">
|
||||
<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="M5 12h14M12 5l7 7-7 7"/></svg>
|
||||
</button>
|
||||
|
||||
<button class="p-2 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800">
|
||||
<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="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path><polyline points="9 22 9 12 15 12 15 22"></polyline></svg>
|
||||
</button>
|
||||
|
||||
<div class="flex-1 flex items-center px-3 py-1 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800">
|
||||
<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 text-gray-500"><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>
|
||||
<span>{{ currentPath }}</span>
|
||||
</div>
|
||||
|
||||
<div class="relative">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Filter in current view..."
|
||||
class="px-3 py-1 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 w-56"
|
||||
/>
|
||||
<span class="absolute right-2 top-1/2 transform -translate-y-1/2 text-gray-500">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" 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>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- View options toolbar -->
|
||||
<div class="view-toolbar border-b border-gray-200 dark:border-gray-700 p-2 flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
@click="changeViewMode('explorer')"
|
||||
class="p-2 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800"
|
||||
:class="viewMode === 'explorer' ? 'bg-gray-100 dark:bg-gray-800' : ''"
|
||||
>
|
||||
<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"><rect x="3" y="3" width="7" height="7"></rect><rect x="14" y="3" width="7" height="7"></rect><rect x="14" y="14" width="7" height="7"></rect><rect x="3" y="14" width="7" height="7"></rect></svg>
|
||||
<span class="ml-1 text-sm">Explorer View</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click="changeViewMode('cabinets')"
|
||||
class="p-2 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800"
|
||||
:class="viewMode === 'cabinets' ? 'bg-gray-100 dark:bg-gray-800' : ''"
|
||||
>
|
||||
<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"><rect x="4" y="4" width="16" height="16" rx="2" ry="2"></rect><rect x="9" y="9" width="6" height="6"></rect><line x1="9" y1="1" x2="9" y2="4"></line><line x1="15" y1="1" x2="15" y2="4"></line><line x1="9" y1="20" x2="9" y2="23"></line><line x1="15" y1="20" x2="15" y2="23"></line><line x1="20" y1="9" x2="23" y2="9"></line><line x1="20" y1="14" x2="23" y2="14"></line><line x1="1" y1="9" x2="4" y2="9"></line><line x1="1" y1="14" x2="4" y2="14"></line></svg>
|
||||
<span class="ml-1 text-sm">Cabinets View</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click="changeViewMode('list')"
|
||||
class="p-2 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800"
|
||||
:class="viewMode === 'list' ? 'bg-gray-100 dark:bg-gray-800' : ''"
|
||||
>
|
||||
<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="8" y1="6" x2="21" y2="6"></line><line x1="8" y1="12" x2="21" y2="12"></line><line x1="8" y1="18" x2="21" y2="18"></line><line x1="3" y1="6" x2="3.01" y2="6"></line><line x1="3" y1="12" x2="3.01" y2="12"></line><line x1="3" y1="18" x2="3.01" y2="18"></line></svg>
|
||||
<span class="ml-1 text-sm">List View</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<rs-button size="sm" color="primary">
|
||||
<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"><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>
|
||||
Upload
|
||||
</rs-button>
|
||||
<rs-button size="sm" color="secondary">
|
||||
<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>
|
||||
New
|
||||
</rs-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main content area -->
|
||||
<div class="flex-1 flex overflow-hidden">
|
||||
<!-- Left sidebar navigation -->
|
||||
<DMSNavigation />
|
||||
|
||||
<!-- File explorer main content -->
|
||||
<div class="flex-1 overflow-y-auto">
|
||||
<!-- List view -->
|
||||
<table class="min-w-full">
|
||||
<thead class="bg-gray-50 dark:bg-gray-800">
|
||||
<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 w-6">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="h-4 w-4 rounded border-gray-300 text-primary focus:ring-primary"
|
||||
@click="selectAllFiles"
|
||||
>
|
||||
</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">
|
||||
Name
|
||||
</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">
|
||||
Type
|
||||
</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">
|
||||
Size
|
||||
</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">
|
||||
Modified Date
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white dark:bg-gray-900 divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<tr
|
||||
v-for="file in myFiles"
|
||||
:key="file.id"
|
||||
@click="viewFile(file)"
|
||||
class="hover:bg-gray-100 dark:hover:bg-gray-800 cursor-pointer"
|
||||
:class="{ 'bg-blue-50 dark:bg-blue-900/20': isFileSelected(file) }"
|
||||
>
|
||||
<td class="px-4 py-3 whitespace-nowrap">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="h-4 w-4 rounded border-gray-300 text-primary focus:ring-primary"
|
||||
:checked="isFileSelected(file)"
|
||||
@click.stop="toggleFileSelection(file)"
|
||||
>
|
||||
</td>
|
||||
<td class="px-4 py-3 whitespace-nowrap">
|
||||
<div class="flex items-center">
|
||||
<span class="text-gray-500 mr-2">
|
||||
<svg v-if="file.extension === 'pdf'" 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="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
|
||||
<polyline points="14 2 14 8 20 8"></polyline>
|
||||
<path d="M9 15h6"></path>
|
||||
<path d="M9 11h6"></path>
|
||||
</svg>
|
||||
<svg v-else-if="file.extension === 'xlsx'" 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="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
|
||||
<polyline points="14 2 14 8 20 8"></polyline>
|
||||
<rect x="8" y="12" width="8" height="6"></rect>
|
||||
<line x1="8" y1="16" x2="16" y2="16"></line>
|
||||
<line x1="11" y1="12" x2="11" y2="18"></line>
|
||||
</svg>
|
||||
<svg v-else-if="file.extension === 'docx'" 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="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
|
||||
<polyline points="14 2 14 8 20 8"></polyline>
|
||||
<line x1="16" y1="13" x2="8" y2="13"></line>
|
||||
<line x1="16" y1="17" x2="8" y2="17"></line>
|
||||
<line x1="10" y1="9" x2="8" y2="9"></line>
|
||||
</svg>
|
||||
</span>
|
||||
<div class="flex items-center">
|
||||
<span>{{ file.name }}</span>
|
||||
<span v-if="file.status === 'locked'" class="ml-2 text-red-500">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" 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>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-4 py-3 whitespace-nowrap">
|
||||
<span class="uppercase">{{ file.extension }}</span>
|
||||
</td>
|
||||
<td class="px-4 py-3 whitespace-nowrap">
|
||||
{{ file.size }}
|
||||
</td>
|
||||
<td class="px-4 py-3 whitespace-nowrap">
|
||||
{{ file.modified }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Right sidebar for document details - optional -->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</rs-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.dms-page {
|
||||
height: calc(100vh - 64px - 48px - 32px); /* Adjust based on your layout */
|
||||
}
|
||||
|
||||
.explorer-layout {
|
||||
height: calc(100vh - 200px); /* Adjust based on your layout */
|
||||
}
|
||||
</style>
|
676
pages/dms/settings.vue
Normal file
676
pages/dms/settings.vue
Normal file
@ -0,0 +1,676 @@
|
||||
<script setup>
|
||||
import { ref, reactive, computed } from 'vue';
|
||||
|
||||
// Define page metadata
|
||||
definePageMeta({
|
||||
title: "DMS Settings",
|
||||
middleware: ["auth"],
|
||||
requiresAuth: true,
|
||||
breadcrumb: [
|
||||
{
|
||||
name: "DMS",
|
||||
path: "/dms",
|
||||
},
|
||||
{
|
||||
name: "Settings",
|
||||
path: "/dms/settings",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Settings categories
|
||||
const settingsCategories = [
|
||||
{ id: 'access', name: 'User & Access Management', icon: '🔐' },
|
||||
{ id: 'documents', name: 'Document & Folder Settings', icon: '📁' },
|
||||
{ id: 'metadata', name: 'Metadata & Tagging', icon: '📝' },
|
||||
{ id: 'upload', name: 'Upload & Storage Settings', icon: '📤' },
|
||||
{ id: 'system', name: 'System Settings', icon: '📅' }
|
||||
];
|
||||
|
||||
// Current active category
|
||||
const activeCategory = ref('access');
|
||||
|
||||
// Settings data structure
|
||||
const settings = reactive({
|
||||
// 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']
|
||||
}
|
||||
},
|
||||
|
||||
// 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
|
||||
}
|
||||
});
|
||||
|
||||
// Computed properties
|
||||
const currentSettings = computed(() => {
|
||||
return settings[activeCategory.value];
|
||||
});
|
||||
|
||||
// Computed properties for array-to-string conversions
|
||||
const predefinedTagsString = computed({
|
||||
get: () => settings.metadata.tagging.predefinedTags.join(', '),
|
||||
set: (value) => {
|
||||
settings.metadata.tagging.predefinedTags = value.split(',').map(tag => tag.trim()).filter(tag => tag.length > 0);
|
||||
}
|
||||
});
|
||||
|
||||
const allowedFileTypesString = computed({
|
||||
get: () => settings.upload.fileTypes.allowed.join(', '),
|
||||
set: (value) => {
|
||||
settings.upload.fileTypes.allowed = value.split(',').map(type => type.trim()).filter(type => type.length > 0);
|
||||
}
|
||||
});
|
||||
|
||||
const blockedFileTypesString = computed({
|
||||
get: () => settings.upload.fileTypes.blocked.join(', '),
|
||||
set: (value) => {
|
||||
settings.upload.fileTypes.blocked = value.split(',').map(type => type.trim()).filter(type => type.length > 0);
|
||||
}
|
||||
});
|
||||
|
||||
// Methods
|
||||
const saveSettings = async () => {
|
||||
try {
|
||||
// In a real app, this would make an API call
|
||||
console.log('Saving settings:', settings);
|
||||
|
||||
// Show success message
|
||||
alert('Settings saved successfully!');
|
||||
} catch (error) {
|
||||
console.error('Error saving settings:', error);
|
||||
alert('Error saving settings. Please try again.');
|
||||
}
|
||||
};
|
||||
|
||||
const resetToDefaults = () => {
|
||||
if (confirm('Are you sure you want to reset all settings to defaults? This action cannot be undone.')) {
|
||||
// Reset logic would go here
|
||||
console.log('Resetting to defaults');
|
||||
}
|
||||
};
|
||||
|
||||
const exportSettings = () => {
|
||||
const dataStr = JSON.stringify(settings, null, 2);
|
||||
const dataUri = 'data:application/json;charset=utf-8,'+ encodeURIComponent(dataStr);
|
||||
|
||||
const exportFileDefaultName = 'edms-settings.json';
|
||||
|
||||
const linkElement = document.createElement('a');
|
||||
linkElement.setAttribute('href', dataUri);
|
||||
linkElement.setAttribute('download', exportFileDefaultName);
|
||||
linkElement.click();
|
||||
};
|
||||
|
||||
const importSettings = (event) => {
|
||||
const file = event.target.files[0];
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
try {
|
||||
const importedSettings = JSON.parse(e.target.result);
|
||||
Object.assign(settings, importedSettings);
|
||||
alert('Settings imported successfully!');
|
||||
} catch (error) {
|
||||
alert('Error importing settings. Please check the file format.');
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
}
|
||||
};
|
||||
|
||||
// Add/Remove methods for dynamic arrays
|
||||
const addCustomField = () => {
|
||||
settings.metadata.customFields.push({
|
||||
name: '',
|
||||
type: 'text',
|
||||
required: false
|
||||
});
|
||||
};
|
||||
|
||||
const removeCustomField = (index) => {
|
||||
settings.metadata.customFields.splice(index, 1);
|
||||
};
|
||||
|
||||
const addUserRole = () => {
|
||||
const roleName = prompt('Enter new role name:');
|
||||
if (roleName && !settings.access.userRoles.includes(roleName)) {
|
||||
settings.access.userRoles.push(roleName);
|
||||
}
|
||||
};
|
||||
|
||||
const removeUserRole = (role) => {
|
||||
const index = settings.access.userRoles.indexOf(role);
|
||||
if (index > -1) {
|
||||
settings.access.userRoles.splice(index, 1);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="dms-settings">
|
||||
<LayoutsBreadcrumb />
|
||||
|
||||
<rs-card class="h-full">
|
||||
<template #header>
|
||||
<div class="flex justify-between items-center">
|
||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-gray-100">DMS Settings</h1>
|
||||
<div class="flex space-x-2">
|
||||
<rs-button @click="exportSettings" class="!bg-gray-100 !text-gray-700 border">
|
||||
<Icon name="ic:outline-download" class="mr-2" />
|
||||
Export
|
||||
</rs-button>
|
||||
<label class="cursor-pointer">
|
||||
<input type="file" @change="importSettings" accept=".json" class="hidden" />
|
||||
<rs-button class="!bg-gray-100 !text-gray-700 border">
|
||||
<Icon name="ic:outline-upload" class="mr-2" />
|
||||
Import
|
||||
</rs-button>
|
||||
</label>
|
||||
<rs-button @click="resetToDefaults" class="!bg-red-100 !text-red-700 border border-red-200">
|
||||
<Icon name="ic:outline-refresh" class="mr-2" />
|
||||
Reset
|
||||
</rs-button>
|
||||
<rs-button @click="saveSettings" class="!bg-blue-600 !text-white">
|
||||
<Icon name="ic:outline-save" class="mr-2" />
|
||||
Save Settings
|
||||
</rs-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #body>
|
||||
<div class="settings-layout flex h-full">
|
||||
<!-- Settings Navigation -->
|
||||
<div class="settings-nav w-80 border-r border-gray-200 dark:border-gray-700 p-4 bg-gray-50 dark:bg-gray-800">
|
||||
<div class="space-y-2">
|
||||
<button
|
||||
v-for="category in settingsCategories"
|
||||
:key="category.id"
|
||||
@click="activeCategory = category.id"
|
||||
class="w-full flex items-center space-x-3 px-4 py-3 text-left rounded-lg transition-colors"
|
||||
:class="activeCategory === category.id
|
||||
? 'bg-blue-100 text-blue-700 dark:bg-blue-900/20 dark:text-blue-300'
|
||||
: 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'"
|
||||
>
|
||||
<span class="text-lg">{{ category.icon }}</span>
|
||||
<span class="font-medium text-sm">{{ category.name }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Settings Content -->
|
||||
<div class="settings-content flex-1 p-6 overflow-y-auto">
|
||||
|
||||
<!-- User & Access Management -->
|
||||
<div v-if="activeCategory === 'access'" class="space-y-8">
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold mb-4">🔐 User & Access Management</h2>
|
||||
|
||||
<!-- User Roles -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg border p-6 mb-6">
|
||||
<h3 class="text-lg font-medium mb-4">User Roles & Permissions</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-2">User Roles</label>
|
||||
<div class="space-y-2">
|
||||
<div v-for="role in settings.access.userRoles" :key="role" class="flex items-center justify-between bg-gray-50 dark:bg-gray-700 px-3 py-2 rounded">
|
||||
<span>{{ role }}</span>
|
||||
<button @click="removeUserRole(role)" class="text-red-500 hover:text-red-700">
|
||||
<Icon name="ic:outline-delete" size="16" />
|
||||
</button>
|
||||
</div>
|
||||
<button @click="addUserRole" class="text-blue-600 hover:text-blue-800 text-sm">+ Add Role</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-2">Access Permissions</label>
|
||||
<div class="space-y-3">
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox" v-model="settings.access.permissions.view" class="mr-2" />
|
||||
View Documents
|
||||
</label>
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox" v-model="settings.access.permissions.edit" class="mr-2" />
|
||||
Edit Documents
|
||||
</label>
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox" v-model="settings.access.permissions.delete" class="mr-2" />
|
||||
Delete Documents
|
||||
</label>
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox" v-model="settings.access.permissions.download" class="mr-2" />
|
||||
Download Documents
|
||||
</label>
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox" v-model="settings.access.permissions.share" class="mr-2" />
|
||||
Share Documents
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Authentication Settings -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg border p-6">
|
||||
<h3 class="text-lg font-medium mb-4">Authentication Settings</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div class="space-y-4">
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox" v-model="settings.access.authentication.ssoEnabled" class="mr-2" />
|
||||
Enable Single Sign-On (SSO)
|
||||
</label>
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox" v-model="settings.access.authentication.mfaRequired" class="mr-2" />
|
||||
Require Multi-Factor Authentication
|
||||
</label>
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox" v-model="settings.access.authentication.ldapIntegration" class="mr-2" />
|
||||
LDAP/Active Directory Integration
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-2">Session Timeout (hours)</label>
|
||||
<input type="number" v-model="settings.access.authentication.sessionTimeout"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md" min="1" max="24" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Document & Folder Settings -->
|
||||
<div v-if="activeCategory === 'documents'" class="space-y-8">
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold mb-4">📁 Document & Folder Settings</h2>
|
||||
|
||||
<!-- Folder Hierarchy -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg border p-6 mb-6">
|
||||
<h3 class="text-lg font-medium mb-4">Folder Hierarchies</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-2">Maximum Folder Depth</label>
|
||||
<input type="number" v-model="settings.documents.folderHierarchy.maxDepth"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md" min="1" max="10" />
|
||||
</div>
|
||||
<div>
|
||||
<rs-select
|
||||
v-model="settings.documents.folderHierarchy.folderTemplates[0]"
|
||||
:options="[
|
||||
{ value: 'Standard', label: 'Standard' },
|
||||
{ value: 'Project-based', label: 'Project-based' },
|
||||
{ value: 'Department-based', label: 'Department-based' },
|
||||
{ value: 'Custom', label: 'Custom' }
|
||||
]"
|
||||
label="Folder Template"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Naming Conventions -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg border p-6 mb-6">
|
||||
<h3 class="text-lg font-medium mb-4">Document Naming Conventions</h3>
|
||||
<div class="space-y-4">
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox" v-model="settings.documents.namingConventions.autoGenerate" class="mr-2" />
|
||||
Auto-generate document names
|
||||
</label>
|
||||
<div>
|
||||
<rs-input
|
||||
v-model="settings.documents.namingConventions.pattern"
|
||||
label="Naming Pattern"
|
||||
placeholder="{department}_{title}_{date}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Version Control -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg border p-6">
|
||||
<h3 class="text-lg font-medium mb-4">Version Control</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div class="space-y-4">
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox" v-model="settings.documents.versionControl.enabled" class="mr-2" />
|
||||
Enable Version Control
|
||||
</label>
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox" v-model="settings.documents.versionControl.autoVersioning" class="mr-2" />
|
||||
Automatic Versioning
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-2">Maximum Versions to Retain</label>
|
||||
<input type="number" v-model="settings.documents.versionControl.maxVersions"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md" min="1" max="50" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Metadata & Tagging -->
|
||||
<div v-if="activeCategory === 'metadata'" class="space-y-8">
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold mb-4">📝 Metadata & Tagging</h2>
|
||||
|
||||
<!-- Custom Metadata Fields -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg border p-6 mb-6">
|
||||
<h3 class="text-lg font-medium mb-4">Custom Metadata Fields</h3>
|
||||
<div class="space-y-4">
|
||||
<div v-for="(field, index) in settings.metadata.customFields" :key="index"
|
||||
class="grid grid-cols-4 gap-4 items-center bg-gray-50 dark:bg-gray-700 p-3 rounded">
|
||||
<input type="text" v-model="field.name" placeholder="Field Name"
|
||||
class="px-3 py-2 border border-gray-300 rounded-md" />
|
||||
<select v-model="field.type" class="px-3 py-2 border border-gray-300 rounded-md">
|
||||
<option value="text">Text</option>
|
||||
<option value="dropdown">Dropdown</option>
|
||||
<option value="date">Date</option>
|
||||
<option value="number">Number</option>
|
||||
<option value="select">Multi-select</option>
|
||||
</select>
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox" v-model="field.required" class="mr-2" />
|
||||
Required
|
||||
</label>
|
||||
<button @click="removeCustomField(index)" class="text-red-500 hover:text-red-700">
|
||||
<Icon name="ic:outline-delete" size="20" />
|
||||
</button>
|
||||
</div>
|
||||
<button @click="addCustomField" class="text-blue-600 hover:text-blue-800">
|
||||
+ Add Custom Field
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tagging System -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg border p-6">
|
||||
<h3 class="text-lg font-medium mb-4">Tagging System</h3>
|
||||
<div class="space-y-4">
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox" v-model="settings.metadata.tagging.userGeneratedTags" class="mr-2" />
|
||||
Allow User-Generated Tags
|
||||
</label>
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox" v-model="settings.metadata.tagging.tagSuggestions" class="mr-2" />
|
||||
Enable Tag Suggestions
|
||||
</label>
|
||||
<div>
|
||||
<rs-textarea
|
||||
v-model="predefinedTagsString"
|
||||
label="Predefined Tags"
|
||||
placeholder="urgent, confidential, public, draft, final"
|
||||
:rows="3"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Upload & Storage Settings -->
|
||||
<div v-if="activeCategory === 'upload'" class="space-y-8">
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold mb-4">📤 Upload & Storage Settings</h2>
|
||||
|
||||
<!-- File Type Restrictions -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg border p-6 mb-6">
|
||||
<h3 class="text-lg font-medium mb-4">File Type Restrictions</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-2">Allowed File Types</label>
|
||||
<textarea v-model="allowedFileTypesString"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md h-24"
|
||||
placeholder="pdf, doc, docx, xls, xlsx"></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-2">Blocked File Types</label>
|
||||
<textarea v-model="blockedFileTypesString"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md h-24"
|
||||
placeholder="exe, bat, cmd"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- File Size and Quotas -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg border p-6">
|
||||
<h3 class="text-lg font-medium mb-4">File Size Limits & Storage Quotas</h3>
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-2">Max File Size (MB)</label>
|
||||
<input type="number" v-model="settings.upload.fileSizeLimit"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md" min="1" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-2">Per User Quota (MB)</label>
|
||||
<input type="number" v-model="settings.upload.quotas.perUser"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-2">Per Group Quota (MB)</label>
|
||||
<input type="number" v-model="settings.upload.quotas.perGroup"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-2">Per Project Quota (MB)</label>
|
||||
<input type="number" v-model="settings.upload.quotas.perProject"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- System Settings -->
|
||||
<div v-if="activeCategory === 'system'" class="space-y-8">
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold mb-4">📅 System Settings</h2>
|
||||
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg border p-6">
|
||||
<h3 class="text-lg font-medium mb-4">General System Configuration</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-2">System Timezone</label>
|
||||
<select v-model="settings.system.timezone" class="w-full px-3 py-2 border border-gray-300 rounded-md">
|
||||
<option value="Asia/Kuala_Lumpur">Asia/Kuala_Lumpur</option>
|
||||
<option value="UTC">UTC</option>
|
||||
<option value="America/New_York">America/New_York</option>
|
||||
<option value="Europe/London">Europe/London</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-2">Backup Schedule</label>
|
||||
<select v-model="settings.system.backupSchedule" class="w-full px-3 py-2 border border-gray-300 rounded-md">
|
||||
<option value="hourly">Hourly</option>
|
||||
<option value="daily">Daily</option>
|
||||
<option value="weekly">Weekly</option>
|
||||
<option value="monthly">Monthly</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-2">Log Level</label>
|
||||
<select v-model="settings.system.logLevel" class="w-full px-3 py-2 border border-gray-300 rounded-md">
|
||||
<option value="debug">Debug</option>
|
||||
<option value="info">Info</option>
|
||||
<option value="warning">Warning</option>
|
||||
<option value="error">Error</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox" v-model="settings.system.maintenanceMode" class="mr-2" />
|
||||
Maintenance Mode
|
||||
</label>
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox" v-model="settings.system.autoUpdates" class="mr-2" />
|
||||
Automatic Updates
|
||||
</label>
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox" v-model="settings.system.systemMonitoring" class="mr-2" />
|
||||
System Monitoring
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Placeholder for other categories -->
|
||||
<div v-if="!['access', 'documents', 'metadata', 'upload', 'system'].includes(activeCategory)" class="space-y-8">
|
||||
<div class="text-center py-12">
|
||||
<div class="text-6xl mb-4">
|
||||
{{ settingsCategories.find(c => c.id === activeCategory)?.icon }}
|
||||
</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>
|
||||
</template>
|
||||
</rs-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.dms-settings {
|
||||
height: calc(100vh - 64px);
|
||||
}
|
||||
|
||||
.settings-layout {
|
||||
height: calc(100vh - 200px);
|
||||
}
|
||||
|
||||
.settings-nav {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.settings-content {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* Custom scrollbar for settings content */
|
||||
.settings-content::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.settings-content::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.settings-content::-webkit-scrollbar-thumb {
|
||||
background: #d1d5db;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.settings-content::-webkit-scrollbar-thumb:hover {
|
||||
background: #9ca3af;
|
||||
}
|
||||
|
||||
/* Smooth transitions */
|
||||
.settings-nav button {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
@apply rounded border-gray-300 text-blue-600 focus:ring-blue-500;
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
input[type="number"],
|
||||
select,
|
||||
textarea {
|
||||
@apply dark:bg-gray-700 dark:border-gray-600 dark:text-gray-200;
|
||||
}
|
||||
</style>
|
@ -1,444 +0,0 @@
|
||||
<script setup>
|
||||
import { ref, reactive } from 'vue';
|
||||
import { useDmsStore } from '~/stores/dms';
|
||||
import DMSNavigation from '~/components/dms/navigation/DMSNavigation.vue';
|
||||
|
||||
// Define page metadata
|
||||
definePageMeta({
|
||||
title: "Upload Document",
|
||||
middleware: ["auth"],
|
||||
requiresAuth: true,
|
||||
breadcrumb: [
|
||||
{
|
||||
name: "Dashboard",
|
||||
path: "/dashboard",
|
||||
},
|
||||
{
|
||||
name: "DMS",
|
||||
path: "/dms",
|
||||
},
|
||||
{
|
||||
name: "Upload Document",
|
||||
path: "/dms/upload-document",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Set up store
|
||||
const dmsStore = useDmsStore();
|
||||
|
||||
// Local state
|
||||
const isUploading = ref(false);
|
||||
const selectedFiles = ref([]);
|
||||
const uploadProgress = ref(0);
|
||||
const currentLocation = ref('/JKR Cawangan Kota Bharu, Kelantan');
|
||||
|
||||
// Document metadata form
|
||||
const documentForm = reactive({
|
||||
title: '',
|
||||
description: '',
|
||||
keywords: '',
|
||||
category: 'Technical Specification',
|
||||
status: 'Draft',
|
||||
retention: '7 years',
|
||||
department: '',
|
||||
accessLevel: 'private'
|
||||
});
|
||||
|
||||
// Available categories
|
||||
const categories = [
|
||||
'Technical Specification',
|
||||
'Project Proposal',
|
||||
'Contract',
|
||||
'Invoice',
|
||||
'Report',
|
||||
'Memo',
|
||||
'Letter',
|
||||
'Other'
|
||||
];
|
||||
|
||||
// Document statuses
|
||||
const statuses = [
|
||||
'Draft',
|
||||
'Under Review',
|
||||
'Approved',
|
||||
'Rejected',
|
||||
'Archived'
|
||||
];
|
||||
|
||||
// Retention periods
|
||||
const retentionPeriods = [
|
||||
'1 year',
|
||||
'3 years',
|
||||
'5 years',
|
||||
'7 years',
|
||||
'10 years',
|
||||
'Permanent'
|
||||
];
|
||||
|
||||
// Access levels
|
||||
const accessLevels = [
|
||||
{ value: 'private', label: 'Private (Only me)' },
|
||||
{ value: 'department', label: 'Department' },
|
||||
{ value: 'organization', label: 'Organization' },
|
||||
{ value: 'public', label: 'Public' }
|
||||
];
|
||||
|
||||
// Handle file selection
|
||||
const handleFileSelect = (event) => {
|
||||
const files = event.target.files;
|
||||
selectedFiles.value = Array.from(files);
|
||||
|
||||
// Auto-populate title based on filename (without extension)
|
||||
if (files.length === 1) {
|
||||
const fileName = files[0].name;
|
||||
const nameWithoutExtension = fileName.split('.').slice(0, -1).join('.');
|
||||
documentForm.title = nameWithoutExtension.replace(/_/g, ' ');
|
||||
}
|
||||
};
|
||||
|
||||
// Clear selected files
|
||||
const clearFiles = () => {
|
||||
selectedFiles.value = [];
|
||||
// Reset form
|
||||
Object.keys(documentForm).forEach(key => {
|
||||
documentForm[key] = key === 'category' ? 'Technical Specification' :
|
||||
key === 'status' ? 'Draft' :
|
||||
key === 'retention' ? '7 years' :
|
||||
key === 'accessLevel' ? 'private' : '';
|
||||
});
|
||||
};
|
||||
|
||||
// Simulate upload process
|
||||
const uploadFiles = async () => {
|
||||
if (selectedFiles.value.length === 0) return;
|
||||
|
||||
isUploading.value = true;
|
||||
uploadProgress.value = 0;
|
||||
|
||||
// Simulate upload progress
|
||||
const interval = setInterval(() => {
|
||||
uploadProgress.value += 10;
|
||||
if (uploadProgress.value >= 100) {
|
||||
clearInterval(interval);
|
||||
setTimeout(() => {
|
||||
isUploading.value = false;
|
||||
// Show success message
|
||||
alert('Files uploaded successfully!');
|
||||
// Clear form and selected files
|
||||
clearFiles();
|
||||
}, 500);
|
||||
}
|
||||
}, 300);
|
||||
};
|
||||
|
||||
// Create dummy folder structure for folder picker
|
||||
const folderStructure = [
|
||||
{
|
||||
id: 'root',
|
||||
name: 'JKR Cawangan Kota Bharu, Kelantan',
|
||||
path: '/JKR Cawangan Kota Bharu, Kelantan',
|
||||
children: [
|
||||
{
|
||||
id: 'tech',
|
||||
name: 'Technical Documents',
|
||||
path: '/JKR Cawangan Kota Bharu, Kelantan/Technical Documents',
|
||||
children: []
|
||||
},
|
||||
{
|
||||
id: 'projects',
|
||||
name: 'Projects',
|
||||
path: '/JKR Cawangan Kota Bharu, Kelantan/Projects',
|
||||
children: [
|
||||
{
|
||||
id: 'proj1',
|
||||
name: 'Project MRT3',
|
||||
path: '/JKR Cawangan Kota Bharu, Kelantan/Projects/Project MRT3',
|
||||
children: []
|
||||
},
|
||||
{
|
||||
id: 'proj2',
|
||||
name: 'Empangan Nenggiri',
|
||||
path: '/JKR Cawangan Kota Bharu, Kelantan/Projects/Empangan Nenggiri',
|
||||
children: []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'admin',
|
||||
name: 'Administrative',
|
||||
path: '/JKR Cawangan Kota Bharu, Kelantan/Administrative',
|
||||
children: []
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
// Change destination folder
|
||||
const selectFolder = (path) => {
|
||||
currentLocation.value = path;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="dms-page">
|
||||
<LayoutsBreadcrumb />
|
||||
|
||||
<rs-card class="h-full">
|
||||
<template #header>
|
||||
<div class="flex flex-wrap items-center justify-between gap-4">
|
||||
<h1 class="text-xl font-bold text-primary">Upload Document</h1>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #body>
|
||||
<div class="explorer-layout h-full flex overflow-hidden">
|
||||
<!-- Left sidebar navigation -->
|
||||
<DMSNavigation />
|
||||
|
||||
<!-- Main content area -->
|
||||
<div class="flex-1 overflow-y-auto p-6">
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<!-- File upload section -->
|
||||
<div class="bg-white dark:bg-gray-800 shadow-sm rounded-lg overflow-hidden mb-6">
|
||||
<div class="bg-gray-50 dark:bg-gray-700 px-6 py-4 border-b border-gray-200 dark:border-gray-600">
|
||||
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100 flex items-center">
|
||||
<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" 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>
|
||||
Select Files
|
||||
</h2>
|
||||
</div>
|
||||
<div class="px-6 py-4">
|
||||
<div v-if="selectedFiles.length === 0" class="border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg p-12 text-center">
|
||||
<input
|
||||
type="file"
|
||||
id="file-upload"
|
||||
multiple
|
||||
class="hidden"
|
||||
@change="handleFileSelect"
|
||||
/>
|
||||
<label
|
||||
for="file-upload"
|
||||
class="cursor-pointer flex flex-col items-center"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="mb-4 text-gray-400"><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>
|
||||
<span class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-1">Drag and drop files here</span>
|
||||
<span class="text-sm text-gray-500 dark:text-gray-400 mb-4">or click to browse</span>
|
||||
<rs-button color="primary" size="sm">Select Files</rs-button>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<div class="mb-4 flex justify-between items-center">
|
||||
<h3 class="font-medium">Selected Files ({{ selectedFiles.length }})</h3>
|
||||
<rs-button size="sm" color="secondary" @click="clearFiles">
|
||||
<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="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
|
||||
Clear
|
||||
</rs-button>
|
||||
</div>
|
||||
|
||||
<div class="border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
|
||||
<ul class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<li v-for="(file, index) in selectedFiles" :key="index" class="p-4 flex items-center">
|
||||
<div class="mr-3 text-gray-400">
|
||||
<svg v-if="file.type.includes('pdf')" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
|
||||
<polyline points="14 2 14 8 20 8"></polyline>
|
||||
<path d="M9 15h6"></path>
|
||||
<path d="M9 11h6"></path>
|
||||
</svg>
|
||||
<svg v-else-if="file.type.includes('spreadsheet') || file.type.includes('excel')" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
|
||||
<polyline points="14 2 14 8 20 8"></polyline>
|
||||
<rect x="8" y="12" width="8" height="6"></rect>
|
||||
<line x1="8" y1="16" x2="16" y2="16"></line>
|
||||
<line x1="11" y1="12" x2="11" y2="18"></line>
|
||||
</svg>
|
||||
<svg v-else-if="file.type.includes('word') || file.type.includes('document')" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
|
||||
<polyline points="14 2 14 8 20 8"></polyline>
|
||||
<line x1="16" y1="13" x2="8" y2="13"></line>
|
||||
<line x1="16" y1="17" x2="8" y2="17"></line>
|
||||
<line x1="10" y1="9" x2="8" y2="9"></line>
|
||||
</svg>
|
||||
<svg v-else xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
|
||||
<polyline points="14 2 14 8 20 8"></polyline>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-gray-100 truncate">
|
||||
{{ file.name }}
|
||||
</p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ Math.round(file.size / 1024) }} KB
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
@click="selectedFiles.splice(index, 1)"
|
||||
class="p-1 text-gray-400 hover:text-red-500"
|
||||
>
|
||||
<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="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Upload destination -->
|
||||
<div class="bg-white dark:bg-gray-800 shadow-sm rounded-lg overflow-hidden mb-6">
|
||||
<div class="bg-gray-50 dark:bg-gray-700 px-6 py-4 border-b border-gray-200 dark:border-gray-600">
|
||||
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100 flex items-center">
|
||||
<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" class="mr-2"><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>
|
||||
Upload Destination
|
||||
</h2>
|
||||
</div>
|
||||
<div class="px-6 py-4">
|
||||
<div class="flex items-center p-2 border border-gray-200 dark:border-gray-700 rounded">
|
||||
<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" class="mr-2 text-gray-400"><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>
|
||||
<span>{{ currentLocation }}</span>
|
||||
<rs-button size="sm" color="secondary" class="ml-auto">
|
||||
Change
|
||||
</rs-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Document metadata -->
|
||||
<div class="bg-white dark:bg-gray-800 shadow-sm rounded-lg overflow-hidden mb-6">
|
||||
<div class="bg-gray-50 dark:bg-gray-700 px-6 py-4 border-b border-gray-200 dark:border-gray-600">
|
||||
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100 flex items-center">
|
||||
<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" class="mr-2"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg>
|
||||
Document Metadata
|
||||
</h2>
|
||||
</div>
|
||||
<div class="px-6 py-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Title</label>
|
||||
<input
|
||||
v-model="documentForm.title"
|
||||
type="text"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-primary focus:ring focus:ring-primary focus:ring-opacity-50"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Description</label>
|
||||
<textarea
|
||||
v-model="documentForm.description"
|
||||
rows="3"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-primary focus:ring focus:ring-primary focus:ring-opacity-50"
|
||||
></textarea>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Keywords (comma separated)</label>
|
||||
<input
|
||||
v-model="documentForm.keywords"
|
||||
type="text"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-primary focus:ring focus:ring-primary focus:ring-opacity-50"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Category</label>
|
||||
<select
|
||||
v-model="documentForm.category"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-primary focus:ring focus:ring-primary focus:ring-opacity-50"
|
||||
>
|
||||
<option v-for="category in categories" :key="category" :value="category">
|
||||
{{ category }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Status</label>
|
||||
<select
|
||||
v-model="documentForm.status"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-primary focus:ring focus:ring-primary focus:ring-opacity-50"
|
||||
>
|
||||
<option v-for="status in statuses" :key="status" :value="status">
|
||||
{{ status }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Retention Period</label>
|
||||
<select
|
||||
v-model="documentForm.retention"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-primary focus:ring focus:ring-primary focus:ring-opacity-50"
|
||||
>
|
||||
<option v-for="period in retentionPeriods" :key="period" :value="period">
|
||||
{{ period }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Department</label>
|
||||
<input
|
||||
v-model="documentForm.department"
|
||||
type="text"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-primary focus:ring focus:ring-primary focus:ring-opacity-50"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Access Level</label>
|
||||
<select
|
||||
v-model="documentForm.accessLevel"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-primary focus:ring focus:ring-primary focus:ring-opacity-50"
|
||||
>
|
||||
<option v-for="level in accessLevels" :key="level.value" :value="level.value">
|
||||
{{ level.label }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Upload progress -->
|
||||
<div v-if="isUploading" class="bg-white dark:bg-gray-800 shadow-sm rounded-lg overflow-hidden mb-6">
|
||||
<div class="px-6 py-4">
|
||||
<h3 class="font-medium mb-2">Uploading Files...</h3>
|
||||
<div class="w-full bg-gray-200 rounded-full h-2.5 mb-2">
|
||||
<div
|
||||
class="bg-primary h-2.5 rounded-full"
|
||||
:style="{ width: `${uploadProgress}%` }"
|
||||
></div>
|
||||
</div>
|
||||
<p class="text-sm text-gray-500">{{ uploadProgress }}% complete</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex justify-end gap-3">
|
||||
<rs-button color="secondary" @click="clearFiles">Cancel</rs-button>
|
||||
<rs-button
|
||||
color="primary"
|
||||
:disabled="selectedFiles.length === 0 || isUploading"
|
||||
@click="uploadFiles"
|
||||
>
|
||||
<svg v-if="!isUploading" 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>
|
||||
Upload {{ selectedFiles.length }} {{ selectedFiles.length === 1 ? 'File' : 'Files' }}
|
||||
</rs-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</rs-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.dms-page {
|
||||
height: calc(100vh - 64px - 48px - 32px); /* Adjust based on your layout */
|
||||
}
|
||||
|
||||
.explorer-layout {
|
||||
height: calc(100vh - 200px); /* Adjust based on your layout */
|
||||
}
|
||||
</style>
|
@ -1,10 +1,18 @@
|
||||
<script setup>
|
||||
definePageMeta({
|
||||
title: "Main",
|
||||
middleware: ["main"],
|
||||
title: "EDMS",
|
||||
middleware: ["auth"],
|
||||
});
|
||||
|
||||
// Redirect to DMS page immediately
|
||||
await navigateTo('/dms', { replace: true });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>Redirect Dashboard</div>
|
||||
<div class="flex items-center justify-center min-h-screen">
|
||||
<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">Redirecting to Document Management System...</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -60,7 +60,7 @@ const login = async () => {
|
||||
showConfirmButton: false,
|
||||
});
|
||||
|
||||
window.location.href = "/dashboard";
|
||||
window.location.href = "/dms";
|
||||
} else {
|
||||
$swal.fire({
|
||||
title: "Error!",
|
||||
|
415
stores/dms.js
415
stores/dms.js
@ -334,7 +334,111 @@ export const useDmsStore = defineStore('dms', {
|
||||
showFileViewer: false,
|
||||
viewerDocument: null,
|
||||
searchQuery: '',
|
||||
searchResults: []
|
||||
searchResults: [],
|
||||
|
||||
// Settings integration
|
||||
systemSettings: {
|
||||
upload: {
|
||||
allowedFileTypes: ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'jpg', 'png'],
|
||||
blockedFileTypes: ['exe', 'bat', 'cmd'],
|
||||
maxFileSize: 100 * 1024 * 1024, // 100MB in bytes
|
||||
quotas: {
|
||||
perUser: 5000 * 1024 * 1024, // 5GB
|
||||
perGroup: 50000 * 1024 * 1024, // 50GB
|
||||
perProject: 100000 * 1024 * 1024 // 100GB
|
||||
}
|
||||
},
|
||||
metadata: {
|
||||
customFields: [
|
||||
{ name: 'Department', type: 'dropdown', required: true, options: ['HR', 'Finance', 'IT', 'Legal'] },
|
||||
{ name: 'Priority', type: 'select', required: false, options: ['Low', 'Medium', 'High', 'Critical'] },
|
||||
{ name: 'Project Code', type: 'text', required: true },
|
||||
{ name: 'Review Date', type: 'date', required: false }
|
||||
],
|
||||
predefinedTags: ['urgent', 'confidential', 'public', 'draft', 'final'],
|
||||
userGeneratedTags: true,
|
||||
tagSuggestions: true
|
||||
},
|
||||
documents: {
|
||||
versionControl: {
|
||||
enabled: true,
|
||||
maxVersions: 10,
|
||||
autoVersioning: true
|
||||
},
|
||||
namingConventions: {
|
||||
autoGenerate: true,
|
||||
pattern: '{department}_{title}_{date}'
|
||||
}
|
||||
},
|
||||
access: {
|
||||
rbacEnabled: true,
|
||||
permissions: {
|
||||
view: true,
|
||||
edit: true,
|
||||
delete: false,
|
||||
download: true,
|
||||
share: true
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Enhanced user permissions with detailed tracking
|
||||
userQuotas: {
|
||||
used: 1500 * 1024 * 1024, // 1.5GB used
|
||||
limit: 5000 * 1024 * 1024, // 5GB limit
|
||||
documents: 450,
|
||||
maxDocuments: 1000
|
||||
},
|
||||
|
||||
// Document metadata templates
|
||||
metadataTemplates: {
|
||||
standard: {
|
||||
title: '',
|
||||
description: '',
|
||||
department: '',
|
||||
author: '',
|
||||
tags: [],
|
||||
priority: 'Medium',
|
||||
reviewDate: null,
|
||||
projectCode: ''
|
||||
},
|
||||
contract: {
|
||||
title: '',
|
||||
description: '',
|
||||
department: 'Legal',
|
||||
contractType: '',
|
||||
vendor: '',
|
||||
startDate: null,
|
||||
endDate: null,
|
||||
value: '',
|
||||
tags: ['contract'],
|
||||
priority: 'High'
|
||||
},
|
||||
report: {
|
||||
title: '',
|
||||
description: '',
|
||||
department: '',
|
||||
reportType: '',
|
||||
period: '',
|
||||
author: '',
|
||||
tags: ['report'],
|
||||
priority: 'Medium'
|
||||
}
|
||||
},
|
||||
|
||||
// Version tracking
|
||||
documentVersions: {},
|
||||
|
||||
// Enhanced search with metadata
|
||||
advancedSearchFilters: {
|
||||
fileType: [],
|
||||
department: [],
|
||||
tags: [],
|
||||
dateRange: { start: null, end: null },
|
||||
author: [],
|
||||
priority: [],
|
||||
hasVersions: false
|
||||
}
|
||||
}),
|
||||
|
||||
getters: {
|
||||
@ -486,6 +590,315 @@ export const useDmsStore = defineStore('dms', {
|
||||
this.viewerDocument = null;
|
||||
},
|
||||
|
||||
// Enhanced file validation based on settings
|
||||
validateFile(file) {
|
||||
const errors = [];
|
||||
const settings = this.systemSettings.upload;
|
||||
|
||||
// Check file type
|
||||
const fileExtension = file.name.split('.').pop().toLowerCase();
|
||||
if (!settings.allowedFileTypes.includes(fileExtension)) {
|
||||
errors.push(`File type .${fileExtension} is not allowed`);
|
||||
}
|
||||
|
||||
if (settings.blockedFileTypes.includes(fileExtension)) {
|
||||
errors.push(`File type .${fileExtension} is blocked`);
|
||||
}
|
||||
|
||||
// Check file size
|
||||
if (file.size > settings.maxFileSize) {
|
||||
const maxSizeMB = settings.maxFileSize / (1024 * 1024);
|
||||
errors.push(`File size exceeds limit of ${maxSizeMB}MB`);
|
||||
}
|
||||
|
||||
// Check user quota
|
||||
if (this.userQuotas.used + file.size > this.userQuotas.limit) {
|
||||
errors.push('Upload would exceed your storage quota');
|
||||
}
|
||||
|
||||
return {
|
||||
isValid: errors.length === 0,
|
||||
errors
|
||||
};
|
||||
},
|
||||
|
||||
// Enhanced metadata validation
|
||||
validateMetadata(metadata, template = 'standard') {
|
||||
const errors = [];
|
||||
const templateConfig = this.metadataTemplates[template];
|
||||
const customFields = this.systemSettings.metadata.customFields;
|
||||
|
||||
// Validate required custom fields
|
||||
customFields.forEach(field => {
|
||||
if (field.required && (!metadata[field.name] || metadata[field.name].toString().trim() === '')) {
|
||||
errors.push(`${field.name} is required`);
|
||||
}
|
||||
|
||||
// Validate field options for dropdown/select types
|
||||
if (field.options && metadata[field.name] && !field.options.includes(metadata[field.name])) {
|
||||
errors.push(`Invalid value for ${field.name}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Validate document naming convention
|
||||
if (this.systemSettings.documents.namingConventions.autoGenerate) {
|
||||
const pattern = this.systemSettings.documents.namingConventions.pattern;
|
||||
const requiredFields = pattern.match(/{(\w+)}/g)?.map(match => match.slice(1, -1)) || [];
|
||||
|
||||
requiredFields.forEach(field => {
|
||||
if (!metadata[field]) {
|
||||
errors.push(`${field} is required for auto-generated naming`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
isValid: errors.length === 0,
|
||||
errors
|
||||
};
|
||||
},
|
||||
|
||||
// Generate document name based on naming convention
|
||||
generateDocumentName(metadata, originalFileName) {
|
||||
if (!this.systemSettings.documents.namingConventions.autoGenerate) {
|
||||
return originalFileName;
|
||||
}
|
||||
|
||||
let pattern = this.systemSettings.documents.namingConventions.pattern;
|
||||
const date = new Date().toISOString().split('T')[0];
|
||||
|
||||
// Replace placeholders
|
||||
pattern = pattern.replace(/{department}/g, metadata.department || 'unknown');
|
||||
pattern = pattern.replace(/{title}/g, metadata.title || 'document');
|
||||
pattern = pattern.replace(/{date}/g, date);
|
||||
pattern = pattern.replace(/{author}/g, metadata.author || 'unknown');
|
||||
pattern = pattern.replace(/{projectCode}/g, metadata.projectCode || '');
|
||||
|
||||
// Get file extension
|
||||
const extension = originalFileName.split('.').pop();
|
||||
|
||||
return `${pattern}.${extension}`;
|
||||
},
|
||||
|
||||
// Enhanced upload with metadata and validation
|
||||
async uploadFileWithMetadata(file, metadata, currentPath) {
|
||||
// Validate file
|
||||
const fileValidation = this.validateFile(file);
|
||||
if (!fileValidation.isValid) {
|
||||
throw new Error(`File validation failed: ${fileValidation.errors.join(', ')}`);
|
||||
}
|
||||
|
||||
// Validate metadata
|
||||
const metadataValidation = this.validateMetadata(metadata);
|
||||
if (!metadataValidation.isValid) {
|
||||
throw new Error(`Metadata validation failed: ${metadataValidation.errors.join(', ')}`);
|
||||
}
|
||||
|
||||
this.isLoading = true;
|
||||
|
||||
try {
|
||||
// Generate document name
|
||||
const documentName = this.generateDocumentName(metadata, file.name);
|
||||
|
||||
// Create document object
|
||||
const newDocument = {
|
||||
id: `doc_${Date.now()}`,
|
||||
name: documentName,
|
||||
originalName: file.name,
|
||||
type: 'file',
|
||||
extension: file.name.split('.').pop().toLowerCase(),
|
||||
size: this.formatFileSize(file.size),
|
||||
modified: new Date().toLocaleDateString(),
|
||||
author: metadata.author || 'Current User',
|
||||
department: metadata.department,
|
||||
description: metadata.description,
|
||||
tags: metadata.tags || [],
|
||||
priority: metadata.priority,
|
||||
projectCode: metadata.projectCode,
|
||||
reviewDate: metadata.reviewDate,
|
||||
version: 1,
|
||||
hasAccess: true,
|
||||
accessType: metadata.accessType || 'private',
|
||||
metadata: { ...metadata }
|
||||
};
|
||||
|
||||
// Add to items (simulate upload)
|
||||
this.items.push(newDocument);
|
||||
|
||||
// Update user quota
|
||||
this.userQuotas.used += file.size;
|
||||
this.userQuotas.documents += 1;
|
||||
|
||||
// Initialize version tracking if enabled
|
||||
if (this.systemSettings.documents.versionControl.enabled) {
|
||||
this.documentVersions[newDocument.id] = [{
|
||||
version: 1,
|
||||
uploadDate: new Date().toISOString(),
|
||||
author: metadata.author,
|
||||
changes: 'Initial upload',
|
||||
fileSize: file.size
|
||||
}];
|
||||
}
|
||||
|
||||
return newDocument;
|
||||
} catch (error) {
|
||||
console.error('Upload failed:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
},
|
||||
|
||||
// Version control actions
|
||||
async createNewVersion(documentId, file, metadata) {
|
||||
if (!this.systemSettings.documents.versionControl.enabled) {
|
||||
throw new Error('Version control is not enabled');
|
||||
}
|
||||
|
||||
const document = this.items.find(item => item.id === documentId);
|
||||
if (!document) {
|
||||
throw new Error('Document not found');
|
||||
}
|
||||
|
||||
// Check version limit
|
||||
const versions = this.documentVersions[documentId] || [];
|
||||
if (versions.length >= this.systemSettings.documents.versionControl.maxVersions) {
|
||||
// Remove oldest version
|
||||
versions.shift();
|
||||
}
|
||||
|
||||
// Create new version
|
||||
const newVersion = {
|
||||
version: document.version + 1,
|
||||
uploadDate: new Date().toISOString(),
|
||||
author: metadata.author,
|
||||
changes: metadata.changes || 'Updated document',
|
||||
fileSize: file.size
|
||||
};
|
||||
|
||||
// Update document
|
||||
document.version = newVersion.version;
|
||||
document.modified = new Date().toLocaleDateString();
|
||||
document.size = this.formatFileSize(file.size);
|
||||
|
||||
// Add version to tracking
|
||||
versions.push(newVersion);
|
||||
this.documentVersions[documentId] = versions;
|
||||
|
||||
return newVersion;
|
||||
},
|
||||
|
||||
// Get document versions
|
||||
getDocumentVersions(documentId) {
|
||||
return this.documentVersions[documentId] || [];
|
||||
},
|
||||
|
||||
// Enhanced search with metadata
|
||||
async advancedSearch(query, filters = {}) {
|
||||
this.isLoading = true;
|
||||
this.searchQuery = query;
|
||||
Object.assign(this.advancedSearchFilters, filters);
|
||||
|
||||
try {
|
||||
// Mock API delay
|
||||
await new Promise(resolve => setTimeout(resolve, 300));
|
||||
|
||||
let results = this.items.filter(item => item.type === 'file');
|
||||
|
||||
// Text search
|
||||
if (query) {
|
||||
const searchLower = query.toLowerCase();
|
||||
results = results.filter(item =>
|
||||
item.name.toLowerCase().includes(searchLower) ||
|
||||
item.description?.toLowerCase().includes(searchLower) ||
|
||||
item.department?.toLowerCase().includes(searchLower) ||
|
||||
item.author?.toLowerCase().includes(searchLower) ||
|
||||
item.projectCode?.toLowerCase().includes(searchLower) ||
|
||||
item.tags?.some(tag => tag.toLowerCase().includes(searchLower))
|
||||
);
|
||||
}
|
||||
|
||||
// Filter by file type
|
||||
if (filters.fileType && filters.fileType.length > 0) {
|
||||
results = results.filter(item => filters.fileType.includes(item.extension));
|
||||
}
|
||||
|
||||
// Filter by department
|
||||
if (filters.department && filters.department.length > 0) {
|
||||
results = results.filter(item => filters.department.includes(item.department));
|
||||
}
|
||||
|
||||
// Filter by tags
|
||||
if (filters.tags && filters.tags.length > 0) {
|
||||
results = results.filter(item =>
|
||||
item.tags?.some(tag => filters.tags.includes(tag))
|
||||
);
|
||||
}
|
||||
|
||||
// Filter by priority
|
||||
if (filters.priority && filters.priority.length > 0) {
|
||||
results = results.filter(item => filters.priority.includes(item.priority));
|
||||
}
|
||||
|
||||
// Filter by date range
|
||||
if (filters.dateRange?.start || filters.dateRange?.end) {
|
||||
results = results.filter(item => {
|
||||
const itemDate = new Date(item.modified);
|
||||
const startDate = filters.dateRange.start ? new Date(filters.dateRange.start) : null;
|
||||
const endDate = filters.dateRange.end ? new Date(filters.dateRange.end) : null;
|
||||
|
||||
if (startDate && itemDate < startDate) return false;
|
||||
if (endDate && itemDate > endDate) return false;
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
// Filter by version existence
|
||||
if (filters.hasVersions) {
|
||||
results = results.filter(item => this.documentVersions[item.id]?.length > 1);
|
||||
}
|
||||
|
||||
this.searchResults = results;
|
||||
return results;
|
||||
} catch (error) {
|
||||
console.error('Advanced search failed:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
},
|
||||
|
||||
// Tag suggestions
|
||||
getTagSuggestions(input) {
|
||||
if (!this.systemSettings.metadata.tagSuggestions) return [];
|
||||
|
||||
const allTags = [
|
||||
...this.systemSettings.metadata.predefinedTags,
|
||||
...new Set(this.items.flatMap(item => item.tags || []))
|
||||
];
|
||||
|
||||
return allTags.filter(tag =>
|
||||
tag.toLowerCase().includes(input.toLowerCase())
|
||||
).slice(0, 10);
|
||||
},
|
||||
|
||||
// Update system settings
|
||||
updateSystemSettings(category, settings) {
|
||||
if (this.systemSettings[category]) {
|
||||
Object.assign(this.systemSettings[category], settings);
|
||||
}
|
||||
},
|
||||
|
||||
// Format file size helper
|
||||
formatFileSize(bytes) {
|
||||
if (bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
},
|
||||
|
||||
// Cabinet access actions
|
||||
async requestCabinetAccess(cabinetId, reason) {
|
||||
this.isLoading = true;
|
||||
|
Loading…
x
Reference in New Issue
Block a user