Compare commits

...

10 Commits

Author SHA1 Message Date
sean 2f289fb09e update Ticket
NPM Installation / build (16.x, ubuntu-latest) (push) Has been cancelled
NPM Installation / build (16.x, windows-latest) (push) Has been cancelled
NPM Installation / build (17.x, ubuntu-latest) (push) Has been cancelled
NPM Installation / build (17.x, windows-latest) (push) Has been cancelled
NPM Installation / build (18.x, ubuntu-latest) (push) Has been cancelled
NPM Installation / build (18.x, windows-latest) (push) Has been cancelled
2026-06-25 09:36:23 +07:00
sean a06d0badfc UI 2026-06-24 14:52:49 +07:00
mrholek 974ba0e3c3 refactor: updade examples style 2026-04-01 17:08:15 +02:00
mrholek 8e07f38df5 release: v5.5.0 2026-04-01 10:55:19 +02:00
mrholek e016f18ce6 update list of components 2026-04-01 10:50:54 +02:00
mrholek 0808fec88b update current year 2026-04-01 10:49:34 +02:00
mrholek 7e6a087049 docs: add AI-friendly documentation and code comments
Add comprehensive documentation for AI assistants:
- .cursorrules with project context and conventions
- ARCHITECTURE.md with technical details
- DEVELOPMENT.md with practical guides
- JSDoc comments in all JavaScript modules
- Update README.md with AI-Friendly Development section

