Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2f289fb09e | |||
| a06d0badfc | |||
| 974ba0e3c3 | |||
| 8e07f38df5 | |||
| e016f18ce6 | |||
| 0808fec88b | |||
| 7e6a087049 | |||
| 970ed3a69c | |||
| 20c410bcac | |||
| 1bb7b0d737 |
+349
@@ -0,0 +1,349 @@
|
||||
# CoreUI Free Vue Admin Template - AI Assistant Rules
|
||||
|
||||
You are working with the CoreUI Free Vue Admin Template, a professional admin dashboard built with Vue 3, CoreUI Vue components, and modern build tools. This project uses Vite for development and building, Vue Router for navigation, Pinia for state management, and Sass for styling.
|
||||
|
||||
## Critical Rules
|
||||
|
||||
**Component Library**: ALWAYS use CoreUI Vue components from https://coreui.io/vue/docs/. NEVER use Tailwind CSS, Vuetify, Element Plus, or other component libraries. This project is built on Bootstrap 5 and CoreUI Vue components exclusively.
|
||||
|
||||
**Technology Stack**: This project uses:
|
||||
- Vue 3.5.x with Composition API and script setup
|
||||
- Single File Components (SFC) with <template>, <script>, and <style> sections
|
||||
- CoreUI Vue 5.x and @coreui/coreui 5.x
|
||||
- Vue Router 5.x for client-side routing
|
||||
- Pinia 3.x for state management
|
||||
- Vite 8.x for development server and building
|
||||
- Sass/SCSS for styling with Bootstrap 5 variables
|
||||
- Chart.js 4.x with @coreui/vue-chartjs for data visualization
|
||||
|
||||
## Code Conventions
|
||||
|
||||
**Vue/JavaScript Standards**:
|
||||
- Use Composition API with `<script setup>` syntax
|
||||
- Use ref() and reactive() for reactive state
|
||||
- Follow Vue 3 Composition API patterns
|
||||
- Use Prettier formatting: no semicolons, single quotes, 2-space indentation
|
||||
- Enforce ESLint rules with Vue and Prettier plugins
|
||||
- Prefer const and arrow functions
|
||||
- Use destructuring where appropriate
|
||||
|
||||
**File Organization**:
|
||||
- `src/` - All source code
|
||||
- `components/` - Reusable UI components (AppHeader, AppSidebar, etc.)
|
||||
- `views/` - Page components organized by feature (dashboard, forms, charts, etc.)
|
||||
- `layouts/` - Layout wrapper components (DefaultLayout)
|
||||
- `assets/` - Static assets (images, brand logos)
|
||||
- `scss/` - Global styles and theme customization
|
||||
- `router/` - Router configuration
|
||||
- `stores/` - Pinia stores
|
||||
- `_nav.js` - Navigation/sidebar menu configuration
|
||||
- `App.vue` - Main application component
|
||||
- `main.js` - Application entry point
|
||||
|
||||
**Vue/SFC Practices**:
|
||||
- Use `<script setup>` for components (preferred modern syntax)
|
||||
- Use `defineProps()` and `defineEmits()` for component API
|
||||
- Use computed() for derived state
|
||||
- Use watch() or watchEffect() for side effects
|
||||
- Keep components focused and single-responsibility
|
||||
- Extract reusable logic into composables (useX pattern)
|
||||
- Use Suspense for async components when needed
|
||||
|
||||
**CSS/Sass Practices**:
|
||||
- Import global styles in main.js: `import './scss/style.scss'`
|
||||
- Use Bootstrap utilities first before custom CSS
|
||||
- Leverage CoreUI CSS custom properties for theming
|
||||
- Support dark mode through CoreUI's color mode system
|
||||
- File: `src/scss/style.scss` - main stylesheet importing CoreUI and Bootstrap
|
||||
- File: `src/scss/_custom.scss` - custom style overrides
|
||||
- Use scoped styles in components: `<style scoped>`
|
||||
- Use SCSS variables from Bootstrap and CoreUI when possible
|
||||
|
||||
**Routing Conventions**:
|
||||
- Use createWebHashHistory for client-side routing (GitHub Pages compatible)
|
||||
- Define routes in router/index.js as array of objects
|
||||
- Use dynamic imports for lazy loading route components
|
||||
- Use exact path matching where needed
|
||||
- Public routes (login, register, 404, 500) defined separately
|
||||
- Protected routes handled in DefaultLayout
|
||||
|
||||
**State Management**:
|
||||
- Use Pinia for global state (theme, sidebar visibility)
|
||||
- Create stores in stores/ directory
|
||||
- Use defineStore() with setup syntax
|
||||
- Access stores with const store = useStore()
|
||||
- Keep component-level state in ref() or reactive() when state is local
|
||||
|
||||
**Naming Conventions**:
|
||||
- PascalCase for component files and component names (AppHeader.vue, DefaultLayout.vue)
|
||||
- camelCase for variables, functions, and composables (useState, useEffect)
|
||||
- UPPER_SNAKE_CASE for constants (API_URL, MAX_ITEMS)
|
||||
- kebab-case for CSS classes (following Bootstrap/CoreUI conventions)
|
||||
- Descriptive names that indicate purpose (AppHeaderDropdown vs Dropdown)
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
coreui-free-vue-admin-template/
|
||||
├── public/ # Static assets served directly
|
||||
├── src/
|
||||
│ ├── assets/ # Images, logos, icons
|
||||
│ │ ├── brand/ # Logo components
|
||||
│ │ └── images/ # Image files
|
||||
│ ├── components/ # Reusable UI components
|
||||
│ │ ├── AppHeader.vue
|
||||
│ │ ├── AppSidebar.vue
|
||||
│ │ ├── AppFooter.vue
|
||||
│ │ ├── AppContent.vue
|
||||
│ │ ├── AppBreadcrumb.vue
|
||||
│ │ └── AppHeaderDropdown.vue
|
||||
│ ├── layouts/ # Layout components
|
||||
│ │ └── DefaultLayout.vue
|
||||
│ ├── views/ # Page components
|
||||
│ │ ├── dashboard/ # Dashboard page
|
||||
│ │ ├── base/ # Base UI components examples
|
||||
│ │ ├── buttons/ # Button examples
|
||||
│ │ ├── forms/ # Form examples
|
||||
│ │ ├── charts/ # Chart examples
|
||||
│ │ ├── icons/ # Icon examples
|
||||
│ │ ├── notifications/ # Notification examples
|
||||
│ │ ├── widgets/ # Widget examples
|
||||
│ │ └── pages/ # Auth & error pages
|
||||
│ ├── router/ # Router configuration
|
||||
│ │ └── index.js # Route definitions
|
||||
│ ├── stores/ # Pinia stores
|
||||
│ ├── scss/ # Stylesheets
|
||||
│ │ ├── style.scss # Main stylesheet
|
||||
│ │ └── _custom.scss # Custom overrides
|
||||
│ ├── App.vue # Main app component
|
||||
│ ├── main.js # Entry point
|
||||
│ └── _nav.js # Navigation config
|
||||
├── index.html # HTML template
|
||||
├── vite.config.mjs # Vite configuration
|
||||
├── eslint.config.mjs # ESLint configuration
|
||||
├── package.json # Dependencies
|
||||
└── README.md # Documentation
|
||||
```
|
||||
|
||||
## Development Workflow
|
||||
|
||||
**Starting Development**:
|
||||
```bash
|
||||
npm install # Install dependencies
|
||||
npm run dev # Start dev server (http://localhost:3000)
|
||||
npm run build # Build for production
|
||||
npm run preview # Preview production build
|
||||
npm run lint # Run ESLint
|
||||
```
|
||||
|
||||
**Adding a New Page**:
|
||||
1. Create component in `src/views/[feature]/ComponentName.vue`
|
||||
2. Add route to `src/router/index.js`
|
||||
3. Add navigation item to `src/_nav.js` (if needed)
|
||||
4. Import and use CoreUI components from '@coreui/vue'
|
||||
|
||||
**Creating Components**:
|
||||
```vue
|
||||
<template>
|
||||
<CCard>
|
||||
<CCardHeader>{{ title }}</CCardHeader>
|
||||
<CCardBody>
|
||||
<slot />
|
||||
</CCardBody>
|
||||
</CCard>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Component-specific styles */
|
||||
</style>
|
||||
```
|
||||
|
||||
**Using Composition API**:
|
||||
```vue
|
||||
<script setup>
|
||||
import { ref, computed, watch, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useStore } from '@/stores/main'
|
||||
|
||||
const data = ref([])
|
||||
const store = useStore()
|
||||
const router = useRouter()
|
||||
|
||||
const filteredData = computed(() => {
|
||||
return data.value.filter(item => item.active)
|
||||
})
|
||||
|
||||
watch(data, (newValue) => {
|
||||
console.log('Data changed:', newValue)
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
// Fetch data or run initialization
|
||||
})
|
||||
|
||||
const navigate = () => {
|
||||
router.push('/dashboard')
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
## Code Quality
|
||||
|
||||
**Linting**:
|
||||
- ESLint configuration in `eslint.config.mjs`
|
||||
- Prettier integration for code formatting
|
||||
- Vue-specific rules with eslint-plugin-vue
|
||||
- Run `npm run lint` before committing
|
||||
|
||||
**Best Practices**:
|
||||
- Always define props with types and validation
|
||||
- Use meaningful component and variable names
|
||||
- Keep components small and focused
|
||||
- Extract complex logic into composables
|
||||
- Use Vue DevTools for debugging
|
||||
- Test in both light and dark themes
|
||||
- Ensure responsive design works on all screen sizes
|
||||
|
||||
**Git Commits**:
|
||||
Follow conventional commit format:
|
||||
- feat: New feature
|
||||
- fix: Bug fix
|
||||
- docs: Documentation changes
|
||||
- style: Code style changes (formatting)
|
||||
- refactor: Code refactoring
|
||||
- test: Adding tests
|
||||
- chore: Maintenance tasks
|
||||
|
||||
## Common Patterns
|
||||
|
||||
**Lazy Loading Routes**:
|
||||
```javascript
|
||||
const Dashboard = () => import('./views/dashboard/Dashboard.vue')
|
||||
```
|
||||
|
||||
**Composables** (Custom hooks):
|
||||
```javascript
|
||||
// composables/useFetch.js
|
||||
import { ref } from 'vue'
|
||||
|
||||
export function useFetch(url) {
|
||||
const data = ref(null)
|
||||
const error = ref(null)
|
||||
const loading = ref(true)
|
||||
|
||||
fetch(url)
|
||||
.then(res => res.json())
|
||||
.then(json => {
|
||||
data.value = json
|
||||
})
|
||||
.catch(err => {
|
||||
error.value = err
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
|
||||
return { data, error, loading }
|
||||
}
|
||||
```
|
||||
|
||||
**Pinia Store**:
|
||||
```javascript
|
||||
// stores/main.js
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
|
||||
export const useStore = defineStore('main', () => {
|
||||
const sidebarVisible = ref(true)
|
||||
const theme = ref('light')
|
||||
|
||||
function toggleSidebar() {
|
||||
sidebarVisible.value = !sidebarVisible.value
|
||||
}
|
||||
|
||||
function setTheme(newTheme) {
|
||||
theme.value = newTheme
|
||||
}
|
||||
|
||||
return { sidebarVisible, theme, toggleSidebar, setTheme }
|
||||
})
|
||||
```
|
||||
|
||||
**Navigation Configuration** (`_nav.js`):
|
||||
```javascript
|
||||
export default [
|
||||
{
|
||||
component: 'CNavItem',
|
||||
name: 'Dashboard',
|
||||
to: '/dashboard',
|
||||
icon: 'cil-speedometer',
|
||||
},
|
||||
]
|
||||
```
|
||||
|
||||
## AI Assistant Guidelines
|
||||
|
||||
**What AI Should Do**:
|
||||
- Generate code using CoreUI Vue components
|
||||
- Follow existing code style and conventions
|
||||
- Use Composition API with <script setup>
|
||||
- Implement responsive designs
|
||||
- Define props with proper validation
|
||||
- Follow the project's file organization
|
||||
- Use existing utility functions and helpers
|
||||
- Suggest performance optimizations when appropriate
|
||||
|
||||
**What AI Should NOT Do**:
|
||||
- Use Options API (use Composition API instead)
|
||||
- Import components from libraries other than CoreUI/Vue
|
||||
- Suggest Tailwind CSS or other CSS frameworks
|
||||
- Ignore ESLint/Prettier rules
|
||||
- Create files outside the src/ directory structure
|
||||
- Modify build configuration without clear reason
|
||||
- Use deprecated Vue patterns
|
||||
|
||||
## External Dependencies
|
||||
|
||||
**Core Libraries**:
|
||||
- @coreui/coreui: ^5.6.1 - CoreUI CSS framework
|
||||
- @coreui/vue: ^5.8.0 - CoreUI Vue components
|
||||
- @coreui/icons-vue: 2.2.0 - CoreUI icons for Vue
|
||||
- vue: ^3.5.31 - Vue framework
|
||||
- vue-router: ^5.0.4 - Routing library
|
||||
- pinia: ^3.0.4 - State management
|
||||
|
||||
**Additional Libraries**:
|
||||
- chart.js: ^4.5.1 - Charting library
|
||||
- @coreui/vue-chartjs: ^3.0.0 - CoreUI Chart.js wrapper for Vue
|
||||
- simplebar-vue: ^2.4.2 - Custom scrollbars
|
||||
- @coreui/utils: ^2.0.2 - Utility functions
|
||||
|
||||
## Browser Support
|
||||
|
||||
Modern browsers with ES6+ support:
|
||||
- Chrome (latest)
|
||||
- Firefox (latest)
|
||||
- Safari (latest)
|
||||
- Edge (latest)
|
||||
|
||||
## Resources
|
||||
|
||||
- CoreUI Vue Documentation: https://coreui.io/vue/docs/
|
||||
- CoreUI Components: https://coreui.io/vue/docs/components/
|
||||
- Vue 3 Documentation: https://vuejs.org/
|
||||
- Vue Router: https://router.vuejs.org/
|
||||
- Pinia: https://pinia.vuejs.org/
|
||||
- Vite: https://vitejs.dev/
|
||||
|
||||
---
|
||||
|
||||
Remember: This is a Vue 3 application using CoreUI Vue components. Always check CoreUI Vue documentation for component APIs and usage examples. Keep the code clean, maintainable, and following Vue 3 best practices with Composition API.
|
||||
+716
@@ -0,0 +1,716 @@
|
||||
# CoreUI Free Vue Admin Template - Architecture
|
||||
|
||||
This document provides a comprehensive overview of the CoreUI Free Vue Admin Template architecture, design patterns, and technical implementation details.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Project Overview](#project-overview)
|
||||
- [Technology Stack](#technology-stack)
|
||||
- [Architectural Pattern](#architectural-pattern)
|
||||
- [Directory Structure](#directory-structure)
|
||||
- [Core Components](#core-components)
|
||||
- [Routing System](#routing-system)
|
||||
- [State Management](#state-management)
|
||||
- [Styling Architecture](#styling-architecture)
|
||||
- [Build System](#build-system)
|
||||
- [Performance Optimizations](#performance-optimizations)
|
||||
- [Browser Support](#browser-support)
|
||||
|
||||
## Project Overview
|
||||
|
||||
The CoreUI Free Vue Admin Template is a professional admin dashboard built on Vue 3, CoreUI Vue components, and Bootstrap 5. It follows modern Vue patterns with Composition API, script setup syntax, and a component-based architecture.
|
||||
|
||||
**Key Features**:
|
||||
- Single Page Application (SPA) with client-side routing
|
||||
- Responsive design with Bootstrap 5 grid system
|
||||
- Dark/Light theme support with automatic detection
|
||||
- Lazy loading and code splitting for optimal performance
|
||||
- Pinia-based state management
|
||||
- Modular and extensible component architecture
|
||||
|
||||
## Technology Stack
|
||||
|
||||
### Frontend Core
|
||||
|
||||
| Technology | Version | Purpose |
|
||||
|------------|---------|---------|
|
||||
| Vue | 3.5.31 | Progressive framework for building user interfaces |
|
||||
| Vue Router | 5.0.4 | Official router for Vue.js |
|
||||
| Pinia | 3.0.4 | Official state management library for Vue |
|
||||
|
||||
### UI Framework
|
||||
|
||||
| Library | Version | Purpose |
|
||||
|---------|---------|---------|
|
||||
| @coreui/coreui | 5.6.1 | CoreUI CSS framework based on Bootstrap 5 |
|
||||
| @coreui/vue | 5.8.0 | CoreUI Vue components |
|
||||
| @coreui/icons | 3.0.1 | CoreUI icon set |
|
||||
| @coreui/icons-vue | 2.2.0 | CoreUI icons as Vue components |
|
||||
| @coreui/utils | 2.0.2 | Utility functions for CoreUI |
|
||||
| simplebar-vue | 2.4.2 | Custom scrollbar component |
|
||||
|
||||
### Data Visualization
|
||||
|
||||
| Library | Version | Purpose |
|
||||
|---------|---------|---------|
|
||||
| Chart.js | 4.5.1 | HTML5 charting library |
|
||||
| @coreui/chartjs | 4.2.0 | CoreUI Chart.js themes and defaults |
|
||||
| @coreui/vue-chartjs | 3.0.0 | Vue wrapper for Chart.js with CoreUI styling |
|
||||
|
||||
### Build Tools & Development
|
||||
|
||||
| Tool | Version | Purpose |
|
||||
|------|---------|---------|
|
||||
| Vite | 8.0.3 | Fast build tool and dev server with HMR |
|
||||
| @vitejs/plugin-vue | 6.0.5 | Vite plugin for Vue 3 |
|
||||
| Sass | 1.98.0 | CSS preprocessor for styling |
|
||||
| PostCSS | 8.5.8 | CSS transformation with autoprefixer |
|
||||
| Autoprefixer | 10.4.27 | Automatic vendor prefixing |
|
||||
| ESLint | 9.39.4 | JavaScript linting and code quality |
|
||||
| eslint-plugin-vue | 10.8.0 | Vue-specific ESLint rules |
|
||||
|
||||
### Utilities
|
||||
|
||||
| Library | Version | Purpose |
|
||||
|---------|---------|---------|
|
||||
| @popperjs/core | 2.11.8 | Tooltip and popover positioning |
|
||||
|
||||
## Architectural Pattern
|
||||
|
||||
### Component-Based Architecture
|
||||
|
||||
The application follows a **Composition API architecture** with script setup syntax:
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────┐
|
||||
│ Application (App.vue) │
|
||||
│ - Router View │
|
||||
│ - Theme Management │
|
||||
│ - Route Configuration │
|
||||
└──────────────────────────────────────────┘
|
||||
↓
|
||||
┌───────────────┴────────────────┐
|
||||
│ │
|
||||
┌───▼────┐ ┌────────▼───────┐
|
||||
│ Public │ │ Protected │
|
||||
│ Routes │ │ Routes │
|
||||
│ │ │(DefaultLayout) │
|
||||
│ Login │ └───────┬────────┘
|
||||
│Register│ │
|
||||
│ 404 │ ┌───────────┼────────────┐
|
||||
│ 500 │ │ │ │
|
||||
└────────┘ ┌────▼────┐ ┌────▼─────┐ ┌────▼─────┐
|
||||
│AppHeader│ │AppSidebar│ │AppContent│
|
||||
└─────────┘ └──────────┘ └────┬─────┘
|
||||
│
|
||||
┌───────▼─────────┐
|
||||
│ View Components │
|
||||
│ (Dashboard, │
|
||||
│ Forms, etc.) │
|
||||
└─────────────────┘
|
||||
```
|
||||
|
||||
### Single Page Application (SPA) Pattern
|
||||
|
||||
The template uses client-side routing with createWebHashHistory:
|
||||
1. **Initial Load**: HTML shell loads, Vue initializes
|
||||
2. **Route Matching**: Vue Router matches URL to component
|
||||
3. **Lazy Loading**: Component bundles load on-demand
|
||||
4. **Rendering**: Component renders with layout wrapper
|
||||
5. **Navigation**: Client-side transitions without page reload
|
||||
|
||||
### State Management Pattern
|
||||
|
||||
Pinia manages global application state:
|
||||
|
||||
```javascript
|
||||
Store (stores/)
|
||||
├── sidebarVisible (boolean)
|
||||
├── sidebarUnfoldable (boolean)
|
||||
└── theme (string: light/dark/auto)
|
||||
```
|
||||
|
||||
Component-level state uses Vue's Composition API (ref, reactive).
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
coreui-free-vue-admin-template/
|
||||
│
|
||||
├── public/ # Static assets (served as-is)
|
||||
│ ├── favicon.ico
|
||||
│ └── robots.txt
|
||||
│
|
||||
├── src/ # Source code
|
||||
│ │
|
||||
│ ├── assets/ # Application assets
|
||||
│ │ ├── brand/ # Logo components (logo.js, sygnet.js)
|
||||
│ │ └── images/ # Image files (avatars, etc.)
|
||||
│ │
|
||||
│ ├── components/ # Reusable UI components
|
||||
│ │ ├── AppBreadcrumb.vue # Breadcrumb navigation
|
||||
│ │ ├── AppContent.vue # Main content area wrapper
|
||||
│ │ ├── AppFooter.vue # Footer component
|
||||
│ │ ├── AppHeader.vue # Header component
|
||||
│ │ ├── AppHeaderDropdown.vue # User dropdown menu
|
||||
│ │ ├── AppSidebar.vue # Sidebar navigation
|
||||
│ │ ├── DocsComponents.vue # Documentation component showcase
|
||||
│ │ └── DocsExample.vue # Code example wrapper
|
||||
│ │
|
||||
│ ├── layouts/ # Layout wrapper components
|
||||
│ │ └── DefaultLayout.vue # Main application layout
|
||||
│ │
|
||||
│ ├── views/ # Page/view components
|
||||
│ │ ├── dashboard/ # Dashboard page
|
||||
│ │ │ ├── Dashboard.vue
|
||||
│ │ │ └── MainChart.vue
|
||||
│ │ ├── base/ # Base UI component examples
|
||||
│ │ │ ├── Accordion.vue
|
||||
│ │ │ ├── Breadcrumbs.vue
|
||||
│ │ │ ├── Cards.vue
|
||||
│ │ │ ├── Carousels.vue
|
||||
│ │ │ ├── Chips.vue
|
||||
│ │ │ ├── Collapses.vue
|
||||
│ │ │ ├── ListGroups.vue
|
||||
│ │ │ ├── Navs.vue
|
||||
│ │ │ ├── Paginations.vue
|
||||
│ │ │ ├── Placeholders.vue
|
||||
│ │ │ ├── Popovers.vue
|
||||
│ │ │ ├── Progress.vue
|
||||
│ │ │ ├── Spinners.vue
|
||||
│ │ │ ├── Tables.vue
|
||||
│ │ │ ├── Tabs.vue
|
||||
│ │ │ └── Tooltips.vue
|
||||
│ │ ├── buttons/ # Button examples
|
||||
│ │ ├── charts/ # Chart examples
|
||||
│ │ ├── forms/ # Form examples
|
||||
│ │ │ └── ChipInput.vue
|
||||
│ │ ├── icons/ # Icon examples
|
||||
│ │ ├── notifications/ # Notification examples
|
||||
│ │ ├── widgets/ # Widget examples
|
||||
│ │ ├── theme/ # Theme examples
|
||||
│ │ └── pages/ # Special pages
|
||||
│ │ ├── Login.vue # Login page
|
||||
│ │ ├── Register.vue # Registration page
|
||||
│ │ ├── Page404.vue # 404 error page
|
||||
│ │ └── Page500.vue # 500 error page
|
||||
│ │
|
||||
│ ├── router/ # Router configuration
|
||||
│ │ └── index.js # Route definitions
|
||||
│ │
|
||||
│ ├── stores/ # Pinia stores (if created)
|
||||
│ │
|
||||
│ ├── scss/ # Global stylesheets
|
||||
│ │ ├── style.scss # Main stylesheet (imports CoreUI)
|
||||
│ │ └── _custom.scss # Custom style overrides
|
||||
│ │
|
||||
│ ├── App.vue # Root application component
|
||||
│ ├── main.js # Application entry point
|
||||
│ └── _nav.js # Sidebar navigation configuration
|
||||
│
|
||||
├── node_modules/ # Dependencies
|
||||
├── index.html # HTML entry point
|
||||
├── vite.config.mjs # Vite build configuration
|
||||
├── eslint.config.mjs # ESLint configuration
|
||||
├── package.json # Project metadata and dependencies
|
||||
├── .browserslistrc # Browser compatibility targets
|
||||
├── .editorconfig # Editor configuration
|
||||
└── README.md # Project documentation
|
||||
```
|
||||
|
||||
## Core Components
|
||||
|
||||
### Application Component (App.vue)
|
||||
|
||||
The root component that:
|
||||
- Sets up router-view for component rendering
|
||||
- Manages theme initialization
|
||||
- Provides the application shell
|
||||
|
||||
**Key Features**:
|
||||
- Theme detection and persistence
|
||||
- Suspense boundaries for lazy-loaded routes
|
||||
- Global error handling boundaries
|
||||
|
||||
### Layout System
|
||||
|
||||
#### DefaultLayout (layouts/DefaultLayout.vue)
|
||||
|
||||
The main application layout wrapper that composes:
|
||||
- **AppSidebar**: Collapsible navigation sidebar
|
||||
- **AppHeader**: Top navigation bar with breadcrumbs and user menu
|
||||
- **AppContent**: Main content area with routing
|
||||
- **AppFooter**: Footer with version and links
|
||||
|
||||
**Responsibility**: Provides consistent layout structure for authenticated views.
|
||||
|
||||
#### Navigation Components
|
||||
|
||||
**AppSidebar** (`components/AppSidebar.vue`):
|
||||
- Renders collapsible sidebar
|
||||
- Uses Pinia for show/hide state
|
||||
- Integrates with _nav.js for menu structure
|
||||
- Includes branding section
|
||||
|
||||
**AppHeader** (`components/AppHeader.vue`):
|
||||
- Fixed top navigation bar
|
||||
- Sidebar toggle button
|
||||
- Breadcrumb navigation
|
||||
- User dropdown menu
|
||||
- Theme switcher
|
||||
|
||||
### View Components
|
||||
|
||||
View components are page-level components that:
|
||||
- Render specific application features (Dashboard, Forms, Charts)
|
||||
- Use CoreUI Vue components for UI
|
||||
- Connect to Pinia when needed for global state
|
||||
- Implement business logic and data fetching
|
||||
|
||||
**Example Structure**:
|
||||
```vue
|
||||
<template>
|
||||
<CCard>
|
||||
<CCardBody>
|
||||
{{ data }}
|
||||
</CCardBody>
|
||||
</CCard>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
|
||||
const data = ref([])
|
||||
|
||||
onMounted(() => {
|
||||
// Fetch dashboard data
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
## Routing System
|
||||
|
||||
### Vue Router v5
|
||||
|
||||
The application uses Vue Router for declarative routing:
|
||||
|
||||
**Configuration** (`router/index.js`):
|
||||
```javascript
|
||||
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'Home',
|
||||
component: DefaultLayout,
|
||||
redirect: '/dashboard',
|
||||
children: [
|
||||
{
|
||||
path: '/dashboard',
|
||||
name: 'Dashboard',
|
||||
component: () => import('@/views/dashboard/Dashboard.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/pages',
|
||||
redirect: '/pages/404',
|
||||
name: 'Pages',
|
||||
component: {
|
||||
render() {
|
||||
return h(resolveComponent('router-view'))
|
||||
},
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '404',
|
||||
name: 'Page404',
|
||||
component: () => import('@/views/pages/Page404'),
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(import.meta.env.BASE_URL),
|
||||
routes,
|
||||
scrollBehavior() {
|
||||
return { top: 0 }
|
||||
},
|
||||
})
|
||||
|
||||
export default router
|
||||
```
|
||||
|
||||
### Lazy Loading & Code Splitting
|
||||
|
||||
All routes use dynamic imports for lazy loading:
|
||||
|
||||
```javascript
|
||||
component: () => import('./views/dashboard/Dashboard.vue')
|
||||
```
|
||||
|
||||
**Benefits**:
|
||||
- Smaller initial bundle size
|
||||
- Faster first page load
|
||||
- Components load only when navigated to
|
||||
- Automatic code splitting by Vite
|
||||
|
||||
### Navigation Configuration
|
||||
|
||||
Navigation structure defined in `_nav.js`:
|
||||
|
||||
```javascript
|
||||
export default [
|
||||
{
|
||||
component: 'CNavItem',
|
||||
name: 'Dashboard',
|
||||
to: '/dashboard',
|
||||
icon: 'cil-speedometer',
|
||||
badge: {
|
||||
color: 'info',
|
||||
text: 'NEW',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'CNavGroup',
|
||||
name: 'Base',
|
||||
icon: 'cil-puzzle',
|
||||
items: [
|
||||
{
|
||||
component: 'CNavItem',
|
||||
name: 'Accordion',
|
||||
to: '/base/accordion',
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
```
|
||||
|
||||
## State Management
|
||||
|
||||
### Pinia Store Architecture
|
||||
|
||||
Pinia uses the Composition API pattern:
|
||||
|
||||
**Store Example** (`stores/main.js`):
|
||||
|
||||
```javascript
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
|
||||
export const useStore = defineStore('main', () => {
|
||||
const sidebarVisible = ref(true)
|
||||
const sidebarUnfoldable = ref(false)
|
||||
const theme = ref('light')
|
||||
|
||||
function toggleSidebar() {
|
||||
sidebarVisible.value = !sidebarVisible.value
|
||||
}
|
||||
|
||||
function setSidebarUnfoldable(value) {
|
||||
sidebarUnfoldable.value = value
|
||||
}
|
||||
|
||||
function setTheme(value) {
|
||||
theme.value = value
|
||||
}
|
||||
|
||||
return {
|
||||
sidebarVisible,
|
||||
sidebarUnfoldable,
|
||||
theme,
|
||||
toggleSidebar,
|
||||
setSidebarUnfoldable,
|
||||
setTheme,
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### State Usage in Components
|
||||
|
||||
**Reading State**:
|
||||
```vue
|
||||
<script setup>
|
||||
import { useStore } from '@/stores/main'
|
||||
|
||||
const store = useStore()
|
||||
// Access: store.sidebarVisible
|
||||
</script>
|
||||
```
|
||||
|
||||
**Updating State**:
|
||||
```vue
|
||||
<script setup>
|
||||
import { useStore } from '@/stores/main'
|
||||
|
||||
const store = useStore()
|
||||
|
||||
const toggleSidebar = () => {
|
||||
store.toggleSidebar()
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
## Styling Architecture
|
||||
|
||||
### Sass/SCSS Structure
|
||||
|
||||
**Main Stylesheet** (`src/scss/style.scss`):
|
||||
```scss
|
||||
@use "@coreui/coreui/scss/coreui" as * with (
|
||||
$enable-deprecation-messages: false
|
||||
);
|
||||
|
||||
// Custom variables and overrides
|
||||
@import 'custom';
|
||||
```
|
||||
|
||||
**Custom Overrides** (`src/scss/_custom.scss`):
|
||||
```scss
|
||||
// Override CoreUI/Bootstrap variables
|
||||
$primary: #321fdb;
|
||||
$secondary: #ced2d8;
|
||||
|
||||
// Custom styles
|
||||
.my-custom-class {
|
||||
// styles
|
||||
}
|
||||
```
|
||||
|
||||
### CSS Custom Properties (CSS Variables)
|
||||
|
||||
CoreUI uses CSS custom properties for theming:
|
||||
|
||||
```css
|
||||
:root {
|
||||
--cui-primary: #321fdb;
|
||||
--cui-secondary: #ced2d8;
|
||||
--cui-body-bg: #ebedef;
|
||||
--cui-body-color: #4f5d73;
|
||||
}
|
||||
|
||||
[data-coreui-theme="dark"] {
|
||||
--cui-body-bg: #2b3035;
|
||||
--cui-body-color: #b4bac0;
|
||||
}
|
||||
```
|
||||
|
||||
**Usage in Components**:
|
||||
```vue
|
||||
<template>
|
||||
<div :style="{ backgroundColor: 'var(--cui-primary)' }">Content</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
### Component Styling
|
||||
|
||||
**Scoped Styles**:
|
||||
```vue
|
||||
<style scoped>
|
||||
.my-component {
|
||||
padding: 1rem;
|
||||
background-color: var(--cui-light);
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
**Class Bindings**:
|
||||
```vue
|
||||
<template>
|
||||
<button :class="['btn', { 'btn-primary': isPrimary, 'active': isActive }]">
|
||||
Click
|
||||
</button>
|
||||
</template>
|
||||
```
|
||||
|
||||
**Bootstrap Utilities**:
|
||||
```vue
|
||||
<template>
|
||||
<CCard class="mb-4 shadow-sm">
|
||||
<CCardBody class="p-4 d-flex justify-content-between">
|
||||
</CCardBody>
|
||||
</CCard>
|
||||
</template>
|
||||
```
|
||||
|
||||
## Build System
|
||||
|
||||
### Vite Configuration
|
||||
|
||||
**File**: `vite.config.mjs`
|
||||
|
||||
```javascript
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import autoprefixer from 'autoprefixer'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||
},
|
||||
extensions: ['.js', '.json', '.jsx', '.mjs', '.ts', '.tsx', '.vue'],
|
||||
},
|
||||
server: {
|
||||
port: 3000,
|
||||
},
|
||||
css: {
|
||||
postcss: {
|
||||
plugins: [autoprefixer()],
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### Build Process
|
||||
|
||||
**Development Build**:
|
||||
1. Vite starts dev server on port 3000
|
||||
2. esbuild compiles Vue SFC components
|
||||
3. PostCSS processes Sass/SCSS with autoprefixer
|
||||
4. Hot Module Replacement (HMR) for instant updates
|
||||
|
||||
**Production Build**:
|
||||
1. `vite build` command
|
||||
2. Code minification and tree-shaking
|
||||
3. Asset optimization (images, fonts)
|
||||
4. CSS extraction and minification
|
||||
5. Source maps generation
|
||||
6. Output to `dist/` directory
|
||||
|
||||
**Build Output**:
|
||||
```
|
||||
dist/
|
||||
├── assets/
|
||||
│ ├── index-[hash].js # Main bundle
|
||||
│ ├── [component]-[hash].js # Lazy-loaded chunks
|
||||
│ └── index-[hash].css # Extracted CSS
|
||||
├── index.html # HTML entry
|
||||
└── favicon.ico # Static assets
|
||||
```
|
||||
|
||||
### Code Splitting Strategy
|
||||
|
||||
**Automatic Splitting**:
|
||||
- Each lazy-loaded route becomes a separate chunk
|
||||
- Vendor libraries (Vue, CoreUI) in separate vendor chunk
|
||||
- Dynamic imports create split points
|
||||
|
||||
**Manual Splitting** (if needed):
|
||||
```javascript
|
||||
const HeavyComponent = () => import(/* webpackChunkName: "heavy" */ './HeavyComponent.vue')
|
||||
```
|
||||
|
||||
## Performance Optimizations
|
||||
|
||||
### Implemented Optimizations
|
||||
|
||||
1. **Lazy Loading**: All routes lazy-loaded with dynamic imports
|
||||
2. **Code Splitting**: Separate bundles per route
|
||||
3. **Tree Shaking**: Unused code eliminated by Vite
|
||||
4. **Asset Optimization**: Images and fonts optimized
|
||||
5. **CSS Extraction**: Separate CSS bundle for caching
|
||||
6. **Hash-based Caching**: File names include content hash
|
||||
|
||||
### Component Optimization
|
||||
|
||||
**v-memo** for expensive renders:
|
||||
```vue
|
||||
<template>
|
||||
<div v-memo="[valueA, valueB]">
|
||||
<!-- Heavy rendering -->
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
**computed** for derived values:
|
||||
```vue
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
const sortedData = computed(() => {
|
||||
return data.value.sort((a, b) => a.value - b.value)
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
### Bundle Size Management
|
||||
|
||||
**Strategies**:
|
||||
- Use named imports: `import { CButton } from '@coreui/vue'`
|
||||
- Avoid importing entire libraries
|
||||
- Check bundle size with `npm run build`
|
||||
- Use Vite's rollup visualizer for analysis
|
||||
|
||||
## Browser Support
|
||||
|
||||
### Target Browsers
|
||||
|
||||
Defined in `.browserslistrc`:
|
||||
```
|
||||
> 0.5%
|
||||
last 2 versions
|
||||
Firefox ESR
|
||||
not dead
|
||||
not IE 11
|
||||
```
|
||||
|
||||
### Polyfills
|
||||
|
||||
Modern browsers support Vue 3 natively. No additional polyfills needed for ES6+ features.
|
||||
|
||||
### Progressive Enhancement
|
||||
|
||||
- Modern features with fallbacks
|
||||
- CSS Grid with flexbox fallback
|
||||
- Modern color modes with theme classes
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### Best Practices
|
||||
|
||||
1. **Content Security Policy**: Configure CSP headers
|
||||
2. **XSS Prevention**: Vue escapes content by default
|
||||
3. **Dependency Auditing**: Run `npm audit` regularly
|
||||
4. **Environment Variables**: Use `.env` files (not committed)
|
||||
5. **HTTPS**: Serve over HTTPS in production
|
||||
|
||||
### Vue Security
|
||||
|
||||
- Avoid `v-html` unless necessary and sanitize content
|
||||
- Validate user input before rendering
|
||||
- Use prop validation for type safety
|
||||
- Keep dependencies updated
|
||||
|
||||
## Deployment
|
||||
|
||||
### Static Hosting
|
||||
|
||||
The application builds to static files suitable for:
|
||||
- Netlify
|
||||
- Vercel
|
||||
- GitHub Pages
|
||||
- AWS S3 + CloudFront
|
||||
- Any static file server
|
||||
|
||||
### Build for Production
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
Output in `dist/` directory ready for deployment.
|
||||
|
||||
### HashRouter for Static Hosts
|
||||
|
||||
Uses createWebHashHistory for GitHub Pages compatibility:
|
||||
- URLs: `https://example.com/#/dashboard`
|
||||
- No server-side routing configuration needed
|
||||
- Works with any static host
|
||||
|
||||
---
|
||||
|
||||
This architecture provides a solid foundation for building modern, performant admin dashboards with Vue 3 and CoreUI. The modular structure allows for easy extension and customization while maintaining code quality and best practices.
|
||||
+1113
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2025 creativeLabs Łukasz Holeczek.
|
||||
Copyright (c) 2026 creativeLabs Łukasz Holeczek.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# CoreUI Free Vue Admin Template [](https://twitter.com/intent/tweet?text=CoreUI%20-%20Free%Vue%204%20Admin%20Template%20&url=https://coreui.io&hashtags=bootstrap,admin,template,dashboard,panel,free,angular,react,vue)
|
||||
# CoreUI Free Vue Admin Template - Built for AI-Assisted Development [](https://twitter.com/intent/tweet?text=CoreUI%20-%20Free%Vue%204%20Admin%20Template%20&url=https://coreui.io&hashtags=bootstrap,admin,template,dashboard,panel,free,angular,react,vue)
|
||||
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
[](https://github.com/coreui/coreui)
|
||||
@@ -29,6 +29,7 @@ CoreUI is meant to be the UX game changer. Pure & transparent code is devoid of
|
||||
* [Installation](#installation)
|
||||
* [Basic usage](#basic-usage)
|
||||
* [What's included](#whats-included)
|
||||
* [AI-Friendly Development](#ai-friendly-development)
|
||||
* [Documentation](#documentation)
|
||||
* [Components](#components)
|
||||
* [Versioning](#versioning)
|
||||
@@ -135,6 +136,43 @@ coreui-free-vue-admin-template
|
||||
└── vite.config.mjs
|
||||
```
|
||||
|
||||
## AI-Friendly Development
|
||||
|
||||
This template is designed to work seamlessly with AI coding assistants like Cursor, GitHub Copilot, and Claude Code. We've included comprehensive documentation and configuration files to help AI understand the project structure and conventions.
|
||||
|
||||
### Features for AI Development
|
||||
|
||||
- **`.cursorrules`**: Complete AI assistant configuration with project conventions, technology stack, and coding patterns
|
||||
- **`ARCHITECTURE.md`**: Detailed technical architecture documentation covering components, routing, state management, and build system
|
||||
- **`DEVELOPMENT.md`**: Comprehensive development guide with setup instructions, workflows, and best practices
|
||||
- **JSDoc Comments**: Main Vue components include documentation for better AI understanding
|
||||
|
||||
### Getting Started with AI Assistants
|
||||
|
||||
1. **Open the project** in your AI-powered IDE (VS Code with Cursor, GitHub Copilot, or Claude Code)
|
||||
2. **Review `.cursorrules`** to understand the project conventions
|
||||
3. **Ask your AI assistant** to help you build features - it will automatically follow the project patterns
|
||||
4. **Use natural language** to describe components, pages, or features you want to add
|
||||
|
||||
### What AI Assistants Know About This Project
|
||||
|
||||
Your AI assistant understands:
|
||||
- **Component Library**: Always use CoreUI Vue components (never Tailwind, Vuetify, or other libraries)
|
||||
- **Code Style**: Vue 3 Composition API with `<script setup>` syntax, Prettier formatting, ESLint rules
|
||||
- **Architecture**: Single Page Application with Vue Router, Pinia for state management, Vite for building
|
||||
- **File Organization**: Where to create components, views, routes, and styles
|
||||
- **Common Patterns**: Lazy loading, composables, navigation configuration, theming
|
||||
|
||||
### Example AI Prompts
|
||||
|
||||
Try asking your AI assistant:
|
||||
- "Create a new products page with a table showing product name, price, and status"
|
||||
- "Add a user profile form with validation"
|
||||
- "Create a chart showing monthly sales data"
|
||||
- "Add a new navigation item for the settings page"
|
||||
|
||||
For more information, see the [DEVELOPMENT.md](DEVELOPMENT.md) guide.
|
||||
|
||||
## Documentation
|
||||
|
||||
The documentation for the CoreUI Admin Template is hosted at our website [CoreUI for Vue](https://coreui.io/vue/docs/templates/installation.html)
|
||||
@@ -155,6 +193,8 @@ CoreUI Vue.js Admin Templates are built on top of CoreUI and CoreUI PRO UI compo
|
||||
- [Vue Card](https://coreui.io/vue/docs/components/card.html)
|
||||
- [Vue Carousel](https://coreui.io/vue/docs/components/carousel.html)
|
||||
- [Vue Checkbox](https://coreui.io/vue/docs/forms/checkbox.html)
|
||||
- [Vue Chip](https://coreui.io/vue/docs/components/chip.html)
|
||||
- [Vue Chip Input](https://coreui.io/vue/docs/forms/chip-input.html)
|
||||
- [Vue Close Button](https://coreui.io/vue/docs/components/close-button.html)
|
||||
- [Vue Collapse](https://coreui.io/vue/docs/components/collapse.html)
|
||||
- [Vue Date Picker](https://coreui.io/vue/docs/forms/date-picker.html) **PRO**
|
||||
@@ -231,6 +271,6 @@ CoreUI is an MIT-licensed open source project and is completely free to use. How
|
||||
|
||||
## Copyright and License
|
||||
|
||||
copyright 2025 creativeLabs Łukasz Holeczek.
|
||||
copyright 2026 creativeLabs Łukasz Holeczek.
|
||||
|
||||
Code released under [the MIT license](https://github.com/coreui/coreui-free-react-admin-template/blob/main/LICENSE).
|
||||
|
||||
+2
-2
@@ -1,9 +1,9 @@
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
* CoreUI Free Vue.js Admin Template
|
||||
* @version v5.4.0
|
||||
* @version v5.5.0
|
||||
* @link https://coreui.io/product/free-vue-admin-template/
|
||||
* Copyright (c) 2025 creativeLabs Łukasz Holeczek
|
||||
* Copyright (c) 2026 creativeLabs Łukasz Holeczek
|
||||
* Licensed under MIT (https://github.com/coreui/coreui-free-vue-admin-template/blob/main/LICENSE)
|
||||
-->
|
||||
<html>
|
||||
|
||||
+17
-16
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@coreui/coreui-free-vue-admin-template",
|
||||
"version": "5.4.0",
|
||||
"version": "5.5.0",
|
||||
"description": "CoreUI Free Vue Admin Template",
|
||||
"bugs": {
|
||||
"url": "https://github.com/coreui/coreui-free-vue-admin-template/issues"
|
||||
@@ -18,28 +18,29 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@coreui/chartjs": "^4.1.0",
|
||||
"@coreui/coreui": "^5.4.1",
|
||||
"@coreui/chartjs": "^4.2.0",
|
||||
"@coreui/coreui": "^5.6.1",
|
||||
"@coreui/icons": "^3.0.1",
|
||||
"@coreui/icons-vue": "2.2.0",
|
||||
"@coreui/utils": "^2.0.2",
|
||||
"@coreui/vue": "^5.5.0",
|
||||
"@coreui/vue": "^5.8.0",
|
||||
"@coreui/vue-chartjs": "^3.0.0",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"chart.js": "^4.5.0",
|
||||
"pinia": "^3.0.3",
|
||||
"axios": "^1.18.1",
|
||||
"chart.js": "^4.5.1",
|
||||
"pinia": "^3.0.4",
|
||||
"simplebar-vue": "^2.4.2",
|
||||
"vue": "^3.5.18",
|
||||
"vue-router": "^4.5.1"
|
||||
"vue": "^3.5.31",
|
||||
"vue-router": "^5.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"eslint": "^9.32.0",
|
||||
"eslint-plugin-vue": "^10.4.0",
|
||||
"globals": "^16.3.0",
|
||||
"postcss": "^8.5.6",
|
||||
"sass": "^1.90.0",
|
||||
"vite": "^7.1.0"
|
||||
"@vitejs/plugin-vue": "^6.0.5",
|
||||
"autoprefixer": "^10.4.27",
|
||||
"eslint": "^9.39.4",
|
||||
"eslint-plugin-vue": "^10.8.0",
|
||||
"globals": "^16.5.0",
|
||||
"postcss": "^8.5.8",
|
||||
"sass": "^1.98.0",
|
||||
"vite": "^8.0.3"
|
||||
}
|
||||
}
|
||||
|
||||
+14
@@ -1,9 +1,23 @@
|
||||
<script setup>
|
||||
/**
|
||||
* App.vue - Main Application Component
|
||||
*
|
||||
* This is the root component of the CoreUI Free Vue Admin Template.
|
||||
* It handles theme initialization and provides the router-view for all routes.
|
||||
*
|
||||
* Key responsibilities:
|
||||
* - Theme detection from URL parameters
|
||||
* - Theme persistence with localStorage
|
||||
* - Router view rendering for SPA navigation
|
||||
*
|
||||
* @component
|
||||
*/
|
||||
import { onBeforeMount } from 'vue'
|
||||
import { useColorModes } from '@coreui/vue'
|
||||
|
||||
import { useThemeStore } from '@/stores/theme.js'
|
||||
|
||||
// Initialize CoreUI color modes with local storage key
|
||||
const { isColorModeSet, setColorMode } = useColorModes(
|
||||
'coreui-free-vue-admin-template-theme',
|
||||
)
|
||||
|
||||
+76
-1
@@ -1,3 +1,26 @@
|
||||
/**
|
||||
* _nav.js - Sidebar Navigation Configuration
|
||||
*
|
||||
* This file defines the structure and content of the sidebar navigation menu.
|
||||
* The navigation is rendered by AppSidebar component using CoreUI nav components.
|
||||
*
|
||||
* Navigation item types:
|
||||
* - CNavItem: Single navigation link
|
||||
* - CNavGroup: Expandable group of navigation items
|
||||
* - CNavTitle: Section title/divider
|
||||
*
|
||||
* Each item can have:
|
||||
* - component: CoreUI component type ('CNavItem', 'CNavGroup', 'CNavTitle')
|
||||
* - name: Display text
|
||||
* - to: Vue Router path (for CNavItem)
|
||||
* - icon: CoreUI icon name (from @coreui/icons)
|
||||
* - badge: Optional badge with color and text
|
||||
* - items: Array of child items (for CNavGroup)
|
||||
* - href: External link URL
|
||||
* - external: Boolean for external links
|
||||
*
|
||||
* @type {Array<Object>}
|
||||
*/
|
||||
export default [
|
||||
{
|
||||
component: 'CNavItem',
|
||||
@@ -9,6 +32,48 @@ export default [
|
||||
text: 'NEW',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'CNavTitle',
|
||||
name: 'Ticketing',
|
||||
},
|
||||
{
|
||||
component: 'CNavGroup',
|
||||
name: 'Ticketing',
|
||||
to: '/ticket',
|
||||
icon: 'cil-layers',
|
||||
items: [
|
||||
{
|
||||
component: 'CNavItem',
|
||||
name: 'Ticket',
|
||||
to: '/tickets/ticket',
|
||||
icon: 'cil-notes',
|
||||
},
|
||||
{
|
||||
component: 'CNavItem',
|
||||
name: 'Ticket Type',
|
||||
to: '/tickets/ticket-type',
|
||||
icon: 'cil-list',
|
||||
},
|
||||
{
|
||||
component: 'CNavItem',
|
||||
name: 'Ticket Materials',
|
||||
to: '/tickets/ticket-materials',
|
||||
icon: 'cil-file',
|
||||
},
|
||||
{
|
||||
component: 'CNavItem',
|
||||
name: 'Approved',
|
||||
to: '/tickets/approved',
|
||||
icon: 'cil-check',
|
||||
},
|
||||
{
|
||||
component: 'CNavItem',
|
||||
name: 'Rejected',
|
||||
to: '/tickets/rejected',
|
||||
icon: 'cil-ban',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
component: 'CNavTitle',
|
||||
name: 'Theme',
|
||||
@@ -65,6 +130,11 @@ export default [
|
||||
name: 'Carousels',
|
||||
to: '/base/carousels',
|
||||
},
|
||||
{
|
||||
component: 'CNavItem',
|
||||
name: 'Chips',
|
||||
to: '/base/chips',
|
||||
},
|
||||
{
|
||||
component: 'CNavItem',
|
||||
name: 'Collapses',
|
||||
@@ -148,7 +218,7 @@ export default [
|
||||
color: 'danger',
|
||||
text: 'PRO',
|
||||
},
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -205,6 +275,11 @@ export default [
|
||||
name: 'Checks & Radios',
|
||||
to: '/forms/checks-radios',
|
||||
},
|
||||
{
|
||||
component: 'CNavItem',
|
||||
name: 'Chip Input',
|
||||
to: '/forms/chip-input',
|
||||
},
|
||||
{
|
||||
component: 'CNavItem',
|
||||
name: 'Date Picker',
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 129 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.0 MiB |
@@ -1,7 +1,8 @@
|
||||
<script setup>
|
||||
import { RouterLink } from 'vue-router'
|
||||
|
||||
import { logo } from '@/assets/brand/logo'
|
||||
// import { logo } from '@/assets/brand/logo'
|
||||
import logo from '@/assets/images/Logo_big.png'
|
||||
import { sygnet } from '@/assets/brand/sygnet'
|
||||
import { AppSidebarNav } from '@/components/AppSidebarNav.js'
|
||||
import { useSidebarStore } from '@/stores/sidebar.js'
|
||||
@@ -20,9 +21,27 @@ const sidebar = useSidebarStore()
|
||||
>
|
||||
<CSidebarHeader class="border-bottom">
|
||||
<RouterLink custom to="/" v-slot="{ href, navigate }">
|
||||
<CSidebarBrand v-bind="$attrs" as="a" :href="href" @click="navigate">
|
||||
<!-- <CSidebarBrand v-bind="$attrs" as="a" :href="href" @click="navigate">
|
||||
<CIcon custom-class-name="sidebar-brand-full" :icon="logo" :height="32" />
|
||||
<CIcon custom-class-name="sidebar-brand-narrow" :icon="sygnet" :height="32" />
|
||||
</CSidebarBrand> -->
|
||||
<CSidebarBrand
|
||||
v-bind="$attrs"
|
||||
as="a"
|
||||
:href="href"
|
||||
@click="navigate"
|
||||
class="d-flex align-items-center text-decoration-none"
|
||||
>
|
||||
<!-- Logo -->
|
||||
<img :src="logo" alt="Logo" style="height: 60px; width: auto" />
|
||||
|
||||
<!-- Teks -->
|
||||
<div class="ms-2 text-start">
|
||||
<div class="fw-bold" style="font-size: 30px; line-height: 1; color: #ff8c00">
|
||||
ManjaPro
|
||||
</div>
|
||||
<small class="fw-bold" style="font-size: 11px"> PT. Rhadika Data Nusantara </small>
|
||||
</div>
|
||||
</CSidebarBrand>
|
||||
</RouterLink>
|
||||
<CCloseButton class="d-lg-none" dark @click="sidebar.toggleVisible()" />
|
||||
|
||||
@@ -28,7 +28,7 @@ import ComponentsImg from '@/assets/images/components.webp'
|
||||
explore extended examples, detailed API documentation, and customization options, refer to
|
||||
our docs.
|
||||
</div>
|
||||
<div class="col-md-auto col-12 mt-3 mt-lg-0">
|
||||
<div class="col-md-auto col-12 mt-3 mt-lg-0 d-flex flex-column">
|
||||
<a
|
||||
class="btn btn-primary text-nowrap text-white"
|
||||
:href="`https://coreui.io/vue/docs/${props.href}`"
|
||||
@@ -37,6 +37,15 @@ import ComponentsImg from '@/assets/images/components.webp'
|
||||
>
|
||||
Explore Documentation
|
||||
</a>
|
||||
<div class="text-center my-1">or</div>
|
||||
<a
|
||||
class="btn btn-danger text-nowrap text-white"
|
||||
href="https://coreui.io/pricing/?framework=vue&src=free-vue-admin-template-docs-banner"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Get CoreUI PRO →
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+31
-3
@@ -1,24 +1,52 @@
|
||||
/**
|
||||
* main.js - Application Entry Point
|
||||
*
|
||||
* This file initializes the Vue 3 application and configures:
|
||||
* - Pinia for state management
|
||||
* - Vue Router for client-side routing
|
||||
* - CoreUI Vue component library
|
||||
* - Global icon system
|
||||
* - Documentation helper components
|
||||
*
|
||||
* The application uses:
|
||||
* - Vue 3 Composition API
|
||||
* - Vite for building and development
|
||||
* - CoreUI Vue components
|
||||
* - Hash-based routing for static hosting compatibility
|
||||
*/
|
||||
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
|
||||
// CoreUI Vue components and icons
|
||||
import CoreuiVue from '@coreui/vue'
|
||||
import CIcon from '@coreui/icons-vue'
|
||||
import { iconsSet as icons } from '@/assets/icons'
|
||||
|
||||
// Documentation components (remove in production if not needed)
|
||||
import DocsComponents from '@/components/DocsComponents'
|
||||
import DocsExample from '@/components/DocsExample'
|
||||
import DocsIcons from '@/components/DocsIcons'
|
||||
|
||||
// Create Vue application instance
|
||||
const app = createApp(App)
|
||||
app.use(createPinia())
|
||||
app.use(router)
|
||||
app.use(CoreuiVue)
|
||||
|
||||
// Install plugins
|
||||
app.use(createPinia()) // State management
|
||||
app.use(router) // Router for SPA navigation
|
||||
app.use(CoreuiVue) // CoreUI component library
|
||||
|
||||
// Provide icons globally
|
||||
app.provide('icons', icons)
|
||||
|
||||
// Register global components
|
||||
app.component('CIcon', CIcon)
|
||||
app.component('DocsComponents', DocsComponents)
|
||||
app.component('DocsExample', DocsExample)
|
||||
app.component('DocsIcons', DocsIcons)
|
||||
|
||||
// Mount application to DOM
|
||||
app.mount('#app')
|
||||
|
||||
@@ -1,8 +1,36 @@
|
||||
/**
|
||||
* router/index.js - Vue Router Configuration
|
||||
*
|
||||
* This file configures the application routing using Vue Router 5.
|
||||
* It defines all routes and navigation structure for the SPA.
|
||||
*
|
||||
* Routing Features:
|
||||
* - Hash-based routing (createWebHashHistory) for static hosting compatibility
|
||||
* - Lazy loading for all route components (code splitting)
|
||||
* - Nested routes for layout-based navigation
|
||||
* - Automatic scroll to top on navigation
|
||||
*
|
||||
* Route Structure:
|
||||
* - Protected routes: Wrapped in DefaultLayout with sidebar and header
|
||||
* - Public routes: Login, Register, 404, 500 pages without layout
|
||||
*
|
||||
* Adding New Routes:
|
||||
* 1. Import component (use dynamic import for code splitting)
|
||||
* 2. Add route object to appropriate section
|
||||
* 3. Update _nav.js for sidebar navigation (if needed)
|
||||
*
|
||||
* @see https://router.vuejs.org/
|
||||
*/
|
||||
|
||||
import { h, resolveComponent } from 'vue'
|
||||
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||
|
||||
import DefaultLayout from '@/layouts/DefaultLayout'
|
||||
|
||||
/**
|
||||
* Application routes configuration
|
||||
* @type {Array<Object>}
|
||||
*/
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
@@ -21,6 +49,43 @@ const routes = [
|
||||
/* webpackChunkName: "dashboard" */ '@/views/dashboard/Dashboard.vue'
|
||||
),
|
||||
},
|
||||
{
|
||||
path: '/tickets',
|
||||
name: 'Tickets',
|
||||
component: {
|
||||
render() {
|
||||
return h(resolveComponent('router-view'))
|
||||
},
|
||||
},
|
||||
redirect: '/tickets/ticket',
|
||||
children: [
|
||||
{
|
||||
path: '/tickets/ticket',
|
||||
name: 'Ticket',
|
||||
component: () => import('@/views/tickets/Ticket.vue'),
|
||||
},
|
||||
{
|
||||
path: '/tickets/ticket-type',
|
||||
name: 'Ticket Type',
|
||||
component: () => import('@/views/tickets/TicketType.vue'),
|
||||
},
|
||||
{
|
||||
path: '/tickets/ticket-materials',
|
||||
name: 'Ticket Materials',
|
||||
component: () => import('@/views/tickets/TicketMaterials.vue'),
|
||||
},
|
||||
{
|
||||
path: '/tickets/approved',
|
||||
name: 'Approved',
|
||||
component: () => import('@/views/tickets/Approved.vue'),
|
||||
},
|
||||
{
|
||||
path: '/tickets/rejected',
|
||||
name: 'Rejected',
|
||||
component: () => import('@/views/tickets/Rejected.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/theme',
|
||||
name: 'Theme',
|
||||
@@ -66,6 +131,11 @@ const routes = [
|
||||
name: 'Carousels',
|
||||
component: () => import('@/views/base/Carousels.vue'),
|
||||
},
|
||||
{
|
||||
path: '/base/chips',
|
||||
name: 'Chips',
|
||||
component: () => import('@/views/base/Chips.vue'),
|
||||
},
|
||||
{
|
||||
path: '/base/collapses',
|
||||
name: 'Collapses',
|
||||
@@ -175,6 +245,11 @@ const routes = [
|
||||
name: 'Checks & Radios',
|
||||
component: () => import('@/views/forms/ChecksRadios.vue'),
|
||||
},
|
||||
{
|
||||
path: '/forms/chip-input',
|
||||
name: 'Chip Input',
|
||||
component: () => import('@/views/forms/ChipInput.vue'),
|
||||
},
|
||||
{
|
||||
path: '/forms/range',
|
||||
name: 'Range',
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import axios from 'axios'
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: 'https://api.radiq.my.id/api',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: 'Bearer uZ1vM4UON3CsDV9niGD1gLS4sHpCxT9nzadkITmc6caf2ea2',
|
||||
},
|
||||
})
|
||||
|
||||
api.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
return Promise.reject(error)
|
||||
},
|
||||
)
|
||||
|
||||
export default api
|
||||
@@ -0,0 +1,105 @@
|
||||
import { ref } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useTicketStore = defineStore('ticket', () => {
|
||||
const tickets = ref([
|
||||
{
|
||||
ticket_no: 'TKT-001',
|
||||
tenant_id: 'tenant-01',
|
||||
customer_id: 'CUST-1001',
|
||||
deskripsi: 'Modem Merah',
|
||||
kode_tiket: 'GGN',
|
||||
status: 'Open',
|
||||
prioritas: 'Low',
|
||||
sla_minutes: '480',
|
||||
ditugaskan_untuk: 'Joko',
|
||||
created_by: 'admin@company.com',
|
||||
approved_by: 'manager@company.com',
|
||||
approved_at: '2026-06-20 09:00',
|
||||
rejected_by: '-',
|
||||
rejected_at: '-',
|
||||
rejection_reason: '-',
|
||||
assigned_at: '2026-06-20 09:30',
|
||||
resolved_at: '-',
|
||||
closed_at: '-',
|
||||
created_at: '2026-06-20 08:00',
|
||||
updated_at: '2026-06-20 09:30',
|
||||
},
|
||||
{
|
||||
ticket_no: 'TKT-002',
|
||||
tenant_id: 'tenant-01',
|
||||
customer_id: 'CUST-1002',
|
||||
deskripsi: 'Ganti Perangkat',
|
||||
kode_tiket: 'ONU',
|
||||
status: 'Open',
|
||||
prioritas: 'Medium',
|
||||
sla_minutes: '240',
|
||||
ditugaskan_untuk: 'Bowo',
|
||||
created_by: 'user@company.com',
|
||||
approved_by: '-',
|
||||
approved_at: '-',
|
||||
rejected_by: 'supervisor@company.com',
|
||||
rejected_at: '2026-06-22 10:00',
|
||||
rejection_reason: 'Duplicate request',
|
||||
assigned_at: '-',
|
||||
resolved_at: '-',
|
||||
closed_at: '-',
|
||||
created_at: '2026-06-22 09:00',
|
||||
updated_at: '2026-06-22 10:00',
|
||||
},
|
||||
{
|
||||
ticket_no: 'TKT-003',
|
||||
tenant_id: 'tenant-02',
|
||||
customer_id: 'CUST-1003',
|
||||
deskripsi: 'Perawatan Perangkat',
|
||||
kode_tiket: 'MNT',
|
||||
status: 'Closed',
|
||||
prioritas: 'High',
|
||||
sla_minutes: '720',
|
||||
ditugaskan_untuk: 'Bahlil',
|
||||
created_by: 'client@domain.com',
|
||||
approved_by: 'manager@company.com',
|
||||
approved_at: '2026-06-23 08:00',
|
||||
rejected_by: '-',
|
||||
rejected_at: '-',
|
||||
rejection_reason: '-',
|
||||
assigned_at: '2026-06-23 08:30',
|
||||
resolved_at: '2026-06-24 15:00',
|
||||
closed_at: '2026-06-24 16:00',
|
||||
created_at: '2026-06-23 07:00',
|
||||
updated_at: '2026-06-24 16:00',
|
||||
},
|
||||
])
|
||||
|
||||
const approvedTickets = ref([])
|
||||
const rejectedTickets = ref([])
|
||||
|
||||
function addTicket(ticket) {
|
||||
tickets.value.push({ ...ticket })
|
||||
}
|
||||
|
||||
function updateTicket(index, ticket) {
|
||||
tickets.value[index] = { ...ticket }
|
||||
}
|
||||
|
||||
function deleteTicket(index) {
|
||||
tickets.value.splice(index, 1)
|
||||
}
|
||||
|
||||
function approveTicket(index, approver) {
|
||||
const now = new Date().toISOString().slice(0, 16).replace('T', ' ')
|
||||
tickets.value[index].approved_by = approver
|
||||
tickets.value[index].approved_at = now
|
||||
tickets.value[index].status = 'Approved'
|
||||
}
|
||||
|
||||
function rejectTicket(index, rejector, reason) {
|
||||
const now = new Date().toISOString().slice(0, 16).replace('T', ' ')
|
||||
tickets.value[index].rejected_by = rejector
|
||||
tickets.value[index].rejected_at = now
|
||||
tickets.value[index].rejection_reason = reason
|
||||
tickets.value[index].status = 'Rejected'
|
||||
}
|
||||
|
||||
return { tickets, addTicket, updateTicket, deleteTicket, approveTicket, rejectTicket }
|
||||
})
|
||||
@@ -0,0 +1,94 @@
|
||||
import { ref } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
import api from '@/services/api.js'
|
||||
|
||||
export const useTicketTypeStore = defineStore('ticketType', () => {
|
||||
const ticketTypes = ref([])
|
||||
const loading = ref(false)
|
||||
const error = ref(null)
|
||||
|
||||
async function fetchTicketTypes(page = 1) {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const response = await api.get(`/ticket-types?page=${page}`)
|
||||
ticketTypes.value = response.data.data.data
|
||||
return response.data.data
|
||||
} catch (err) {
|
||||
error.value = err.response?.data?.message || err.message || 'Gagal memuat data'
|
||||
throw err
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchTicketType(id) {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const response = await api.get(`/ticket-types/${id}`)
|
||||
return response.data.data
|
||||
} catch (err) {
|
||||
error.value = err.response?.data?.message || err.message || 'Gagal memuat data'
|
||||
throw err
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function addTicketType(ticketType) {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const response = await api.post('/ticket-types', ticketType)
|
||||
return response.data
|
||||
} catch (err) {
|
||||
error.value = err.response?.data?.message || err.message || 'Gagal menambah data'
|
||||
throw err
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function updateTicketType(id, ticketType) {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const response = await api.post(`/ticket-types/${id}`, {
|
||||
...ticketType,
|
||||
_method: 'PUT',
|
||||
})
|
||||
return response.data
|
||||
} catch (err) {
|
||||
error.value = err.response?.data?.message || err.message || 'Gagal mengupdate data'
|
||||
throw err
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteTicketType(id) {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const response = await api.delete(`/ticket-types/${id}`)
|
||||
return response.data
|
||||
} catch (err) {
|
||||
error.value = err.response?.data?.message || err.message || 'Gagal menghapus data'
|
||||
throw err
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
ticketTypes,
|
||||
loading,
|
||||
error,
|
||||
fetchTicketTypes,
|
||||
fetchTicketType,
|
||||
addTicketType,
|
||||
updateTicketType,
|
||||
deleteTicketType,
|
||||
}
|
||||
})
|
||||
@@ -9,7 +9,7 @@
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
background-color: var(--#{$prefix}tertiary-bg);
|
||||
background-color: rgba(var(--#{$prefix}tertiary-bg-rgb), .75);
|
||||
}
|
||||
|
||||
& + p {
|
||||
@@ -106,9 +106,3 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include color-mode(dark) {
|
||||
.example .tab-content {
|
||||
background-color: var(--#{$prefix}secondary-bg);
|
||||
}
|
||||
}
|
||||
|
||||
+69
-5
@@ -1,8 +1,8 @@
|
||||
@use "@coreui/coreui/scss/coreui" as * with (
|
||||
@use '@coreui/coreui/scss/coreui' as * with (
|
||||
$enable-deprecation-messages: false
|
||||
);
|
||||
@use "@coreui/chartjs/scss/coreui-chartjs";
|
||||
@use "vendors/simplebar";
|
||||
@use '@coreui/chartjs/scss/coreui-chartjs';
|
||||
@use 'vendors/simplebar';
|
||||
|
||||
body {
|
||||
background-color: var(--cui-tertiary-bg);
|
||||
@@ -12,7 +12,7 @@ body {
|
||||
width: 100%;
|
||||
padding-inline: var(--cui-sidebar-occupy-start, 0) var(--cui-sidebar-occupy-end, 0);
|
||||
will-change: auto;
|
||||
@include transition(padding .15s);
|
||||
@include transition(padding 0.15s);
|
||||
}
|
||||
|
||||
.header > .container-fluid,
|
||||
@@ -48,6 +48,37 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-nav .nav-link {
|
||||
&.active {
|
||||
color: #ff8c00;
|
||||
background: rgba(255, 140, 0, 0.1);
|
||||
|
||||
.nav-icon {
|
||||
color: #ff8c00;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: #ff8c00;
|
||||
background: rgba(255, 140, 0, 0.08);
|
||||
|
||||
.nav-icon {
|
||||
color: #ff8c00;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.header-nav .nav-link {
|
||||
&.active {
|
||||
color: #ff8c00;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: #ff8c00;
|
||||
}
|
||||
}
|
||||
|
||||
.header > .container-fluid + .container-fluid {
|
||||
min-height: 3rem;
|
||||
}
|
||||
@@ -57,11 +88,44 @@ body {
|
||||
}
|
||||
|
||||
@include color-mode(dark) {
|
||||
--cui-body-bg: #0d0e12;
|
||||
--cui-body-bg-rgb: 13, 14, 18;
|
||||
--cui-secondary-bg: #121318;
|
||||
--cui-secondary-bg-rgb: 18, 19, 24;
|
||||
--cui-tertiary-bg: #16171d;
|
||||
--cui-tertiary-bg-rgb: 22, 23, 29;
|
||||
--cui-sidebar-bg: #0d0e12;
|
||||
--cui-sidebar-nav-link-active-bg: rgba(242, 133, 0, 0.1);
|
||||
--cui-card-bg: #16171d;
|
||||
--cui-card-cap-bg: #16171d;
|
||||
--cui-card-border-color: #22242d;
|
||||
--cui-border-color: #22242d;
|
||||
--cui-border-color-translucent: rgba(34, 36, 45, 0.8);
|
||||
--cui-dark-bg-subtle: #0d0e12;
|
||||
--cui-primary: #f28500;
|
||||
--cui-primary-rgb: 242, 133, 0;
|
||||
--cui-link-color: #f28500;
|
||||
--cui-link-color-rgb: 242, 133, 0;
|
||||
--cui-table-bg: #16171d;
|
||||
--cui-table-color: rgba(255, 255, 255, 0.87);
|
||||
--cui-input-bg: #16171d;
|
||||
--cui-input-color: rgba(255, 255, 255, 0.87);
|
||||
--cui-input-border-color: #22242d;
|
||||
--cui-list-group-bg: #16171d;
|
||||
--cui-list-group-border-color: #22242d;
|
||||
|
||||
body {
|
||||
background-color: var(--cui-dark-bg-subtle);
|
||||
background-color: var(--cui-body-bg);
|
||||
}
|
||||
|
||||
.footer {
|
||||
--cui-footer-bg: var(--cui-body-bg);
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-dark {
|
||||
--cui-body-bg: #0d0e12;
|
||||
--cui-secondary-bg: #121318;
|
||||
--cui-tertiary-bg: #16171d;
|
||||
--cui-border-color: #22242d;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
<template>
|
||||
<CRow>
|
||||
<CCol :xs="12">
|
||||
<DocsComponents href="components/chip/" />
|
||||
<CCard class="mb-4">
|
||||
<CCardHeader>
|
||||
<strong>Vue Chip</strong> <small>Basic and outline</small>
|
||||
</CCardHeader>
|
||||
<CCardBody>
|
||||
<p class="text-body-secondary small">
|
||||
Chips are compact UI elements for labels, filters, and quick selections. Use the
|
||||
<code>CChip</code> component with optional <code>variant</code> prop.
|
||||
</p>
|
||||
<DocsExample href="components/chip/#basic-chips">
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<CChip>Basic chip</CChip>
|
||||
<CChip>Frontend</CChip>
|
||||
<CChip>Design system</CChip>
|
||||
<CChip>Documentation</CChip>
|
||||
</div>
|
||||
</DocsExample>
|
||||
<p class="text-body-secondary small">
|
||||
Add <code>variant="outline"</code> to create a lighter, bordered appearance.
|
||||
</p>
|
||||
<DocsExample href="components/chip/#outline-chips">
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<CChip variant="outline">Outline chip</CChip>
|
||||
<CChip variant="outline">Product</CChip>
|
||||
<CChip variant="outline">Marketing</CChip>
|
||||
<CChip variant="outline">Analytics</CChip>
|
||||
</div>
|
||||
</DocsExample>
|
||||
</CCardBody>
|
||||
</CCard>
|
||||
|
||||
<CCard class="mb-4">
|
||||
<CCardHeader>
|
||||
<strong>Vue Chip</strong> <small>With icons and avatars</small>
|
||||
</CCardHeader>
|
||||
<CCardBody>
|
||||
<p class="text-body-secondary small">
|
||||
Chips can include icons, user avatars, or avatar components to make tags easier to scan.
|
||||
</p>
|
||||
<DocsExample href="components/chip/#chips-with-icons">
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<CChip>
|
||||
<span class="chip-icon">
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 16 16"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M8 1a3 3 0 1 1 0 6 3 3 0 0 1 0-6Zm-5 12s0-3 5-3 5 3 5 3v1H3v-1Z" />
|
||||
</svg>
|
||||
</span>
|
||||
Team member
|
||||
</CChip>
|
||||
<CChip>
|
||||
<span class="chip-icon">
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 16 16"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M2 2h12v2H2V2Zm0 5h8v2H2V7Zm0 5h12v2H2v-2Z" />
|
||||
</svg>
|
||||
</span>
|
||||
Backlog item
|
||||
</CChip>
|
||||
</div>
|
||||
</DocsExample>
|
||||
<DocsExample href="components/chip/#chips-with-avatars">
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<CChip>
|
||||
<img class="chip-img" :src="avatar1" width="16" height="16" alt="" /> Olivia Martin
|
||||
</CChip>
|
||||
<CChip>
|
||||
<img class="chip-img" :src="avatar4" width="16" height="16" alt="" /> Ethan Carter
|
||||
</CChip>
|
||||
<CChip>
|
||||
<span class="avatar avatar-sm bg-primary text-white">A</span> Account manager
|
||||
</CChip>
|
||||
<CChip>
|
||||
<span class="avatar avatar-sm bg-success text-white">Q</span> QA owner
|
||||
</CChip>
|
||||
</div>
|
||||
</DocsExample>
|
||||
</CCardBody>
|
||||
</CCard>
|
||||
</CCol>
|
||||
|
||||
<CCol :xs="12">
|
||||
<CCard class="mb-4">
|
||||
<CCardHeader>
|
||||
<strong>Vue Chip</strong> <small>Variants and sizes</small>
|
||||
</CCardHeader>
|
||||
<CCardBody>
|
||||
<p class="text-body-secondary small">
|
||||
Use the <code>color</code> prop for contextual variants and the <code>size</code> prop
|
||||
for different sizes.
|
||||
</p>
|
||||
<DocsExample href="components/chip/#variants">
|
||||
<div class="d-flex flex-wrap gap-2 mb-3">
|
||||
<CChip color="primary" clickable>Product</CChip>
|
||||
<CChip color="primary" active>Active product</CChip>
|
||||
<CChip color="success" clickable>Published</CChip>
|
||||
<CChip color="success" active>Live</CChip>
|
||||
<CChip color="warning" clickable>Review</CChip>
|
||||
<CChip color="danger" active>Blocked</CChip>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<CChip size="sm">Small chip</CChip>
|
||||
<CChip>Default chip</CChip>
|
||||
<CChip size="lg">Large chip</CChip>
|
||||
<CChip variant="outline" color="primary" clickable>Outline primary</CChip>
|
||||
</div>
|
||||
</DocsExample>
|
||||
</CCardBody>
|
||||
</CCard>
|
||||
|
||||
<CCard class="mb-4">
|
||||
<CCardHeader>
|
||||
<strong>Vue Chip</strong> <small>Interactive examples</small>
|
||||
</CCardHeader>
|
||||
<CCardBody>
|
||||
<p class="text-body-secondary small">
|
||||
Chips support selection, removal, and disabled states using props like
|
||||
<code>selectable</code>, <code>removable</code>, and <code>disabled</code>.
|
||||
</p>
|
||||
<DocsExample href="components/chip/#interactive">
|
||||
<div class="d-flex flex-wrap gap-2 mb-3">
|
||||
<CChip selectable>Selectable</CChip>
|
||||
<CChip selectable selected>Selected</CChip>
|
||||
<CChip removable>Removable</CChip>
|
||||
<CChip disabled>Disabled</CChip>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<CChip size="lg" removable>Team Alpha</CChip>
|
||||
<CChip variant="outline" color="info" selectable>Filter: Priority</CChip>
|
||||
<CChip variant="outline" color="success" selectable selected>Filter: Ready</CChip>
|
||||
</div>
|
||||
</DocsExample>
|
||||
</CCardBody>
|
||||
</CCard>
|
||||
</CCol>
|
||||
</CRow>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import avatar1 from '@/assets/images/avatars/1.jpg'
|
||||
import avatar4 from '@/assets/images/avatars/4.jpg'
|
||||
|
||||
export default {
|
||||
name: 'Chips',
|
||||
setup() {
|
||||
return {
|
||||
avatar1,
|
||||
avatar4,
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,190 @@
|
||||
<template>
|
||||
<CRow>
|
||||
<CCol :xs="12">
|
||||
<DocsComponents href="forms/chip-input/" />
|
||||
<CCard class="mb-4">
|
||||
<CCardHeader>
|
||||
<strong>Vue Chip Input</strong> <small>Basic example</small>
|
||||
</CCardHeader>
|
||||
<CCardBody>
|
||||
<p class="text-body-secondary small">
|
||||
Chip input lets users enter multiple values in one field. Use the
|
||||
<code>CChipInput</code> component with props like <code>placeholder</code> and
|
||||
<code>defaultValue</code>.
|
||||
</p>
|
||||
<DocsExample href="forms/chip-input/#basic-example">
|
||||
<CChipInput
|
||||
label="Skills:"
|
||||
name="skills"
|
||||
placeholder="Add a skill..."
|
||||
:defaultValue="['JavaScript', 'TypeScript', 'Accessibility']"
|
||||
/>
|
||||
</DocsExample>
|
||||
</CCardBody>
|
||||
</CCard>
|
||||
|
||||
<CCard class="mb-4">
|
||||
<CCardHeader>
|
||||
<strong>Vue Chip Input</strong> <small>Variants</small>
|
||||
</CCardHeader>
|
||||
<CCardBody>
|
||||
<p class="text-body-secondary small">
|
||||
Use the <code>chipClassName</code> prop to apply contextual chip styles, which is handy
|
||||
for labels, issue types, or priorities.
|
||||
</p>
|
||||
<DocsExample href="forms/chip-input/#variants">
|
||||
<CChipInput
|
||||
name="issues"
|
||||
placeholder="Add label..."
|
||||
:defaultValue="['Feature', 'Approved', 'Needs review', 'Blocking']"
|
||||
:chipClassName="getChipClassName"
|
||||
/>
|
||||
</DocsExample>
|
||||
</CCardBody>
|
||||
</CCard>
|
||||
|
||||
<CCard class="mb-4">
|
||||
<CCardHeader>
|
||||
<strong>Vue Chip Input</strong> <small>Sizes</small>
|
||||
</CCardHeader>
|
||||
<CCardBody>
|
||||
<p class="text-body-secondary small">
|
||||
Use the <code>size</code> prop with <code>"sm"</code> or <code>"lg"</code> to align the
|
||||
field with surrounding form controls.
|
||||
</p>
|
||||
<DocsExample href="forms/chip-input/#sizes">
|
||||
<CChipInput
|
||||
label="Small"
|
||||
size="sm"
|
||||
placeholder="Add small tag..."
|
||||
:defaultValue="['HTML']"
|
||||
class="mb-3"
|
||||
/>
|
||||
<CChipInput
|
||||
label="Default"
|
||||
placeholder="Add default tag..."
|
||||
:defaultValue="['JavaScript']"
|
||||
class="mb-3"
|
||||
/>
|
||||
<CChipInput
|
||||
label="Large"
|
||||
size="lg"
|
||||
placeholder="Add large tag..."
|
||||
:defaultValue="['TypeScript']"
|
||||
/>
|
||||
</DocsExample>
|
||||
</CCardBody>
|
||||
</CCard>
|
||||
</CCol>
|
||||
|
||||
<CCol :xs="12">
|
||||
<CCard class="mb-4">
|
||||
<CCardHeader>
|
||||
<strong>Vue Chip Input</strong> <small>Empty state and label</small>
|
||||
</CCardHeader>
|
||||
<CCardBody>
|
||||
<p class="text-body-secondary small">
|
||||
You can start with an empty field or use a separate <code>CFormLabel</code> for clearer
|
||||
form structure and accessibility.
|
||||
</p>
|
||||
<DocsExample href="forms/chip-input/#empty-state">
|
||||
<CChipInput name="tags" placeholder="Start typing tags..." class="mb-3" />
|
||||
<div class="mb-0">
|
||||
<CFormLabel for="techStackInput">Tech stack</CFormLabel>
|
||||
<CChipInput
|
||||
id="techStackInput"
|
||||
name="techStack"
|
||||
placeholder="Add package..."
|
||||
:defaultValue="['Vue', 'Vite']"
|
||||
/>
|
||||
<CFormText>Press Enter or comma to add a value.</CFormText>
|
||||
</div>
|
||||
</DocsExample>
|
||||
</CCardBody>
|
||||
</CCard>
|
||||
|
||||
<CCard class="mb-4">
|
||||
<CCardHeader>
|
||||
<strong>Vue Chip Input</strong> <small>Disabled and readonly</small>
|
||||
</CCardHeader>
|
||||
<CCardBody>
|
||||
<p class="text-body-secondary small">
|
||||
Use the <code>disabled</code> prop to block interaction entirely, or
|
||||
<code>readOnly</code> to keep values visible while preventing changes.
|
||||
</p>
|
||||
<DocsExample href="forms/chip-input/#disabled">
|
||||
<CChipInput
|
||||
disabled
|
||||
:removable="false"
|
||||
placeholder="Input disabled"
|
||||
:defaultValue="['Read only', 'Locked']"
|
||||
class="mb-3"
|
||||
/>
|
||||
<CChipInput
|
||||
readOnly
|
||||
placeholder="Read-only values"
|
||||
:defaultValue="['JavaScript', 'TypeScript']"
|
||||
/>
|
||||
</DocsExample>
|
||||
</CCardBody>
|
||||
</CCard>
|
||||
|
||||
<CCard class="mb-4">
|
||||
<CCardHeader>
|
||||
<strong>Vue Chip Input</strong> <small>Form-friendly examples</small>
|
||||
</CCardHeader>
|
||||
<CCardBody>
|
||||
<p class="text-body-secondary small">
|
||||
The component integrates well with ordinary forms, including helper text and predefined
|
||||
values.
|
||||
</p>
|
||||
<DocsExample href="forms/chip-input/#with-label">
|
||||
<form class="row g-3">
|
||||
<div class="col-12">
|
||||
<CFormLabel for="recipientsInput">Recipients</CFormLabel>
|
||||
<CChipInput
|
||||
id="recipientsInput"
|
||||
name="recipients"
|
||||
placeholder="Add email..."
|
||||
:defaultValue="['olivia@coreui.io', 'ethan@coreui.io']"
|
||||
/>
|
||||
<CFormText>Add one address at a time and press Enter.</CFormText>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<CFormLabel for="categoriesInput">Categories</CFormLabel>
|
||||
<CChipInput
|
||||
id="categoriesInput"
|
||||
name="categories"
|
||||
placeholder="Add category..."
|
||||
:defaultValue="['Product', 'Design', 'Docs']"
|
||||
chipClassName="chip-outline"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</DocsExample>
|
||||
</CCardBody>
|
||||
</CCard>
|
||||
</CCol>
|
||||
</CRow>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'ChipInput',
|
||||
setup() {
|
||||
const getChipClassName = (value) => {
|
||||
const colorMap = {
|
||||
Feature: 'chip-primary',
|
||||
Approved: 'chip-success',
|
||||
'Needs review': 'chip-warning',
|
||||
Blocking: 'chip-danger',
|
||||
}
|
||||
return colorMap[value] || ''
|
||||
}
|
||||
|
||||
return {
|
||||
getChipClassName,
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,55 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { useTicketStore } from '@/stores/ticket.js'
|
||||
|
||||
const store = useTicketStore()
|
||||
|
||||
const approvedTickets = computed(() =>
|
||||
store.tickets.filter((t) => t.approved_by && t.approved_by !== '-')
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CRow>
|
||||
<CCol :xs="12">
|
||||
<CCard class="mb-4">
|
||||
<CCardHeader>
|
||||
<strong>Approved Tickets</strong>
|
||||
</CCardHeader>
|
||||
<CCardBody>
|
||||
<div class="table-responsive">
|
||||
<CTable striped hover small>
|
||||
<CTableHead>
|
||||
<CTableRow>
|
||||
<CTableHeaderCell scope="col">No</CTableHeaderCell>
|
||||
<CTableHeaderCell scope="col">Ticket No</CTableHeaderCell>
|
||||
<CTableHeaderCell scope="col">Kode Tiket</CTableHeaderCell>
|
||||
<CTableHeaderCell scope="col">Deskripsi</CTableHeaderCell>
|
||||
<CTableHeaderCell scope="col">Approved By</CTableHeaderCell>
|
||||
<CTableHeaderCell scope="col">Approved At</CTableHeaderCell>
|
||||
<CTableHeaderCell scope="col">Status</CTableHeaderCell>
|
||||
</CTableRow>
|
||||
</CTableHead>
|
||||
<CTableBody>
|
||||
<CTableRow v-for="(item, index) in approvedTickets" :key="item.ticket_no">
|
||||
<CTableHeaderCell scope="row">{{ index + 1 }}</CTableHeaderCell>
|
||||
<CTableDataCell>{{ item.ticket_no }}</CTableDataCell>
|
||||
<CTableDataCell>{{ item.kode_tiket }}</CTableDataCell>
|
||||
<CTableDataCell>{{ item.deskripsi }}</CTableDataCell>
|
||||
<CTableDataCell>{{ item.approved_by }}</CTableDataCell>
|
||||
<CTableDataCell>{{ item.approved_at }}</CTableDataCell>
|
||||
<CTableDataCell>
|
||||
<CBadge color="success">Approved</CBadge>
|
||||
</CTableDataCell>
|
||||
</CTableRow>
|
||||
<CTableRow v-if="approvedTickets.length === 0">
|
||||
<CTableDataCell colspan="7" class="text-center">Tidak ada data approved tickets</CTableDataCell>
|
||||
</CTableRow>
|
||||
</CTableBody>
|
||||
</CTable>
|
||||
</div>
|
||||
</CCardBody>
|
||||
</CCard>
|
||||
</CCol>
|
||||
</CRow>
|
||||
</template>
|
||||
@@ -0,0 +1,53 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { useTicketStore } from '@/stores/ticket.js'
|
||||
|
||||
const store = useTicketStore()
|
||||
|
||||
const rejectedTickets = computed(() =>
|
||||
store.tickets.filter((t) => t.rejected_by && t.rejected_by !== '-')
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CRow>
|
||||
<CCol :xs="12">
|
||||
<CCard class="mb-4">
|
||||
<CCardHeader>
|
||||
<strong>Rejected Tickets</strong>
|
||||
</CCardHeader>
|
||||
<CCardBody>
|
||||
<div class="table-responsive">
|
||||
<CTable striped hover small>
|
||||
<CTableHead>
|
||||
<CTableRow>
|
||||
<CTableHeaderCell scope="col">No</CTableHeaderCell>
|
||||
<CTableHeaderCell scope="col">Ticket No</CTableHeaderCell>
|
||||
<CTableHeaderCell scope="col">Kode Tiket</CTableHeaderCell>
|
||||
<CTableHeaderCell scope="col">Deskripsi</CTableHeaderCell>
|
||||
<CTableHeaderCell scope="col">Rejected By</CTableHeaderCell>
|
||||
<CTableHeaderCell scope="col">Rejected At</CTableHeaderCell>
|
||||
<CTableHeaderCell scope="col">Alasan</CTableHeaderCell>
|
||||
</CTableRow>
|
||||
</CTableHead>
|
||||
<CTableBody>
|
||||
<CTableRow v-for="(item, index) in rejectedTickets" :key="item.ticket_no">
|
||||
<CTableHeaderCell scope="row">{{ index + 1 }}</CTableHeaderCell>
|
||||
<CTableDataCell>{{ item.ticket_no }}</CTableDataCell>
|
||||
<CTableDataCell>{{ item.kode_tiket }}</CTableDataCell>
|
||||
<CTableDataCell>{{ item.deskripsi }}</CTableDataCell>
|
||||
<CTableDataCell>{{ item.rejected_by }}</CTableDataCell>
|
||||
<CTableDataCell>{{ item.rejected_at }}</CTableDataCell>
|
||||
<CTableDataCell>{{ item.rejection_reason }}</CTableDataCell>
|
||||
</CTableRow>
|
||||
<CTableRow v-if="rejectedTickets.length === 0">
|
||||
<CTableDataCell colspan="7" class="text-center">Tidak ada data rejected tickets</CTableDataCell>
|
||||
</CTableRow>
|
||||
</CTableBody>
|
||||
</CTable>
|
||||
</div>
|
||||
</CCardBody>
|
||||
</CCard>
|
||||
</CCol>
|
||||
</CRow>
|
||||
</template>
|
||||
@@ -0,0 +1,479 @@
|
||||
<script setup>
|
||||
import { ref, reactive } from 'vue'
|
||||
import { useTicketStore } from '@/stores/ticket.js'
|
||||
|
||||
const store = useTicketStore()
|
||||
|
||||
const ticketModal = ref(false)
|
||||
const detailModal = ref(false)
|
||||
const approveModal = ref(false)
|
||||
const rejectModal = ref(false)
|
||||
const editingIndex = ref(-1)
|
||||
const isEdit = ref(false)
|
||||
const form = reactive({
|
||||
ticket_no: '',
|
||||
tenant_id: '',
|
||||
customer_id: '',
|
||||
deskripsi: '',
|
||||
kode_tiket: '',
|
||||
status: 'Open',
|
||||
prioritas: 'Low',
|
||||
sla_minutes: '',
|
||||
ditugaskan_untuk: '',
|
||||
created_by: '',
|
||||
approved_by: '',
|
||||
approved_at: '',
|
||||
rejected_by: '',
|
||||
rejected_at: '',
|
||||
rejection_reason: '',
|
||||
assigned_at: '',
|
||||
resolved_at: '',
|
||||
closed_at: '',
|
||||
created_at: '',
|
||||
updated_at: '',
|
||||
})
|
||||
|
||||
const emptyForm = () => ({
|
||||
ticket_no: '',
|
||||
tenant_id: '',
|
||||
customer_id: '',
|
||||
deskripsi: '',
|
||||
kode_tiket: '',
|
||||
status: 'Open',
|
||||
prioritas: 'Low',
|
||||
sla_minutes: '',
|
||||
ditugaskan_untuk: '',
|
||||
created_by: '',
|
||||
approved_by: '',
|
||||
approved_at: '',
|
||||
rejected_by: '',
|
||||
rejected_at: '',
|
||||
rejection_reason: '',
|
||||
assigned_at: '',
|
||||
resolved_at: '',
|
||||
closed_at: '',
|
||||
created_at: '',
|
||||
updated_at: '',
|
||||
})
|
||||
|
||||
const detailItem = ref(null)
|
||||
const approveIndex = ref(-1)
|
||||
const rejectIndex = ref(-1)
|
||||
const approverName = ref('')
|
||||
const rejectorName = ref('')
|
||||
const rejectReason = ref('')
|
||||
|
||||
function openAddModal() {
|
||||
isEdit.value = false
|
||||
editingIndex.value = -1
|
||||
Object.assign(form, emptyForm())
|
||||
ticketModal.value = true
|
||||
}
|
||||
|
||||
function openEditModal(index) {
|
||||
isEdit.value = true
|
||||
editingIndex.value = index
|
||||
Object.assign(form, store.tickets[index])
|
||||
ticketModal.value = true
|
||||
}
|
||||
|
||||
function openDetail(item) {
|
||||
detailItem.value = item
|
||||
detailModal.value = true
|
||||
}
|
||||
|
||||
function saveTicket() {
|
||||
if (isEdit.value && editingIndex.value >= 0) {
|
||||
store.updateTicket(editingIndex.value, form)
|
||||
} else {
|
||||
store.addTicket(form)
|
||||
}
|
||||
ticketModal.value = false
|
||||
}
|
||||
|
||||
function deleteTicket(index) {
|
||||
store.deleteTicket(index)
|
||||
}
|
||||
|
||||
function openApproveModal(index) {
|
||||
approveIndex.value = index
|
||||
approverName.value = ''
|
||||
approveModal.value = true
|
||||
}
|
||||
|
||||
function confirmApprove() {
|
||||
if (approverName.value.trim()) {
|
||||
store.approveTicket(approveIndex.value, approverName.value.trim())
|
||||
approveModal.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openRejectModal(index) {
|
||||
rejectIndex.value = index
|
||||
rejectorName.value = ''
|
||||
rejectReason.value = ''
|
||||
rejectModal.value = true
|
||||
}
|
||||
|
||||
function confirmReject() {
|
||||
if (rejectorName.value.trim() && rejectReason.value.trim()) {
|
||||
store.rejectTicket(rejectIndex.value, rejectorName.value.trim(), rejectReason.value.trim())
|
||||
rejectModal.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function isPending(item) {
|
||||
return (
|
||||
(item.approved_by === '-' || !item.approved_by) &&
|
||||
(item.rejected_by === '-' || !item.rejected_by)
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CRow>
|
||||
<CCol :xs="12">
|
||||
<CCard class="mb-4">
|
||||
<CCardHeader>
|
||||
<strong>Ticket</strong>
|
||||
<CButton color="primary" size="sm" class="float-end" @click="openAddModal">
|
||||
+ Tambah Ticket
|
||||
</CButton>
|
||||
</CCardHeader>
|
||||
<CCardBody>
|
||||
<div class="table-responsive">
|
||||
<CTable striped hover small>
|
||||
<CTableHead>
|
||||
<CTableRow>
|
||||
<CTableHeaderCell scope="col">No</CTableHeaderCell>
|
||||
<CTableHeaderCell scope="col" class="text-center">Ticket No</CTableHeaderCell>
|
||||
<CTableHeaderCell scope="col" class="text-center w-25"
|
||||
>Kode Tiket</CTableHeaderCell
|
||||
>
|
||||
<CTableHeaderCell scope="col">Deskripsi</CTableHeaderCell>
|
||||
<CTableHeaderCell scope="col">Status</CTableHeaderCell>
|
||||
<CTableHeaderCell scope="col">Prioritas</CTableHeaderCell>
|
||||
<CTableHeaderCell scope="col" class="text-center"
|
||||
>Ditugaskan Untuk</CTableHeaderCell
|
||||
>
|
||||
<CTableHeaderCell scope="col" class="text-center">Aksi</CTableHeaderCell>
|
||||
</CTableRow>
|
||||
</CTableHead>
|
||||
<CTableBody>
|
||||
<CTableRow v-for="(item, index) in store.tickets" :key="item.ticket_no">
|
||||
<CTableHeaderCell scope="row">{{ index + 1 }}</CTableHeaderCell>
|
||||
<CTableDataCell class="text-center">{{ item.ticket_no }}</CTableDataCell>
|
||||
<CTableDataCell class="text-center w-25">{{ item.kode_tiket }}</CTableDataCell>
|
||||
<CTableDataCell>{{ item.deskripsi }}</CTableDataCell>
|
||||
<CTableDataCell>
|
||||
<CBadge
|
||||
:color="
|
||||
item.status === 'Closed'
|
||||
? 'success'
|
||||
: item.status === 'Approved'
|
||||
? 'info'
|
||||
: item.status === 'Rejected'
|
||||
? 'dark'
|
||||
: 'primary'
|
||||
"
|
||||
>
|
||||
{{ item.status }}
|
||||
</CBadge>
|
||||
</CTableDataCell>
|
||||
<CTableDataCell>
|
||||
<CBadge
|
||||
:color="
|
||||
item.prioritas === 'High'
|
||||
? 'danger'
|
||||
: item.prioritas === 'Medium'
|
||||
? 'warning'
|
||||
: 'secondary'
|
||||
"
|
||||
>
|
||||
{{ item.prioritas }}
|
||||
</CBadge>
|
||||
</CTableDataCell>
|
||||
<CTableDataCell class="text-center">{{ item.ditugaskan_untuk }}</CTableDataCell>
|
||||
<CTableDataCell class="text-center">
|
||||
<CButton color="info" size="sm" class="me-1" @click="openDetail(item)"
|
||||
>Detail</CButton
|
||||
>
|
||||
<CButton
|
||||
v-if="isPending(item)"
|
||||
color="success"
|
||||
size="sm"
|
||||
class="me-1"
|
||||
@click="openApproveModal(index)"
|
||||
>Setujui</CButton
|
||||
>
|
||||
<CButton
|
||||
v-if="isPending(item)"
|
||||
color="warning"
|
||||
size="sm"
|
||||
class="me-1"
|
||||
@click="openRejectModal(index)"
|
||||
>Tolak</CButton
|
||||
>
|
||||
<CButton color="warning" size="sm" class="me-1" @click="openEditModal(index)"
|
||||
>Edit</CButton
|
||||
>
|
||||
<CButton color="danger" size="sm" @click="deleteTicket(index)">Hapus</CButton>
|
||||
</CTableDataCell>
|
||||
</CTableRow>
|
||||
</CTableBody>
|
||||
</CTable>
|
||||
</div>
|
||||
</CCardBody>
|
||||
</CCard>
|
||||
</CCol>
|
||||
</CRow>
|
||||
|
||||
<!-- Form Modal (Add / Edit) -->
|
||||
<CModal :visible="ticketModal" @close="ticketModal = false" size="lg" alignment="center">
|
||||
<CModalHeader dismiss @close="ticketModal = false">
|
||||
<CModalTitle>{{ isEdit ? 'Edit Ticket' : 'Tambah Ticket' }}</CModalTitle>
|
||||
</CModalHeader>
|
||||
<CModalBody>
|
||||
<CForm>
|
||||
<CRow class="mb-3">
|
||||
<CCol :md="6">
|
||||
<CFormLabel>Ticket No</CFormLabel>
|
||||
<CFormInput v-model="form.ticket_no" />
|
||||
</CCol>
|
||||
<CCol :md="6">
|
||||
<CFormLabel>Tenant ID</CFormLabel>
|
||||
<CFormInput v-model="form.tenant_id" />
|
||||
</CCol>
|
||||
</CRow>
|
||||
<CRow class="mb-3">
|
||||
<CCol :md="6">
|
||||
<CFormLabel>Customer ID</CFormLabel>
|
||||
<CFormInput v-model="form.customer_id" />
|
||||
</CCol>
|
||||
<CCol :md="6">
|
||||
<CFormLabel>Kode Tiket</CFormLabel>
|
||||
<CFormInput v-model="form.kode_tiket" />
|
||||
</CCol>
|
||||
</CRow>
|
||||
<CRow class="mb-3">
|
||||
<CCol :md="12">
|
||||
<CFormLabel>Deskripsi</CFormLabel>
|
||||
<CFormTextarea v-model="form.deskripsi" rows="2"></CFormTextarea>
|
||||
</CCol>
|
||||
</CRow>
|
||||
<CRow class="mb-3">
|
||||
<CCol :md="4">
|
||||
<CFormLabel>Status</CFormLabel>
|
||||
<CFormSelect v-model="form.status">
|
||||
<option value="Open">Open</option>
|
||||
<option value="In Progress">In Progress</option>
|
||||
<option value="Closed">Closed</option>
|
||||
</CFormSelect>
|
||||
</CCol>
|
||||
<CCol :md="4">
|
||||
<CFormLabel>Prioritas</CFormLabel>
|
||||
<CFormSelect v-model="form.prioritas">
|
||||
<option value="Low">Low</option>
|
||||
<option value="Medium">Medium</option>
|
||||
<option value="High">High</option>
|
||||
<option value="Critical">Critical</option>
|
||||
</CFormSelect>
|
||||
</CCol>
|
||||
<CCol :md="4">
|
||||
<CFormLabel>SLA (menit)</CFormLabel>
|
||||
<CFormInput v-model="form.sla_minutes" type="number" />
|
||||
</CCol>
|
||||
</CRow>
|
||||
<CRow class="mb-3">
|
||||
<CCol :md="6">
|
||||
<CFormLabel>Ditugaskan Untuk</CFormLabel>
|
||||
<CFormInput v-model="form.ditugaskan_untuk" />
|
||||
</CCol>
|
||||
<CCol :md="6">
|
||||
<CFormLabel>Created By</CFormLabel>
|
||||
<CFormInput v-model="form.created_by" />
|
||||
</CCol>
|
||||
</CRow>
|
||||
</CForm>
|
||||
</CModalBody>
|
||||
<CModalFooter>
|
||||
<CButton color="secondary" @click="ticketModal = false">Batal</CButton>
|
||||
<CButton color="primary" @click="saveTicket">Simpan</CButton>
|
||||
</CModalFooter>
|
||||
</CModal>
|
||||
|
||||
<!-- Approve Modal -->
|
||||
<CModal :visible="approveModal" @close="approveModal = false" alignment="center">
|
||||
<CModalHeader dismiss @close="approveModal = false">
|
||||
<CModalTitle>Setujui Ticket</CModalTitle>
|
||||
</CModalHeader>
|
||||
<CModalBody>
|
||||
<CForm>
|
||||
<div class="mb-3">
|
||||
<CFormLabel>Nama Leader / SPV</CFormLabel>
|
||||
<CFormInput v-model="approverName" placeholder="Masukkan nama" />
|
||||
</div>
|
||||
</CForm>
|
||||
</CModalBody>
|
||||
<CModalFooter>
|
||||
<CButton color="secondary" @click="approveModal = false">Batal</CButton>
|
||||
<CButton color="success" @click="confirmApprove" :disabled="!approverName.trim()"
|
||||
>Setujui</CButton
|
||||
>
|
||||
</CModalFooter>
|
||||
</CModal>
|
||||
|
||||
<!-- Reject Modal -->
|
||||
<CModal :visible="rejectModal" @close="rejectModal = false" alignment="center">
|
||||
<CModalHeader dismiss @close="rejectModal = false">
|
||||
<CModalTitle>Tolak Ticket</CModalTitle>
|
||||
</CModalHeader>
|
||||
<CModalBody>
|
||||
<CForm>
|
||||
<div class="mb-3">
|
||||
<CFormLabel>Nama Leader / SPV</CFormLabel>
|
||||
<CFormInput v-model="rejectorName" placeholder="Masukkan nama" />
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<CFormLabel>Alasan Penolakan</CFormLabel>
|
||||
<CFormTextarea
|
||||
v-model="rejectReason"
|
||||
rows="3"
|
||||
placeholder="Masukkan alasan"
|
||||
></CFormTextarea>
|
||||
</div>
|
||||
</CForm>
|
||||
</CModalBody>
|
||||
<CModalFooter>
|
||||
<CButton color="secondary" @click="rejectModal = false">Batal</CButton>
|
||||
<CButton
|
||||
color="warning"
|
||||
@click="confirmReject"
|
||||
:disabled="!rejectorName.trim() || !rejectReason.trim()"
|
||||
>Tolak</CButton
|
||||
>
|
||||
</CModalFooter>
|
||||
</CModal>
|
||||
|
||||
<!-- Detail Modal -->
|
||||
<CModal :visible="detailModal" @close="detailModal = false" size="lg" alignment="center">
|
||||
<CModalHeader dismiss @close="detailModal = false">
|
||||
<CModalTitle>Detail Ticket</CModalTitle>
|
||||
</CModalHeader>
|
||||
<CModalBody>
|
||||
<CTable bordered small v-if="detailItem">
|
||||
<CTableBody>
|
||||
<CTableRow>
|
||||
<CTableHeaderCell style="width: 30%">Ticket No</CTableHeaderCell>
|
||||
<CTableDataCell>{{ detailItem.ticket_no }}</CTableDataCell>
|
||||
</CTableRow>
|
||||
<CTableRow>
|
||||
<CTableHeaderCell>Tenant ID</CTableHeaderCell>
|
||||
<CTableDataCell>{{ detailItem.tenant_id }}</CTableDataCell>
|
||||
</CTableRow>
|
||||
<CTableRow>
|
||||
<CTableHeaderCell>Customer ID</CTableHeaderCell>
|
||||
<CTableDataCell>{{ detailItem.customer_id }}</CTableDataCell>
|
||||
</CTableRow>
|
||||
<CTableRow>
|
||||
<CTableHeaderCell>Kode Tiket</CTableHeaderCell>
|
||||
<CTableDataCell>{{ detailItem.kode_tiket }}</CTableDataCell>
|
||||
</CTableRow>
|
||||
<CTableRow>
|
||||
<CTableHeaderCell>Deskripsi</CTableHeaderCell>
|
||||
<CTableDataCell>{{ detailItem.deskripsi }}</CTableDataCell>
|
||||
</CTableRow>
|
||||
<CTableRow>
|
||||
<CTableHeaderCell>Status</CTableHeaderCell>
|
||||
<CTableDataCell>
|
||||
<CBadge
|
||||
:color="
|
||||
detailItem.status === 'Closed'
|
||||
? 'success'
|
||||
: detailItem.status === 'Approved'
|
||||
? 'info'
|
||||
: detailItem.status === 'Rejected'
|
||||
? 'dark'
|
||||
: 'primary'
|
||||
"
|
||||
>
|
||||
{{ detailItem.status }}
|
||||
</CBadge>
|
||||
</CTableDataCell>
|
||||
</CTableRow>
|
||||
<CTableRow>
|
||||
<CTableHeaderCell>Prioritas</CTableHeaderCell>
|
||||
<CTableDataCell>
|
||||
<CBadge
|
||||
:color="
|
||||
detailItem.prioritas === 'High'
|
||||
? 'danger'
|
||||
: detailItem.prioritas === 'Medium'
|
||||
? 'warning'
|
||||
: 'secondary'
|
||||
"
|
||||
>
|
||||
{{ detailItem.prioritas }}
|
||||
</CBadge>
|
||||
</CTableDataCell>
|
||||
</CTableRow>
|
||||
<CTableRow>
|
||||
<CTableHeaderCell>SLA (menit)</CTableHeaderCell>
|
||||
<CTableDataCell>{{ detailItem.sla_minutes }}</CTableDataCell>
|
||||
</CTableRow>
|
||||
<CTableRow>
|
||||
<CTableHeaderCell>Ditugaskan Untuk</CTableHeaderCell>
|
||||
<CTableDataCell>{{ detailItem.ditugaskan_untuk }}</CTableDataCell>
|
||||
</CTableRow>
|
||||
<CTableRow>
|
||||
<CTableHeaderCell>Created By</CTableHeaderCell>
|
||||
<CTableDataCell>{{ detailItem.created_by }}</CTableDataCell>
|
||||
</CTableRow>
|
||||
<CTableRow>
|
||||
<CTableHeaderCell>Approved By</CTableHeaderCell>
|
||||
<CTableDataCell>{{ detailItem.approved_by || '-' }}</CTableDataCell>
|
||||
</CTableRow>
|
||||
<CTableRow>
|
||||
<CTableHeaderCell>Approved At</CTableHeaderCell>
|
||||
<CTableDataCell>{{ detailItem.approved_at || '-' }}</CTableDataCell>
|
||||
</CTableRow>
|
||||
<CTableRow>
|
||||
<CTableHeaderCell>Rejected By</CTableHeaderCell>
|
||||
<CTableDataCell>{{ detailItem.rejected_by || '-' }}</CTableDataCell>
|
||||
</CTableRow>
|
||||
<CTableRow>
|
||||
<CTableHeaderCell>Rejected At</CTableHeaderCell>
|
||||
<CTableDataCell>{{ detailItem.rejected_at || '-' }}</CTableDataCell>
|
||||
</CTableRow>
|
||||
<CTableRow>
|
||||
<CTableHeaderCell>Rejection Reason</CTableHeaderCell>
|
||||
<CTableDataCell>{{ detailItem.rejection_reason || '-' }}</CTableDataCell>
|
||||
</CTableRow>
|
||||
<CTableRow>
|
||||
<CTableHeaderCell>Assigned At</CTableHeaderCell>
|
||||
<CTableDataCell>{{ detailItem.assigned_at || '-' }}</CTableDataCell>
|
||||
</CTableRow>
|
||||
<CTableRow>
|
||||
<CTableHeaderCell>Resolved At</CTableHeaderCell>
|
||||
<CTableDataCell>{{ detailItem.resolved_at || '-' }}</CTableDataCell>
|
||||
</CTableRow>
|
||||
<CTableRow>
|
||||
<CTableHeaderCell>Closed At</CTableHeaderCell>
|
||||
<CTableDataCell>{{ detailItem.closed_at || '-' }}</CTableDataCell>
|
||||
</CTableRow>
|
||||
<CTableRow>
|
||||
<CTableHeaderCell>Created At</CTableHeaderCell>
|
||||
<CTableDataCell>{{ detailItem.created_at || '-' }}</CTableDataCell>
|
||||
</CTableRow>
|
||||
<CTableRow>
|
||||
<CTableHeaderCell>Updated At</CTableHeaderCell>
|
||||
<CTableDataCell>{{ detailItem.updated_at || '-' }}</CTableDataCell>
|
||||
</CTableRow>
|
||||
</CTableBody>
|
||||
</CTable>
|
||||
</CModalBody>
|
||||
<CModalFooter>
|
||||
<CButton color="secondary" @click="detailModal = false">Tutup</CButton>
|
||||
</CModalFooter>
|
||||
</CModal>
|
||||
</template>
|
||||
@@ -0,0 +1,57 @@
|
||||
<template>
|
||||
<CRow>
|
||||
<CCol :xs="12">
|
||||
<CCard class="mb-4">
|
||||
<CCardHeader> <strong>Ticket Materials</strong> </CCardHeader>
|
||||
<CCardBody>
|
||||
<CTable striped hover>
|
||||
<CTableHead>
|
||||
<CTableRow>
|
||||
<CTableHeaderCell scope="col">#</CTableHeaderCell>
|
||||
<CTableHeaderCell scope="col">Material Name</CTableHeaderCell>
|
||||
<CTableHeaderCell scope="col">Ticket</CTableHeaderCell>
|
||||
<CTableHeaderCell scope="col">Quantity</CTableHeaderCell>
|
||||
<CTableHeaderCell scope="col">Unit</CTableHeaderCell>
|
||||
<CTableHeaderCell scope="col">Cost</CTableHeaderCell>
|
||||
</CTableRow>
|
||||
</CTableHead>
|
||||
<CTableBody>
|
||||
<CTableRow>
|
||||
<CTableHeaderCell scope="row">1</CTableHeaderCell>
|
||||
<CTableDataCell>Cable CAT6</CTableDataCell>
|
||||
<CTableDataCell>Network setup</CTableDataCell>
|
||||
<CTableDataCell>100</CTableDataCell>
|
||||
<CTableDataCell>Meters</CTableDataCell>
|
||||
<CTableDataCell>$150.00</CTableDataCell>
|
||||
</CTableRow>
|
||||
<CTableRow>
|
||||
<CTableHeaderCell scope="row">2</CTableHeaderCell>
|
||||
<CTableDataCell>RJ45 Connector</CTableDataCell>
|
||||
<CTableDataCell>Network setup</CTableDataCell>
|
||||
<CTableDataCell>50</CTableDataCell>
|
||||
<CTableDataCell>Pcs</CTableDataCell>
|
||||
<CTableDataCell>$25.00</CTableDataCell>
|
||||
</CTableRow>
|
||||
<CTableRow>
|
||||
<CTableHeaderCell scope="row">3</CTableHeaderCell>
|
||||
<CTableDataCell>SSD 512GB</CTableDataCell>
|
||||
<CTableDataCell>Hardware upgrade</CTableDataCell>
|
||||
<CTableDataCell>2</CTableDataCell>
|
||||
<CTableDataCell>Pcs</CTableDataCell>
|
||||
<CTableDataCell>$200.00</CTableDataCell>
|
||||
</CTableRow>
|
||||
<CTableRow>
|
||||
<CTableHeaderCell scope="row">4</CTableHeaderCell>
|
||||
<CTableDataCell>RAM 16GB</CTableDataCell>
|
||||
<CTableDataCell>Hardware upgrade</CTableDataCell>
|
||||
<CTableDataCell>2</CTableDataCell>
|
||||
<CTableDataCell>Pcs</CTableDataCell>
|
||||
<CTableDataCell>$180.00</CTableDataCell>
|
||||
</CTableRow>
|
||||
</CTableBody>
|
||||
</CTable>
|
||||
</CCardBody>
|
||||
</CCard>
|
||||
</CCol>
|
||||
</CRow>
|
||||
</template>
|
||||
@@ -0,0 +1,346 @@
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { useTicketTypeStore } from '@/stores/ticketType.js'
|
||||
|
||||
const store = useTicketTypeStore()
|
||||
|
||||
const ticketTypeModal = ref(false)
|
||||
const detailModal = ref(false)
|
||||
const editingId = ref(null)
|
||||
const isEdit = ref(false)
|
||||
const form = reactive({
|
||||
code: '',
|
||||
name: '',
|
||||
need_approval: false,
|
||||
sla_minutes: '',
|
||||
require_photo: false,
|
||||
require_material: false,
|
||||
need_customer: false,
|
||||
})
|
||||
|
||||
const emptyForm = () => ({
|
||||
code: '',
|
||||
name: '',
|
||||
need_approval: false,
|
||||
sla_minutes: '',
|
||||
require_photo: false,
|
||||
require_material: false,
|
||||
need_customer: false,
|
||||
})
|
||||
|
||||
const detailItem = ref(null)
|
||||
const pagination = ref(null)
|
||||
|
||||
onMounted(() => {
|
||||
store.fetchTicketTypes()
|
||||
})
|
||||
|
||||
async function loadPage(page) {
|
||||
const result = await store.fetchTicketTypes(page)
|
||||
pagination.value = result
|
||||
}
|
||||
|
||||
function openAddModal() {
|
||||
isEdit.value = false
|
||||
editingId.value = null
|
||||
Object.assign(form, emptyForm())
|
||||
ticketTypeModal.value = true
|
||||
}
|
||||
|
||||
function openEditModal(item) {
|
||||
isEdit.value = true
|
||||
editingId.value = item.id
|
||||
Object.assign(form, {
|
||||
code: item.code,
|
||||
name: item.name,
|
||||
need_approval: item.need_approval,
|
||||
sla_minutes: item.sla_minutes,
|
||||
require_photo: item.require_photo,
|
||||
require_material: item.require_material,
|
||||
need_customer: item.need_customer,
|
||||
})
|
||||
ticketTypeModal.value = true
|
||||
}
|
||||
|
||||
function openDetail(item) {
|
||||
detailItem.value = item
|
||||
detailModal.value = true
|
||||
}
|
||||
|
||||
async function saveTicketType() {
|
||||
const payload = {
|
||||
code: form.code,
|
||||
name: form.name,
|
||||
need_approval: form.need_approval,
|
||||
sla_minutes: Number(form.sla_minutes),
|
||||
require_photo: form.require_photo,
|
||||
require_material: form.require_material,
|
||||
need_customer: form.need_customer,
|
||||
}
|
||||
|
||||
try {
|
||||
if (isEdit.value && editingId.value) {
|
||||
await store.updateTicketType(editingId.value, payload)
|
||||
} else {
|
||||
await store.addTicketType(payload)
|
||||
}
|
||||
ticketTypeModal.value = false
|
||||
await store.fetchTicketTypes()
|
||||
} catch (err) {
|
||||
store.error = err.response?.data?.message || err.message || 'Gagal menyimpan data'
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteTicketType(id) {
|
||||
try {
|
||||
await store.deleteTicketType(id)
|
||||
await store.fetchTicketTypes()
|
||||
} catch (err) {
|
||||
// error handled by store
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CRow>
|
||||
<CCol :xs="12">
|
||||
<CCard class="mb-4">
|
||||
<CCardHeader>
|
||||
<strong>Ticket Type</strong>
|
||||
<CButton color="primary" size="sm" class="float-end" @click="openAddModal">
|
||||
+ Tambah Ticket Type
|
||||
</CButton>
|
||||
</CCardHeader>
|
||||
<CCardBody>
|
||||
<CAlert color="danger" v-if="store.error" dismissible @close="store.error = null">
|
||||
{{ store.error }}
|
||||
</CAlert>
|
||||
|
||||
<div v-if="store.loading" class="text-center py-3">
|
||||
<CSpinner color="primary" />
|
||||
<span class="ms-2">Memuat data...</span>
|
||||
</div>
|
||||
|
||||
<div v-else class="table-responsive">
|
||||
<CTable striped hover small>
|
||||
<CTableHead>
|
||||
<CTableRow>
|
||||
<CTableHeaderCell scope="col">No</CTableHeaderCell>
|
||||
<CTableHeaderCell scope="col" class="text-center">Code</CTableHeaderCell>
|
||||
<CTableHeaderCell scope="col">Name</CTableHeaderCell>
|
||||
<CTableHeaderCell scope="col" class="text-center">Need Approval</CTableHeaderCell>
|
||||
<CTableHeaderCell scope="col" class="text-center">SLA (menit)</CTableHeaderCell>
|
||||
<CTableHeaderCell scope="col" class="text-center">Require Photo</CTableHeaderCell>
|
||||
<CTableHeaderCell scope="col" class="text-center"
|
||||
>Require Material</CTableHeaderCell
|
||||
>
|
||||
<CTableHeaderCell scope="col" class="text-center">Need Customer</CTableHeaderCell>
|
||||
<CTableHeaderCell scope="col" class="text-center">Aksi</CTableHeaderCell>
|
||||
</CTableRow>
|
||||
</CTableHead>
|
||||
<CTableBody>
|
||||
<CTableRow v-for="(item, index) in store.ticketTypes" :key="item.id">
|
||||
<CTableHeaderCell scope="row">{{ index + 1 }}</CTableHeaderCell>
|
||||
<CTableDataCell class="text-center">
|
||||
<CBadge color="dark">{{ item.code }}</CBadge>
|
||||
</CTableDataCell>
|
||||
<CTableDataCell>{{ item.name }}</CTableDataCell>
|
||||
<CTableDataCell class="text-center">
|
||||
<CBadge :color="item.need_approval ? 'success' : 'secondary'">
|
||||
{{ item.need_approval ? 'Yes' : 'No' }}
|
||||
</CBadge>
|
||||
</CTableDataCell>
|
||||
<CTableDataCell class="text-center">{{ item.sla_minutes }}</CTableDataCell>
|
||||
<CTableDataCell class="text-center">
|
||||
<CBadge :color="item.require_photo ? 'success' : 'secondary'">
|
||||
{{ item.require_photo ? 'Yes' : 'No' }}
|
||||
</CBadge>
|
||||
</CTableDataCell>
|
||||
<CTableDataCell class="text-center">
|
||||
<CBadge :color="item.require_material ? 'success' : 'secondary'">
|
||||
{{ item.require_material ? 'Yes' : 'No' }}
|
||||
</CBadge>
|
||||
</CTableDataCell>
|
||||
<CTableDataCell class="text-center">
|
||||
<CBadge :color="item.need_customer ? 'success' : 'secondary'">
|
||||
{{ item.need_customer ? 'Yes' : 'No' }}
|
||||
</CBadge>
|
||||
</CTableDataCell>
|
||||
<CTableDataCell class="text-center">
|
||||
<CButton color="info" size="sm" class="me-1" @click="openDetail(item)"
|
||||
>Detail</CButton
|
||||
>
|
||||
<CButton color="warning" size="sm" class="me-1" @click="openEditModal(item)"
|
||||
>Edit</CButton
|
||||
>
|
||||
<CButton color="danger" size="sm" @click="deleteTicketType(item.id)"
|
||||
>Hapus</CButton
|
||||
>
|
||||
</CTableDataCell>
|
||||
</CTableRow>
|
||||
<CTableRow v-if="store.ticketTypes.length === 0">
|
||||
<CTableDataCell colSpan="9" class="text-center">Tidak ada data</CTableDataCell>
|
||||
</CTableRow>
|
||||
</CTableBody>
|
||||
</CTable>
|
||||
</div>
|
||||
|
||||
<nav v-if="pagination" class="mt-3">
|
||||
<ul class="pagination justify-content-center mb-0">
|
||||
<li class="page-item" :class="{ disabled: !pagination.prev_page_url }">
|
||||
<button
|
||||
class="page-link"
|
||||
@click="loadPage(pagination.current_page - 1)"
|
||||
:disabled="!pagination.prev_page_url"
|
||||
>
|
||||
Previous
|
||||
</button>
|
||||
</li>
|
||||
<li
|
||||
class="page-item"
|
||||
v-for="link in pagination.links"
|
||||
:key="link.label"
|
||||
:class="{ active: link.active }"
|
||||
v-if="!link.label.includes('Previous') && !link.label.includes('Next')"
|
||||
>
|
||||
<button class="page-link" @click="loadPage(link.page)" v-html="link.label"></button>
|
||||
</li>
|
||||
<li class="page-item" :class="{ disabled: !pagination.next_page_url }">
|
||||
<button
|
||||
class="page-link"
|
||||
@click="loadPage(pagination.current_page + 1)"
|
||||
:disabled="!pagination.next_page_url"
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</CCardBody>
|
||||
</CCard>
|
||||
</CCol>
|
||||
</CRow>
|
||||
|
||||
<!-- Form Modal (Add / Edit) -->
|
||||
<CModal :visible="ticketTypeModal" @close="ticketTypeModal = false" size="lg" alignment="center">
|
||||
<CModalHeader dismiss @close="ticketTypeModal = false">
|
||||
<CModalTitle>{{ isEdit ? 'Edit Ticket Type' : 'Tambah Ticket Type' }}</CModalTitle>
|
||||
</CModalHeader>
|
||||
<CModalBody>
|
||||
<CForm>
|
||||
<CRow class="mb-3">
|
||||
<CCol :md="6">
|
||||
<CFormLabel>Code</CFormLabel>
|
||||
<CFormInput v-model="form.code" placeholder="Contoh: GGN" />
|
||||
</CCol>
|
||||
<CCol :md="6">
|
||||
<CFormLabel>Name</CFormLabel>
|
||||
<CFormInput v-model="form.name" placeholder="Contoh: Gangguan" />
|
||||
</CCol>
|
||||
</CRow>
|
||||
<CRow class="mb-3">
|
||||
<CCol :md="6">
|
||||
<CFormLabel>SLA (menit)</CFormLabel>
|
||||
<CFormInput v-model="form.sla_minutes" type="number" />
|
||||
</CCol>
|
||||
</CRow>
|
||||
<CRow class="mb-3">
|
||||
<CCol :md="3">
|
||||
<CFormCheck id="need_approval" v-model="form.need_approval" :label="'Need Approval'" />
|
||||
</CCol>
|
||||
<CCol :md="3">
|
||||
<CFormCheck id="require_photo" v-model="form.require_photo" :label="'Require Photo'" />
|
||||
</CCol>
|
||||
<CCol :md="3">
|
||||
<CFormCheck
|
||||
id="require_material"
|
||||
v-model="form.require_material"
|
||||
:label="'Require Material'"
|
||||
/>
|
||||
</CCol>
|
||||
<CCol :md="3">
|
||||
<CFormCheck id="need_customer" v-model="form.need_customer" :label="'Need Customer'" />
|
||||
</CCol>
|
||||
</CRow>
|
||||
</CForm>
|
||||
</CModalBody>
|
||||
<CModalFooter>
|
||||
<CButton color="secondary" @click="ticketTypeModal = false">Batal</CButton>
|
||||
<CButton color="primary" @click="saveTicketType">Simpan</CButton>
|
||||
</CModalFooter>
|
||||
</CModal>
|
||||
|
||||
<!-- Detail Modal -->
|
||||
<CModal :visible="detailModal" @close="detailModal = false" size="lg" alignment="center">
|
||||
<CModalHeader dismiss @close="detailModal = false">
|
||||
<CModalTitle>Detail Ticket Type</CModalTitle>
|
||||
</CModalHeader>
|
||||
<CModalBody>
|
||||
<CTable bordered small v-if="detailItem">
|
||||
<CTableBody>
|
||||
<CTableRow>
|
||||
<CTableHeaderCell style="width: 30%">ID</CTableHeaderCell>
|
||||
<CTableDataCell>{{ detailItem.id }}</CTableDataCell>
|
||||
</CTableRow>
|
||||
<CTableRow>
|
||||
<CTableHeaderCell>Code</CTableHeaderCell>
|
||||
<CTableDataCell>
|
||||
<CBadge color="dark">{{ detailItem.code }}</CBadge>
|
||||
</CTableDataCell>
|
||||
</CTableRow>
|
||||
<CTableRow>
|
||||
<CTableHeaderCell>Name</CTableHeaderCell>
|
||||
<CTableDataCell>{{ detailItem.name }}</CTableDataCell>
|
||||
</CTableRow>
|
||||
<CTableRow>
|
||||
<CTableHeaderCell>Need Approval</CTableHeaderCell>
|
||||
<CTableDataCell>
|
||||
<CBadge :color="detailItem.need_approval ? 'success' : 'secondary'">
|
||||
{{ detailItem.need_approval ? 'Yes' : 'No' }}
|
||||
</CBadge>
|
||||
</CTableDataCell>
|
||||
</CTableRow>
|
||||
<CTableRow>
|
||||
<CTableHeaderCell>SLA (menit)</CTableHeaderCell>
|
||||
<CTableDataCell>{{ detailItem.sla_minutes }}</CTableDataCell>
|
||||
</CTableRow>
|
||||
<CTableRow>
|
||||
<CTableHeaderCell>Require Photo</CTableHeaderCell>
|
||||
<CTableDataCell>
|
||||
<CBadge :color="detailItem.require_photo ? 'success' : 'secondary'">
|
||||
{{ detailItem.require_photo ? 'Yes' : 'No' }}
|
||||
</CBadge>
|
||||
</CTableDataCell>
|
||||
</CTableRow>
|
||||
<CTableRow>
|
||||
<CTableHeaderCell>Require Material</CTableHeaderCell>
|
||||
<CTableDataCell>
|
||||
<CBadge :color="detailItem.require_material ? 'success' : 'secondary'">
|
||||
{{ detailItem.require_material ? 'Yes' : 'No' }}
|
||||
</CBadge>
|
||||
</CTableDataCell>
|
||||
</CTableRow>
|
||||
<CTableRow>
|
||||
<CTableHeaderCell>Need Customer</CTableHeaderCell>
|
||||
<CTableDataCell>
|
||||
<CBadge :color="detailItem.need_customer ? 'success' : 'secondary'">
|
||||
{{ detailItem.need_customer ? 'Yes' : 'No' }}
|
||||
</CBadge>
|
||||
</CTableDataCell>
|
||||
</CTableRow>
|
||||
<CTableRow>
|
||||
<CTableHeaderCell>Created At</CTableHeaderCell>
|
||||
<CTableDataCell>{{ detailItem.created_at || '-' }}</CTableDataCell>
|
||||
</CTableRow>
|
||||
<CTableRow>
|
||||
<CTableHeaderCell>Updated At</CTableHeaderCell>
|
||||
<CTableDataCell>{{ detailItem.updated_at || '-' }}</CTableDataCell>
|
||||
</CTableRow>
|
||||
</CTableBody>
|
||||
</CTable>
|
||||
</CModalBody>
|
||||
<CModalFooter>
|
||||
<CButton color="secondary" @click="detailModal = false">Tutup</CButton>
|
||||
</CModalFooter>
|
||||
</CModal>
|
||||
</template>
|
||||
Reference in New Issue
Block a user