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

This commit is contained in:
MuhdAthir 2025-05-31 10:31:18 +08:00
commit 1c0afe2b8a
23 changed files with 3660 additions and 1800 deletions

204
CHANGELOG.md Normal file
View 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
View File

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

View File

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

View File

@ -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;
const files = Array.from(event.dataTransfer.files);
addFiles(files);
// Drag and drop handlers
const handleDragEnter = (e) => {
e.preventDefault();
dragCounter.value++;
isDragOver.value = true;
};
const handleFileInput = (event) => {
const files = Array.from(event.target.files);
addFiles(files);
// Reset input
event.target.value = '';
const handleDragLeave = (e) => {
e.preventDefault();
dragCounter.value--;
if (dragCounter.value === 0) {
isDragOver.value = false;
}
};
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 handleDragOver = (e) => {
e.preventDefault();
};
uploadFiles.value.push(fileObj);
selectedTags.value[fileObj.id] = [];
customMetadata.value[fileObj.id] = {
title: '',
description: '',
category: '',
author: '',
department: ''
const handleDrop = (e) => {
e.preventDefault();
dragCounter.value = 0;
isDragOver.value = false;
const files = Array.from(e.dataTransfer.files);
handleFiles(files);
};
// Tag input handling
const handleTagInput = (fileName, input) => {
if (input.length > 2) {
tagSuggestions.value = dmsStore.getTagSuggestions(input);
} else {
tagSuggestions.value = [];
}
};
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];
// 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>
</template>
<!-- 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 #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>
<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 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>
<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>
<!-- 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>
<!-- 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>
<!-- 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>
<!-- 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>
<!-- 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>
<!-- 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>
<!-- 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">
<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>
<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>
</div>
<!-- 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>
<!-- 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>

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

View File

@ -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,
### 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.
// UI state
treeExpanded: {},
searchQuery: '',
viewMode: 'list', // list, grid, details
sortOrder: 'asc',
sortField: 'name',
activeTab: 'all', // all, public, private, personal
### 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
// Loading states
isLoading: false,
isUploading: false,
uploadProgress: 0,
### Component Library
// Dialog states
showUploadDialog: false,
showAccessRequestDialog: false,
accessRequestItem: null
}),
#### Form Components
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']
}
})
##### 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

View File

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

View File

@ -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");
});

View File

@ -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");
});

View File

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

View File

@ -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
View 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>&lt;rs-modal :visible="showModal" @close="closeModal"&gt;
&lt;template #header&gt;
Modal Title
&lt;/template&gt;
&lt;template #body&gt;
Modal content here
&lt;/template&gt;
&lt;template #footer&gt;
&lt;rs-button variant="secondary"&gt;Cancel&lt;/rs-button&gt;
&lt;rs-button variant="primary"&gt;Confirm&lt;/rs-button&gt;
&lt;/template&gt;
&lt;/rs-modal&gt;</code></pre>
</div>
</section>
<!-- Color System -->
<section>
<h2 class="text-xl font-semibold mb-4">Color System</h2>
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4">
<div class="text-center">
<div class="w-full h-12 bg-primary rounded mb-2"></div>
<span class="text-sm">Primary</span>
</div>
<div class="text-center">
<div class="w-full h-12 bg-secondary rounded mb-2"></div>
<span class="text-sm">Secondary</span>
</div>
<div class="text-center">
<div class="w-full h-12 bg-info rounded mb-2"></div>
<span class="text-sm">Info</span>
</div>
<div class="text-center">
<div class="w-full h-12 bg-success rounded mb-2"></div>
<span class="text-sm">Success</span>
</div>
<div class="text-center">
<div class="w-full h-12 bg-warning rounded mb-2"></div>
<span class="text-sm">Warning</span>
</div>
<div class="text-center">
<div class="w-full h-12 bg-danger rounded mb-2"></div>
<span class="text-sm">Danger</span>
</div>
</div>
</section>
<!-- Usage Guidelines -->
<section>
<h2 class="text-xl font-semibold mb-4">Usage Guidelines</h2>
<div class="space-y-4">
<rs-card>
<template #body>
<h3 class="font-medium mb-2"> Do</h3>
<ul class="list-disc pl-6 space-y-1 text-sm">
<li>Use RsButton, RsInput, RsSelect, RsTextarea for all form elements</li>
<li>Use RsModal for all dialog components</li>
<li>Follow consistent spacing and sizing patterns</li>
<li>Use semantic variant names (primary, secondary, danger, etc.)</li>
</ul>
</template>
</rs-card>
<rs-card>
<template #body>
<h3 class="font-medium mb-2"> Don't</h3>
<ul class="list-disc pl-6 space-y-1 text-sm">
<li>Use manual button styling with px-4 py-2 classes</li>
<li>Create custom input styles without using Rs components</li>
<li>Use hardcoded colors instead of CSS variables</li>
<li>Mix different component styling approaches</li>
</ul>
</template>
</rs-card>
</div>
</section>
</div>
</template>
</rs-card>
</div>
</template>
<style scoped>
pre {
overflow-x: auto;
}
code {
color: rgb(var(--text-color));
}
</style>

View File

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

View File

@ -10,10 +10,6 @@ definePageMeta({
middleware: ["auth"],
requiresAuth: true,
breadcrumb: [
{
name: "Dashboard",
path: "/dashboard",
},
{
name: "DMS",
path: "/dms",

View File

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

View File

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

View File

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

View File

@ -60,7 +60,7 @@ const login = async () => {
showConfirmButton: false,
});
window.location.href = "/dashboard";
window.location.href = "/dms";
} else {
$swal.fire({
title: "Error!",

View File

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