This enables AI tools (Cursor, Claude Code, GitHub Copilot) to understand the project and generate code following CoreUI Vue patterns.
2026-04-01 10:47:02 +02:00
mrholek 970ed3a69c feat(CChip, CChipInput): add new components 2026-04-01 10:29:41 +02:00
mrholek 20c410bcac chore: update documentation banner 2026-04-01 10:29:08 +02:00
mrholek 1bb7b0d737 chore: update dependencies and devDependencies
@coreui/chartjs       ^4.1.0  →    ^4.2.0
@coreui/coreui        ^5.5.0  →    ^5.6.1
@coreui/vue           ^5.7.0  →    ^5.8.0
@vitejs/plugin-vue    ^6.0.3  →    ^6.0.5
autoprefixer        ^10.4.23  →  ^10.4.27
eslint               ^9.39.2  →   ^9.39.4
eslint-plugin-vue    ^10.6.2  →   ^10.8.0
globals              ^16.3.0  →   ^16.5.0
postcss               ^8.5.6  →    ^8.5.8
sass                 ^1.97.0  →   ^1.98.0
vite                  ^7.3.0  →    ^8.0.3
vue                  ^3.5.26  →   ^3.5.31
vue-router            ^4.6.4  →    ^5.0.4
2026-04-01 10:06:07 +02:00
27 changed files with 4101 additions and 40 deletions
+349
View File
@@ -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
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,6 +1,6 @@
The MIT License (MIT) 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 Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
+42 -2
View File
@@ -1,4 +1,4 @@
# CoreUI Free Vue Admin Template [![Tweet](https://img.shields.io/twitter/url/http/shields.io.svg?style=social&logo=twitter)](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 [![Tweet](https://img.shields.io/twitter/url/http/shields.io.svg?style=social&logo=twitter)](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)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square)](https://opensource.org/licenses/MIT) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square)](https://opensource.org/licenses/MIT)
[![@coreui coreui](https://img.shields.io/badge/@coreui%20-coreui-lightgrey.svg?style=flat-square)](https://github.com/coreui/coreui) [![@coreui coreui](https://img.shields.io/badge/@coreui%20-coreui-lightgrey.svg?style=flat-square)](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) * [Installation](#installation)
* [Basic usage](#basic-usage) * [Basic usage](#basic-usage)
* [What's included](#whats-included) * [What's included](#whats-included)
* [AI-Friendly Development](#ai-friendly-development)
* [Documentation](#documentation) * [Documentation](#documentation)
* [Components](#components) * [Components](#components)
* [Versioning](#versioning) * [Versioning](#versioning)
@@ -135,6 +136,43 @@ coreui-free-vue-admin-template
└── vite.config.mjs └── 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 ## Documentation
The documentation for the CoreUI Admin Template is hosted at our website [CoreUI for Vue](https://coreui.io/vue/docs/templates/installation.html) 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 Card](https://coreui.io/vue/docs/components/card.html)
- [Vue Carousel](https://coreui.io/vue/docs/components/carousel.html) - [Vue Carousel](https://coreui.io/vue/docs/components/carousel.html)
- [Vue Checkbox](https://coreui.io/vue/docs/forms/checkbox.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 Close Button](https://coreui.io/vue/docs/components/close-button.html)
- [Vue Collapse](https://coreui.io/vue/docs/components/collapse.html) - [Vue Collapse](https://coreui.io/vue/docs/components/collapse.html)
- [Vue Date Picker](https://coreui.io/vue/docs/forms/date-picker.html) **PRO** - [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 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). Code released under [the MIT license](https://github.com/coreui/coreui-free-react-admin-template/blob/main/LICENSE).
+2 -2
View File
@@ -1,9 +1,9 @@
<!DOCTYPE html> <!DOCTYPE html>
<!-- <!--
* CoreUI Free Vue.js Admin Template * CoreUI Free Vue.js Admin Template
* @version v5.4.0 * @version v5.5.0
* @link https://coreui.io/product/free-vue-admin-template/ * @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) * Licensed under MIT (https://github.com/coreui/coreui-free-vue-admin-template/blob/main/LICENSE)
--> -->
<html> <html>
+17 -16
View File
@@ -1,6 +1,6 @@
{ {
"name": "@coreui/coreui-free-vue-admin-template", "name": "@coreui/coreui-free-vue-admin-template",
"version": "5.4.0", "version": "5.5.0",
"description": "CoreUI Free Vue Admin Template", "description": "CoreUI Free Vue Admin Template",
"bugs": { "bugs": {
"url": "https://github.com/coreui/coreui-free-vue-admin-template/issues" "url": "https://github.com/coreui/coreui-free-vue-admin-template/issues"
@@ -18,28 +18,29 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@coreui/chartjs": "^4.1.0", "@coreui/chartjs": "^4.2.0",
"@coreui/coreui": "^5.4.1", "@coreui/coreui": "^5.6.1",
"@coreui/icons": "^3.0.1", "@coreui/icons": "^3.0.1",
"@coreui/icons-vue": "2.2.0", "@coreui/icons-vue": "2.2.0",
"@coreui/utils": "^2.0.2", "@coreui/utils": "^2.0.2",
"@coreui/vue": "^5.5.0", "@coreui/vue": "^5.8.0",
"@coreui/vue-chartjs": "^3.0.0", "@coreui/vue-chartjs": "^3.0.0",
"@popperjs/core": "^2.11.8", "@popperjs/core": "^2.11.8",
"chart.js": "^4.5.0", "axios": "^1.18.1",
"pinia": "^3.0.3", "chart.js": "^4.5.1",
"pinia": "^3.0.4",
"simplebar-vue": "^2.4.2", "simplebar-vue": "^2.4.2",
"vue": "^3.5.18", "vue": "^3.5.31",
"vue-router": "^4.5.1" "vue-router": "^5.0.4"
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "^6.0.1", "@vitejs/plugin-vue": "^6.0.5",
"autoprefixer": "^10.4.21", "autoprefixer": "^10.4.27",
"eslint": "^9.32.0", "eslint": "^9.39.4",
"eslint-plugin-vue": "^10.4.0", "eslint-plugin-vue": "^10.8.0",
"globals": "^16.3.0", "globals": "^16.5.0",
"postcss": "^8.5.6", "postcss": "^8.5.8",
"sass": "^1.90.0", "sass": "^1.98.0",
"vite": "^7.1.0" "vite": "^8.0.3"
} }
} }
+14
View File
@@ -1,9 +1,23 @@
<script setup> <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 { onBeforeMount } from 'vue'
import { useColorModes } from '@coreui/vue' import { useColorModes } from '@coreui/vue'
import { useThemeStore } from '@/stores/theme.js' import { useThemeStore } from '@/stores/theme.js'
// Initialize CoreUI color modes with local storage key
const { isColorModeSet, setColorMode } = useColorModes( const { isColorModeSet, setColorMode } = useColorModes(
'coreui-free-vue-admin-template-theme', 'coreui-free-vue-admin-template-theme',
) )
+76 -1
View File
@@ -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 [ export default [
{ {
component: 'CNavItem', component: 'CNavItem',
@@ -9,6 +32,48 @@ export default [
text: 'NEW', 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', component: 'CNavTitle',
name: 'Theme', name: 'Theme',
@@ -65,6 +130,11 @@ export default [
name: 'Carousels', name: 'Carousels',
to: '/base/carousels', to: '/base/carousels',
}, },
{
component: 'CNavItem',
name: 'Chips',
to: '/base/chips',
},
{ {
component: 'CNavItem', component: 'CNavItem',
name: 'Collapses', name: 'Collapses',
@@ -148,7 +218,7 @@ export default [
color: 'danger', color: 'danger',
text: 'PRO', text: 'PRO',
}, },
} },
], ],
}, },
{ {
@@ -205,6 +275,11 @@ export default [
name: 'Checks & Radios', name: 'Checks & Radios',
to: '/forms/checks-radios', to: '/forms/checks-radios',
}, },
{
component: 'CNavItem',
name: 'Chip Input',
to: '/forms/chip-input',
},
{ {
component: 'CNavItem', component: 'CNavItem',
name: 'Date Picker', name: 'Date Picker',
Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

+21 -2
View File
@@ -1,7 +1,8 @@
<script setup> <script setup>
import { RouterLink } from 'vue-router' 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 { sygnet } from '@/assets/brand/sygnet'
import { AppSidebarNav } from '@/components/AppSidebarNav.js' import { AppSidebarNav } from '@/components/AppSidebarNav.js'
import { useSidebarStore } from '@/stores/sidebar.js' import { useSidebarStore } from '@/stores/sidebar.js'
@@ -20,9 +21,27 @@ const sidebar = useSidebarStore()
> >
<CSidebarHeader class="border-bottom"> <CSidebarHeader class="border-bottom">
<RouterLink custom to="/" v-slot="{ href, navigate }"> <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-full" :icon="logo" :height="32" />
<CIcon custom-class-name="sidebar-brand-narrow" :icon="sygnet" :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> </CSidebarBrand>
</RouterLink> </RouterLink>
<CCloseButton class="d-lg-none" dark @click="sidebar.toggleVisible()" /> <CCloseButton class="d-lg-none" dark @click="sidebar.toggleVisible()" />
+10 -1
View File
@@ -28,7 +28,7 @@ import ComponentsImg from '@/assets/images/components.webp'
explore extended examples, detailed API documentation, and customization options, refer to explore extended examples, detailed API documentation, and customization options, refer to
our docs. our docs.
</div> </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 <a
class="btn btn-primary text-nowrap text-white" class="btn btn-primary text-nowrap text-white"
:href="`https://coreui.io/vue/docs/${props.href}`" :href="`https://coreui.io/vue/docs/${props.href}`"
@@ -37,6 +37,15 @@ import ComponentsImg from '@/assets/images/components.webp'
> >
Explore Documentation Explore Documentation
</a> </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> </div>
</div> </div>
+31 -3
View File
@@ -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 { createApp } from 'vue'
import { createPinia } from 'pinia' import { createPinia } from 'pinia'
import App from './App.vue' import App from './App.vue'
import router from './router' import router from './router'
// CoreUI Vue components and icons
import CoreuiVue from '@coreui/vue' import CoreuiVue from '@coreui/vue'
import CIcon from '@coreui/icons-vue' import CIcon from '@coreui/icons-vue'
import { iconsSet as icons } from '@/assets/icons' import { iconsSet as icons } from '@/assets/icons'
// Documentation components (remove in production if not needed)
import DocsComponents from '@/components/DocsComponents' import DocsComponents from '@/components/DocsComponents'
import DocsExample from '@/components/DocsExample' import DocsExample from '@/components/DocsExample'
import DocsIcons from '@/components/DocsIcons' import DocsIcons from '@/components/DocsIcons'
// Create Vue application instance
const app = createApp(App) const app = createApp(App)
app.use(createPinia())
app.use(router) // Install plugins
app.use(CoreuiVue) 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) app.provide('icons', icons)
// Register global components
app.component('CIcon', CIcon) app.component('CIcon', CIcon)
app.component('DocsComponents', DocsComponents) app.component('DocsComponents', DocsComponents)
app.component('DocsExample', DocsExample) app.component('DocsExample', DocsExample)
app.component('DocsIcons', DocsIcons) app.component('DocsIcons', DocsIcons)
// Mount application to DOM
app.mount('#app') app.mount('#app')
+75
View File
@@ -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 { h, resolveComponent } from 'vue'
import { createRouter, createWebHashHistory } from 'vue-router' import { createRouter, createWebHashHistory } from 'vue-router'
import DefaultLayout from '@/layouts/DefaultLayout' import DefaultLayout from '@/layouts/DefaultLayout'
/**
* Application routes configuration
* @type {Array<Object>}
*/
const routes = [ const routes = [
{ {
path: '/', path: '/',
@@ -21,6 +49,43 @@ const routes = [
/* webpackChunkName: "dashboard" */ '@/views/dashboard/Dashboard.vue' /* 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', path: '/theme',
name: 'Theme', name: 'Theme',
@@ -66,6 +131,11 @@ const routes = [
name: 'Carousels', name: 'Carousels',
component: () => import('@/views/base/Carousels.vue'), component: () => import('@/views/base/Carousels.vue'),
}, },
{
path: '/base/chips',
name: 'Chips',
component: () => import('@/views/base/Chips.vue'),
},
{ {
path: '/base/collapses', path: '/base/collapses',
name: 'Collapses', name: 'Collapses',
@@ -175,6 +245,11 @@ const routes = [
name: 'Checks & Radios', name: 'Checks & Radios',
component: () => import('@/views/forms/ChecksRadios.vue'), component: () => import('@/views/forms/ChecksRadios.vue'),
}, },
{
path: '/forms/chip-input',
name: 'Chip Input',
component: () => import('@/views/forms/ChipInput.vue'),
},
{ {
path: '/forms/range', path: '/forms/range',
name: 'Range', name: 'Range',
+19
View File
@@ -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
+105
View File
@@ -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 }
})
+94
View File
@@ -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,
}
})
+1 -7
View File
@@ -9,7 +9,7 @@
} }
.tab-content { .tab-content {
background-color: var(--#{$prefix}tertiary-bg); background-color: rgba(var(--#{$prefix}tertiary-bg-rgb), .75);
} }
& + p { & + p {
@@ -106,9 +106,3 @@
} }
} }
} }
@include color-mode(dark) {
.example .tab-content {
background-color: var(--#{$prefix}secondary-bg);
}
}
+69 -5
View File
@@ -1,8 +1,8 @@
@use "@coreui/coreui/scss/coreui" as * with ( @use '@coreui/coreui/scss/coreui' as * with (
$enable-deprecation-messages: false $enable-deprecation-messages: false
); );
@use "@coreui/chartjs/scss/coreui-chartjs"; @use '@coreui/chartjs/scss/coreui-chartjs';
@use "vendors/simplebar"; @use 'vendors/simplebar';
body { body {
background-color: var(--cui-tertiary-bg); background-color: var(--cui-tertiary-bg);
@@ -12,7 +12,7 @@ body {
width: 100%; width: 100%;
padding-inline: var(--cui-sidebar-occupy-start, 0) var(--cui-sidebar-occupy-end, 0); padding-inline: var(--cui-sidebar-occupy-start, 0) var(--cui-sidebar-occupy-end, 0);
will-change: auto; will-change: auto;
@include transition(padding .15s); @include transition(padding 0.15s);
} }
.header > .container-fluid, .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 { .header > .container-fluid + .container-fluid {
min-height: 3rem; min-height: 3rem;
} }
@@ -57,11 +88,44 @@ body {
} }
@include color-mode(dark) { @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 { body {
background-color: var(--cui-dark-bg-subtle); background-color: var(--cui-body-bg);
} }
.footer { .footer {
--cui-footer-bg: var(--cui-body-bg); --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;
}
+166
View File
@@ -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>
+190
View File
@@ -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>
+55
View File
@@ -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>
+53
View File
@@ -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>
+479
View File
@@ -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>
+57
View File
@@ -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>
+346
View File
@@ -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>