Complete corradAF template transformation and add comprehensive llms.txt
This commit is contained in:
parent
bcbf2f0958
commit
06c580ce97
211
README.md
211
README.md
@ -1,45 +1,214 @@
|
|||||||
# Nuxt 3 Minimal Starter
|
# corradAF - Corrad Application Framework
|
||||||
|
|
||||||
Look at the [nuxt 3 documentation](https://v3.nuxtjs.org) to learn more.
|
Welcome to **corradAF**, a comprehensive Nuxt.js template designed for rapid application development. This framework provides a solid foundation with essential development tools, authentication system, and modern UI components.
|
||||||
|
|
||||||
## Setup
|
## 🚀 Features
|
||||||
|
|
||||||
Make sure to install the dependencies:
|
- **🔐 Complete Authentication System** - Login, Register, Password Recovery, Logout
|
||||||
|
- **👥 User Management** - User and role management with CRUD operations
|
||||||
|
- **🛠️ Development Tools Suite** - Comprehensive set of dev tools for rapid development
|
||||||
|
- **🎨 Modern UI Components** - Built with TailwindCSS and custom components
|
||||||
|
- **📱 Responsive Design** - Mobile-first approach with modern UX patterns
|
||||||
|
- **🔧 API Management** - Built-in API endpoint design and testing tools
|
||||||
|
- **📝 Content Management** - Dynamic content and template management
|
||||||
|
- **🎯 Menu Configuration** - Easy navigation structure management
|
||||||
|
- **💻 Code Playground** - In-browser code testing and prototyping
|
||||||
|
- **🗄️ ORM Integration** - Database schema management tools
|
||||||
|
- **⚙️ Configuration Management** - System settings and environment setup
|
||||||
|
|
||||||
|
## 🛠️ Development Tools Included
|
||||||
|
|
||||||
|
### User Management
|
||||||
|
- User CRUD operations
|
||||||
|
- Role-based access control
|
||||||
|
- Permission management
|
||||||
|
|
||||||
|
### Menu Editor
|
||||||
|
- Dynamic navigation configuration
|
||||||
|
- Hierarchical menu structure
|
||||||
|
- Permission-based menu visibility
|
||||||
|
|
||||||
|
### API Editor
|
||||||
|
- Design and test API endpoints
|
||||||
|
- Interactive API documentation
|
||||||
|
- Request/response testing
|
||||||
|
|
||||||
|
### Content Editor
|
||||||
|
- Dynamic content management
|
||||||
|
- Template editing
|
||||||
|
- Content versioning
|
||||||
|
|
||||||
|
### Code Playground
|
||||||
|
- Real-time code testing
|
||||||
|
- Multiple language support
|
||||||
|
- Instant preview
|
||||||
|
|
||||||
|
### ORM Tools
|
||||||
|
- Database schema management
|
||||||
|
- Query builder interface
|
||||||
|
- Migration tools
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
- Environment variable management
|
||||||
|
- System settings
|
||||||
|
- Feature toggles
|
||||||
|
|
||||||
|
## 📋 Prerequisites
|
||||||
|
|
||||||
|
- Node.js 18+
|
||||||
|
- Yarn or npm
|
||||||
|
- Database (PostgreSQL/MySQL recommended)
|
||||||
|
|
||||||
|
## 🚀 Quick Start
|
||||||
|
|
||||||
|
### 1. Clone the Template
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# yarn
|
git clone <repository-url> your-project-name
|
||||||
|
cd your-project-name
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Install Dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Using yarn (recommended)
|
||||||
yarn install
|
yarn install
|
||||||
|
|
||||||
# npm
|
# Or using npm
|
||||||
npm install
|
npm install
|
||||||
|
|
||||||
# pnpm
|
|
||||||
pnpm install --shamefully-hoist
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Development Server
|
### 3. Environment Setup
|
||||||
|
|
||||||
Start the development server on http://localhost:3000
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run dev
|
# Copy environment template
|
||||||
|
cp .env.example .env
|
||||||
|
|
||||||
|
# Edit your environment variables
|
||||||
|
nano .env
|
||||||
```
|
```
|
||||||
|
|
||||||
## Production
|
Configure your database connection and other environment variables in the `.env` file.
|
||||||
|
|
||||||
Build the application for production:
|
### 4. Database Setup
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run build
|
# Run database migrations and generate Prisma client
|
||||||
|
yarn prisma
|
||||||
|
|
||||||
|
# Or manually
|
||||||
|
npx prisma db pull && npx prisma generate
|
||||||
```
|
```
|
||||||
|
|
||||||
Locally preview production build:
|
### 5. Start Development Server
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run preview
|
yarn dev
|
||||||
```
|
```
|
||||||
|
|
||||||
Checkout the [deployment documentation](https://v3.nuxtjs.org/guide/deploy/presets) for more information.
|
Your application will be available at `http://localhost:3000`
|
||||||
# corradAF
|
|
||||||
|
|
||||||
This is the base project for corradAF.
|
## 📁 Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
├── assets/ # Static assets (images, styles)
|
||||||
|
├── components/ # Vue components
|
||||||
|
├── composables/ # Vue composables
|
||||||
|
├── layouts/ # Application layouts
|
||||||
|
├── middleware/ # Route middleware
|
||||||
|
├── navigation/ # Navigation configuration
|
||||||
|
├── pages/ # Application pages
|
||||||
|
│ ├── devtool/ # Development tools
|
||||||
|
│ ├── dashboard/ # Main dashboard
|
||||||
|
│ ├── login/ # Authentication pages
|
||||||
|
│ ├── register/ # User registration
|
||||||
|
│ └── ...
|
||||||
|
├── plugins/ # Nuxt plugins
|
||||||
|
├── prisma/ # Database schema and migrations
|
||||||
|
├── public/ # Public static files
|
||||||
|
├── server/ # Server-side API
|
||||||
|
├── stores/ # Pinia stores
|
||||||
|
└── templates/ # Template files
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 Configuration
|
||||||
|
|
||||||
|
### Database
|
||||||
|
Configure your database connection in the `.env` file:
|
||||||
|
|
||||||
|
```env
|
||||||
|
DATABASE_URL="postgresql://username:password@localhost:5432/database_name"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Authentication
|
||||||
|
Set up JWT secrets and authentication settings:
|
||||||
|
|
||||||
|
```env
|
||||||
|
JWT_SECRET="your-super-secret-jwt-key"
|
||||||
|
AUTH_ORIGIN="http://localhost:3000"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎨 Customization
|
||||||
|
|
||||||
|
### Theme Configuration
|
||||||
|
Customize colors, fonts, and spacing in:
|
||||||
|
- `tailwind.config.js` - TailwindCSS configuration
|
||||||
|
- `app.config.js` - Application-specific settings
|
||||||
|
|
||||||
|
### Adding New Development Tools
|
||||||
|
1. Create a new page in `pages/devtool/your-tool/`
|
||||||
|
2. Add navigation entry in the navigation configuration
|
||||||
|
3. Implement your tool's functionality
|
||||||
|
|
||||||
|
## 📖 Documentation
|
||||||
|
|
||||||
|
- [Nuxt 3 Documentation](https://nuxt.com/docs)
|
||||||
|
- [TailwindCSS Documentation](https://tailwindcss.com/docs)
|
||||||
|
- [Prisma Documentation](https://www.prisma.io/docs)
|
||||||
|
- [FormKit Documentation](https://formkit.com/)
|
||||||
|
|
||||||
|
## 🚀 Deployment
|
||||||
|
|
||||||
|
### Build for Production
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Preview Production Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn preview
|
||||||
|
```
|
||||||
|
|
||||||
|
### Environment Variables for Production
|
||||||
|
|
||||||
|
Ensure all environment variables are properly set in your production environment:
|
||||||
|
|
||||||
|
- `DATABASE_URL`
|
||||||
|
- `JWT_SECRET`
|
||||||
|
- `AUTH_ORIGIN`
|
||||||
|
- `NUXT_SECRET_KEY`
|
||||||
|
|
||||||
|
## 🤝 Contributing
|
||||||
|
|
||||||
|
1. Fork the repository
|
||||||
|
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
||||||
|
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
|
||||||
|
4. Push to the branch (`git push origin feature/amazing-feature`)
|
||||||
|
5. Open a Pull Request
|
||||||
|
|
||||||
|
## 📄 License
|
||||||
|
|
||||||
|
This project is licensed under the MIT License - see the LICENSE file for details.
|
||||||
|
|
||||||
|
## 🆘 Support
|
||||||
|
|
||||||
|
For support and questions:
|
||||||
|
- Create an issue in the repository
|
||||||
|
- Check existing documentation
|
||||||
|
- Review the development tools included in the framework
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Built with ❤️ using Nuxt 3, TailwindCSS, and modern web technologies.**
|
||||||
|
155
SETUP.md
Normal file
155
SETUP.md
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
# corradAF Setup Guide
|
||||||
|
|
||||||
|
This guide will help you set up the corradAF framework template for your new project.
|
||||||
|
|
||||||
|
## 🚀 Quick Setup
|
||||||
|
|
||||||
|
### 1. Environment Configuration
|
||||||
|
|
||||||
|
Create a `.env` file in your project root with the following variables:
|
||||||
|
|
||||||
|
```env
|
||||||
|
# Database Configuration
|
||||||
|
DATABASE_URL="postgresql://username:password@localhost:5432/database_name"
|
||||||
|
|
||||||
|
# Authentication
|
||||||
|
JWT_SECRET="your-super-secret-jwt-key-change-this-in-production"
|
||||||
|
AUTH_ORIGIN="http://localhost:3000"
|
||||||
|
|
||||||
|
# Application
|
||||||
|
NUXT_SECRET_KEY="your-nuxt-secret-key-for-session-encryption"
|
||||||
|
APP_NAME="Your Application Name"
|
||||||
|
APP_URL="http://localhost:3000"
|
||||||
|
|
||||||
|
# Email Configuration (Optional)
|
||||||
|
MAIL_HOST="smtp.example.com"
|
||||||
|
MAIL_PORT="587"
|
||||||
|
MAIL_USERNAME="your-email@example.com"
|
||||||
|
MAIL_PASSWORD="your-email-password"
|
||||||
|
MAIL_FROM_ADDRESS="noreply@yourapp.com"
|
||||||
|
MAIL_FROM_NAME="Your App Name"
|
||||||
|
|
||||||
|
# Development
|
||||||
|
NODE_ENV="development"
|
||||||
|
NUXT_HOST="localhost"
|
||||||
|
NUXT_PORT="3000"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Database Setup
|
||||||
|
|
||||||
|
corradAF uses Prisma as the ORM. Follow these steps:
|
||||||
|
|
||||||
|
1. **Configure your database URL** in the `.env` file
|
||||||
|
2. **Run database setup**:
|
||||||
|
```bash
|
||||||
|
yarn prisma
|
||||||
|
# This runs: npx prisma db pull && npx prisma generate && nuxt dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. First Run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install dependencies
|
||||||
|
yarn install
|
||||||
|
|
||||||
|
# Start development server
|
||||||
|
yarn dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 Development Tools Access
|
||||||
|
|
||||||
|
After setup, you can access these development tools:
|
||||||
|
|
||||||
|
- **User Management**: `/devtool/user-management/user`
|
||||||
|
- **Menu Editor**: `/devtool/menu-editor`
|
||||||
|
- **API Editor**: `/devtool/api-editor`
|
||||||
|
- **Content Editor**: `/devtool/content-editor`
|
||||||
|
- **Code Playground**: `/devtool/code-playground`
|
||||||
|
- **ORM Tools**: `/devtool/orm`
|
||||||
|
- **Configuration**: `/devtool/config`
|
||||||
|
|
||||||
|
## 🎨 Customization
|
||||||
|
|
||||||
|
### Update Branding
|
||||||
|
|
||||||
|
1. **App Name**: Update in `.env` file (`APP_NAME`)
|
||||||
|
2. **Colors**: Modify `tailwind.config.js`
|
||||||
|
3. **Logo**: Replace files in `public/` directory
|
||||||
|
4. **Favicon**: Replace `public/favicon.ico`
|
||||||
|
|
||||||
|
### Navigation Structure
|
||||||
|
|
||||||
|
Edit the navigation configuration in `navigation/` directory to customize menus.
|
||||||
|
|
||||||
|
### Authentication
|
||||||
|
|
||||||
|
The authentication system is ready to use with:
|
||||||
|
- User registration at `/register`
|
||||||
|
- Login at `/login`
|
||||||
|
- Password recovery at `/forgot-password`
|
||||||
|
|
||||||
|
## 📦 Production Deployment
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
Ensure these production environment variables are set:
|
||||||
|
|
||||||
|
```env
|
||||||
|
NODE_ENV="production"
|
||||||
|
DATABASE_URL="your-production-database-url"
|
||||||
|
JWT_SECRET="your-production-jwt-secret"
|
||||||
|
AUTH_ORIGIN="https://yourdomain.com"
|
||||||
|
NUXT_SECRET_KEY="your-production-secret-key"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build for production
|
||||||
|
yarn build
|
||||||
|
|
||||||
|
# Preview production build locally
|
||||||
|
yarn preview
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🛠️ Extending the Framework
|
||||||
|
|
||||||
|
### Adding New Development Tools
|
||||||
|
|
||||||
|
1. Create new page in `pages/devtool/your-tool/`
|
||||||
|
2. Add navigation entry
|
||||||
|
3. Implement functionality
|
||||||
|
|
||||||
|
### Custom Components
|
||||||
|
|
||||||
|
Add your custom components in `components/` directory following the existing structure.
|
||||||
|
|
||||||
|
### API Endpoints
|
||||||
|
|
||||||
|
Create server routes in `server/api/` directory.
|
||||||
|
|
||||||
|
## 🐛 Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
1. **Database Connection Issues**: Verify DATABASE_URL in `.env`
|
||||||
|
2. **Authentication Problems**: Check JWT_SECRET configuration
|
||||||
|
3. **Build Errors**: Ensure all dependencies are installed with `yarn install`
|
||||||
|
4. **Port Conflicts**: Change NUXT_PORT in `.env` file
|
||||||
|
|
||||||
|
### Getting Help
|
||||||
|
|
||||||
|
- Check the main README.md for detailed documentation
|
||||||
|
- Review existing development tools for implementation examples
|
||||||
|
- Create issues in the repository for bugs or feature requests
|
||||||
|
|
||||||
|
## 📝 Next Steps
|
||||||
|
|
||||||
|
After setup:
|
||||||
|
|
||||||
|
1. Customize the dashboard welcome page with your project branding
|
||||||
|
2. Set up your project-specific features
|
||||||
|
3. Configure authentication and user roles
|
||||||
|
4. Start building your application features
|
||||||
|
|
||||||
|
Happy coding with corradAF! 🚀
|
589
llms.txt
Normal file
589
llms.txt
Normal file
@ -0,0 +1,589 @@
|
|||||||
|
# corradAF Framework - LLM Interaction Guide
|
||||||
|
|
||||||
|
## Metadata
|
||||||
|
- **Framework Name**: corradAF (Corrad Application Framework)
|
||||||
|
- **Framework Version**: 1.0.0
|
||||||
|
- **License**: MIT License
|
||||||
|
- **Base Technology**: Nuxt 3, Vue 3, TypeScript/JavaScript
|
||||||
|
- **UI Framework**: TailwindCSS with custom components
|
||||||
|
- **Database ORM**: Prisma
|
||||||
|
- **Supported Languages**: English (primary), multi-language ready
|
||||||
|
- **Input Format**: Text, Markdown, JSON for API specifications
|
||||||
|
- **Output Format**: Vue SFC components, TypeScript/JavaScript, JSON configurations
|
||||||
|
|
||||||
|
## Framework Overview
|
||||||
|
|
||||||
|
### Introduction:
|
||||||
|
corradAF is a comprehensive Nuxt.js template designed for rapid application development. It provides a complete development tools suite, authentication system, and modern UI components. This document defines interaction patterns for LLMs working with the corradAF framework.
|
||||||
|
|
||||||
|
### Core Architecture:
|
||||||
|
- **Frontend**: Nuxt 3 with Vue 3 Composition API
|
||||||
|
- **Styling**: TailwindCSS with custom component library
|
||||||
|
- **State Management**: Pinia stores
|
||||||
|
- **Authentication**: JWT-based with middleware protection
|
||||||
|
- **Database**: Prisma ORM with PostgreSQL (configurable)
|
||||||
|
- **Development Tools**: Built-in suite for rapid development
|
||||||
|
|
||||||
|
## Content Structure Guidelines
|
||||||
|
|
||||||
|
### File Organization:
|
||||||
|
```
|
||||||
|
├── pages/ # Route pages (Nuxt auto-routing)
|
||||||
|
├── components/ # Reusable Vue components
|
||||||
|
├── composables/ # Vue composables for shared logic
|
||||||
|
├── layouts/ # Application layouts
|
||||||
|
├── middleware/ # Route middleware (auth, permissions)
|
||||||
|
├── server/api/ # Server-side API endpoints
|
||||||
|
├── stores/ # Pinia state management
|
||||||
|
├── assets/ # Static assets and styles
|
||||||
|
├── public/ # Public static files
|
||||||
|
├── prisma/ # Database schema and migrations
|
||||||
|
└── devtool/ # Development tools pages
|
||||||
|
```
|
||||||
|
|
||||||
|
### Component Naming Conventions:
|
||||||
|
- **Pages**: Use kebab-case for file names (e.g., `user-management.vue`)
|
||||||
|
- **Components**: Use PascalCase for component names (e.g., `UserCard.vue`)
|
||||||
|
- **Composables**: Prefix with `use` (e.g., `useUserManagement.js`)
|
||||||
|
- **Stores**: Use camelCase with descriptive names (e.g., `userStore.js`)
|
||||||
|
|
||||||
|
## Writing Guidelines for LLMs
|
||||||
|
|
||||||
|
### Controlled Vocabulary:
|
||||||
|
- Use `navigateTo()` instead of `router.push()` for navigation (Nuxt 3 pattern)
|
||||||
|
- Use `definePageMeta()` for page configuration
|
||||||
|
- Use `ref()` and `reactive()` for Vue 3 Composition API
|
||||||
|
- Use `composables` instead of `mixins` for shared logic
|
||||||
|
- Use `middleware` for route protection and validation
|
||||||
|
|
||||||
|
### Grammar Rules:
|
||||||
|
- **Sentence Length**: Maximum 20 words for code comments
|
||||||
|
- **Active Voice**: Prefer active voice in documentation
|
||||||
|
- **Function Names**: Use camelCase with descriptive verbs
|
||||||
|
- **Variable Names**: Use camelCase with clear, descriptive nouns
|
||||||
|
|
||||||
|
### Code Style Guidelines:
|
||||||
|
- **Vue SFC Structure**: `<script setup>`, `<template>`, `<style>` order
|
||||||
|
- **Import Order**: Vue imports, Nuxt imports, local imports, type imports
|
||||||
|
- **Function Declaration**: Use `function` keyword for named functions, arrow functions for callbacks
|
||||||
|
- **Async/Await**: Prefer async/await over promises for better readability
|
||||||
|
|
||||||
|
## Component Templates
|
||||||
|
|
||||||
|
### Standard Vue SFC Component Template:
|
||||||
|
```vue
|
||||||
|
<script setup>
|
||||||
|
// Imports
|
||||||
|
import { ref, computed, onMounted } from 'vue'
|
||||||
|
|
||||||
|
// Props and emits
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: {
|
||||||
|
type: [String, Number, Boolean],
|
||||||
|
default: null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue', 'change'])
|
||||||
|
|
||||||
|
// Reactive data
|
||||||
|
const isLoading = ref(false)
|
||||||
|
const data = ref([])
|
||||||
|
|
||||||
|
// Computed properties
|
||||||
|
const processedData = computed(() => {
|
||||||
|
return data.value.filter(item => item.active)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
function handleUpdate(value) {
|
||||||
|
emit('update:modelValue', value)
|
||||||
|
emit('change', value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lifecycle
|
||||||
|
onMounted(async () => {
|
||||||
|
await loadData()
|
||||||
|
})
|
||||||
|
|
||||||
|
async function loadData() {
|
||||||
|
isLoading.value = true
|
||||||
|
try {
|
||||||
|
// Load data logic
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading data:', error)
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="component-wrapper">
|
||||||
|
<!-- Component content -->
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* Component-specific styles */
|
||||||
|
</style>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Page Component Template:
|
||||||
|
```vue
|
||||||
|
<script setup>
|
||||||
|
definePageMeta({
|
||||||
|
title: "Page Title",
|
||||||
|
middleware: ["auth"], // Add middleware as needed
|
||||||
|
requiresAuth: true,
|
||||||
|
breadcrumb: [
|
||||||
|
{
|
||||||
|
name: "Dashboard",
|
||||||
|
path: "/dashboard",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Current Page",
|
||||||
|
path: "/current-page",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
// Page-specific logic
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="page-container">
|
||||||
|
<LayoutsBreadcrumb />
|
||||||
|
|
||||||
|
<!-- Page content -->
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Development Tool Page Template:
|
||||||
|
```vue
|
||||||
|
<script setup>
|
||||||
|
definePageMeta({
|
||||||
|
title: "Tool Name",
|
||||||
|
middleware: ["auth", "dev-tools"],
|
||||||
|
requiresAuth: true,
|
||||||
|
layout: "devtool",
|
||||||
|
breadcrumb: [
|
||||||
|
{
|
||||||
|
name: "Dashboard",
|
||||||
|
path: "/dashboard",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Development Tools",
|
||||||
|
path: "/devtool",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Tool Name",
|
||||||
|
path: "/devtool/tool-name",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
// Tool-specific functionality
|
||||||
|
const toolData = ref([])
|
||||||
|
const isLoading = ref(false)
|
||||||
|
|
||||||
|
// CRUD operations
|
||||||
|
async function createItem(data) {
|
||||||
|
// Implementation
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateItem(id, data) {
|
||||||
|
// Implementation
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteItem(id) {
|
||||||
|
// Implementation
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="devtool-container space-y-6">
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<h1 class="text-2xl font-bold text-primary">Tool Name</h1>
|
||||||
|
<rs-button variant="primary" @click="createNew">
|
||||||
|
<Icon name="mdi:plus" size="18" />
|
||||||
|
Add New
|
||||||
|
</rs-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tool interface -->
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Endpoint Templates
|
||||||
|
|
||||||
|
### Server API Route Template:
|
||||||
|
```typescript
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
// Validate authentication
|
||||||
|
const user = await requireUserSession(event)
|
||||||
|
|
||||||
|
// Handle different HTTP methods
|
||||||
|
const method = getMethod(event)
|
||||||
|
|
||||||
|
if (method === 'GET') {
|
||||||
|
// GET logic
|
||||||
|
return { data: [] }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (method === 'POST') {
|
||||||
|
// POST logic
|
||||||
|
const body = await readBody(event)
|
||||||
|
|
||||||
|
// Validate input
|
||||||
|
if (!body.name) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 400,
|
||||||
|
statusMessage: 'Name is required'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process request
|
||||||
|
return { success: true, data: body }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle other methods
|
||||||
|
throw createError({
|
||||||
|
statusCode: 405,
|
||||||
|
statusMessage: 'Method not allowed'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### API Response Format:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {},
|
||||||
|
"message": "Operation completed successfully",
|
||||||
|
"meta": {
|
||||||
|
"timestamp": "2024-01-01T00:00:00Z",
|
||||||
|
"version": "1.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error Response Format:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"error": {
|
||||||
|
"code": "VALIDATION_ERROR",
|
||||||
|
"message": "Invalid input provided",
|
||||||
|
"details": {}
|
||||||
|
},
|
||||||
|
"meta": {
|
||||||
|
"timestamp": "2024-01-01T00:00:00Z",
|
||||||
|
"version": "1.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Database Schema Patterns
|
||||||
|
|
||||||
|
### Prisma Model Template:
|
||||||
|
```prisma
|
||||||
|
model User {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
email String @unique
|
||||||
|
name String?
|
||||||
|
role Role @default(USER)
|
||||||
|
isActive Boolean @default(true)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
profile UserProfile?
|
||||||
|
posts Post[]
|
||||||
|
|
||||||
|
@@map("users")
|
||||||
|
}
|
||||||
|
|
||||||
|
model UserProfile {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
userId String @unique
|
||||||
|
avatar String?
|
||||||
|
bio String?
|
||||||
|
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@map("user_profiles")
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Role {
|
||||||
|
USER
|
||||||
|
ADMIN
|
||||||
|
DEVELOPER
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## TailwindCSS and Styling Guidelines
|
||||||
|
|
||||||
|
### Component Styling Patterns:
|
||||||
|
- **Cards**: Use `rs-card` component or `bg-white rounded-lg shadow-sm border`
|
||||||
|
- **Buttons**: Use `rs-button` component with variants (primary, secondary, danger)
|
||||||
|
- **Forms**: Use FormKit components with TailwindCSS utilities
|
||||||
|
- **Grid Layouts**: Use `grid grid-cols-{n} gap-{size}` for responsive layouts
|
||||||
|
- **Spacing**: Use consistent spacing scale (4, 6, 8, 12, 16, 24)
|
||||||
|
|
||||||
|
### Color Scheme:
|
||||||
|
- **Primary**: `text-primary`, `bg-primary`
|
||||||
|
- **Secondary**: `text-secondary`, `bg-secondary`
|
||||||
|
- **Success**: `text-green-600`, `bg-green-100`
|
||||||
|
- **Warning**: `text-yellow-600`, `bg-yellow-100`
|
||||||
|
- **Danger**: `text-red-600`, `bg-red-100`
|
||||||
|
|
||||||
|
## Authentication and Security Patterns
|
||||||
|
|
||||||
|
### Route Protection:
|
||||||
|
```javascript
|
||||||
|
// middleware/auth.js
|
||||||
|
export default defineNuxtRouteMiddleware((to, from) => {
|
||||||
|
const { $auth } = useNuxtApp()
|
||||||
|
|
||||||
|
if (!$auth.user) {
|
||||||
|
return navigateTo('/login')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// middleware/dev-tools.js
|
||||||
|
export default defineNuxtRouteMiddleware((to, from) => {
|
||||||
|
const { $auth } = useNuxtApp()
|
||||||
|
|
||||||
|
if (!$auth.user?.isDeveloper) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 403,
|
||||||
|
statusMessage: 'Access forbidden'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### API Security:
|
||||||
|
```typescript
|
||||||
|
// server/utils/auth.ts
|
||||||
|
export async function requireUserSession(event) {
|
||||||
|
const token = getCookie(event, 'auth-token') || getHeader(event, 'authorization')
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 401,
|
||||||
|
statusMessage: 'Authentication required'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const user = await verifyJWT(token)
|
||||||
|
return user
|
||||||
|
} catch (error) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 401,
|
||||||
|
statusMessage: 'Invalid token'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development Tools Integration
|
||||||
|
|
||||||
|
### Adding New Development Tools:
|
||||||
|
1. Create page in `pages/devtool/tool-name/`
|
||||||
|
2. Add navigation entry in navigation configuration
|
||||||
|
3. Implement CRUD operations
|
||||||
|
4. Add proper middleware and permissions
|
||||||
|
5. Follow established UI patterns
|
||||||
|
|
||||||
|
### Tool Configuration Template:
|
||||||
|
```javascript
|
||||||
|
// devtool configuration
|
||||||
|
export const toolConfig = {
|
||||||
|
name: 'Tool Name',
|
||||||
|
description: 'Tool description',
|
||||||
|
icon: 'mdi:tool-icon',
|
||||||
|
path: '/devtool/tool-name',
|
||||||
|
permissions: ['developer', 'admin'],
|
||||||
|
features: [
|
||||||
|
'create',
|
||||||
|
'read',
|
||||||
|
'update',
|
||||||
|
'delete',
|
||||||
|
'export',
|
||||||
|
'import'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Input/Output Specifications for Code Generation
|
||||||
|
|
||||||
|
### Request Format for Component Generation:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"task": "component_generation",
|
||||||
|
"type": "vue_component",
|
||||||
|
"specifications": {
|
||||||
|
"name": "UserCard",
|
||||||
|
"props": ["user", "showActions"],
|
||||||
|
"events": ["edit", "delete"],
|
||||||
|
"features": ["responsive", "accessible"],
|
||||||
|
"styling": "tailwindcss"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Response Format:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"generated_code": "<!-- Vue component code -->",
|
||||||
|
"file_path": "components/UserCard.vue",
|
||||||
|
"dependencies": ["vue", "@nuxt/icon"],
|
||||||
|
"usage_example": "<UserCard :user=\"user\" @edit=\"handleEdit\" />",
|
||||||
|
"explanation": "Generated a responsive user card component with edit and delete actions."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling Patterns
|
||||||
|
|
||||||
|
### Frontend Error Handling:
|
||||||
|
```javascript
|
||||||
|
// composables/useErrorHandler.js
|
||||||
|
export const useErrorHandler = () => {
|
||||||
|
const handleError = (error, context = '') => {
|
||||||
|
console.error(`Error in ${context}:`, error)
|
||||||
|
|
||||||
|
// Show user-friendly message
|
||||||
|
const { $toast } = useNuxtApp()
|
||||||
|
$toast.error('An error occurred. Please try again.')
|
||||||
|
|
||||||
|
// Log to monitoring service if available
|
||||||
|
if (process.env.NODE_ENV === 'production') {
|
||||||
|
// Log to external service
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { handleError }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### API Error Handling:
|
||||||
|
```typescript
|
||||||
|
// server/utils/errorHandler.ts
|
||||||
|
export function handleAPIError(error: unknown) {
|
||||||
|
if (error instanceof ValidationError) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 400,
|
||||||
|
statusMessage: 'Validation failed',
|
||||||
|
data: error.details
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error instanceof DatabaseError) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 500,
|
||||||
|
statusMessage: 'Database operation failed'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generic error
|
||||||
|
throw createError({
|
||||||
|
statusCode: 500,
|
||||||
|
statusMessage: 'Internal server error'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Patterns
|
||||||
|
|
||||||
|
### Component Testing Template:
|
||||||
|
```javascript
|
||||||
|
// tests/components/UserCard.test.js
|
||||||
|
import { mount } from '@vue/test-utils'
|
||||||
|
import UserCard from '~/components/UserCard.vue'
|
||||||
|
|
||||||
|
describe('UserCard', () => {
|
||||||
|
const mockUser = {
|
||||||
|
id: '1',
|
||||||
|
name: 'John Doe',
|
||||||
|
email: 'john@example.com'
|
||||||
|
}
|
||||||
|
|
||||||
|
it('renders user information correctly', () => {
|
||||||
|
const wrapper = mount(UserCard, {
|
||||||
|
props: { user: mockUser }
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(wrapper.text()).toContain('John Doe')
|
||||||
|
expect(wrapper.text()).toContain('john@example.com')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('emits edit event when edit button is clicked', async () => {
|
||||||
|
const wrapper = mount(UserCard, {
|
||||||
|
props: { user: mockUser, showActions: true }
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.find('[data-test="edit-button"]').trigger('click')
|
||||||
|
|
||||||
|
expect(wrapper.emitted('edit')).toBeTruthy()
|
||||||
|
expect(wrapper.emitted('edit')[0]).toEqual([mockUser])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Ethical Guidelines and Security Considerations
|
||||||
|
|
||||||
|
### Security Best Practices:
|
||||||
|
- **Input Validation**: Always validate and sanitize user inputs
|
||||||
|
- **SQL Injection Prevention**: Use Prisma ORM parameterized queries
|
||||||
|
- **XSS Prevention**: Use Vue's built-in template sanitization
|
||||||
|
- **CSRF Protection**: Implement CSRF tokens for state-changing operations
|
||||||
|
- **Authentication**: Use secure JWT implementation with proper expiration
|
||||||
|
- **Authorization**: Implement role-based access control (RBAC)
|
||||||
|
|
||||||
|
### Ethical Considerations:
|
||||||
|
- **Accessibility**: Follow WCAG 2.1 guidelines for component generation
|
||||||
|
- **Privacy**: Implement data protection and GDPR compliance patterns
|
||||||
|
- **Bias Prevention**: Ensure generated content is inclusive and unbiased
|
||||||
|
- **Performance**: Generate efficient code that doesn't compromise user experience
|
||||||
|
|
||||||
|
### Data Handling:
|
||||||
|
- **Encryption**: Use encryption for sensitive data storage
|
||||||
|
- **Logging**: Log security events without exposing sensitive information
|
||||||
|
- **Audit Trail**: Maintain audit logs for critical operations
|
||||||
|
- **Data Retention**: Implement proper data retention and deletion policies
|
||||||
|
|
||||||
|
## Performance Guidelines
|
||||||
|
|
||||||
|
### Code Generation Best Practices:
|
||||||
|
- **Lazy Loading**: Use dynamic imports for large components
|
||||||
|
- **Tree Shaking**: Write code that supports tree shaking
|
||||||
|
- **Bundle Size**: Consider bundle size impact in generated code
|
||||||
|
- **Caching**: Implement appropriate caching strategies
|
||||||
|
- **Database Queries**: Use efficient Prisma queries with proper relations
|
||||||
|
|
||||||
|
### Optimization Patterns:
|
||||||
|
```javascript
|
||||||
|
// Lazy component loading
|
||||||
|
const LazyComponent = defineAsyncComponent(() => import('~/components/HeavyComponent.vue'))
|
||||||
|
|
||||||
|
// Efficient data fetching
|
||||||
|
const { data, pending, error } = await useLazyFetch('/api/data', {
|
||||||
|
key: 'unique-key',
|
||||||
|
server: false // Client-side only if needed
|
||||||
|
})
|
||||||
|
|
||||||
|
// Computed optimization
|
||||||
|
const expensiveComputation = computed(() => {
|
||||||
|
return useMemo(() => heavyOperation(props.data), [props.data])
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
This document serves as a comprehensive guide for LLMs working with the corradAF framework. It ensures consistent code generation, proper architecture patterns, and adherence to best practices for security, performance, and maintainability.
|
25
package.json
25
package.json
@ -1,4 +1,29 @@
|
|||||||
{
|
{
|
||||||
|
"name": "corrad-af",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Corrad Application Framework - A comprehensive Nuxt.js template for rapid application development",
|
||||||
|
"author": "Corrad Team",
|
||||||
|
"license": "MIT",
|
||||||
|
"keywords": [
|
||||||
|
"nuxt",
|
||||||
|
"vue",
|
||||||
|
"typescript",
|
||||||
|
"tailwindcss",
|
||||||
|
"template",
|
||||||
|
"framework",
|
||||||
|
"development-tools",
|
||||||
|
"admin-panel",
|
||||||
|
"cms",
|
||||||
|
"authentication"
|
||||||
|
],
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/your-org/corrad-af"
|
||||||
|
},
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/your-org/corrad-af/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/your-org/corrad-af#readme",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "nuxt build",
|
"build": "nuxt build",
|
||||||
|
@ -1,674 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { ref, computed, onMounted } from 'vue';
|
|
||||||
import { useAsnafMockData } from '~/composables/useAsnafMockData';
|
|
||||||
import { useToast } from 'vue-toastification';
|
|
||||||
|
|
||||||
const route = useRoute();
|
|
||||||
const toast = useToast();
|
|
||||||
|
|
||||||
const { getProfileById } = useAsnafMockData();
|
|
||||||
const profile = ref(null);
|
|
||||||
const loading = ref(true);
|
|
||||||
const isLoadingAnalysis = ref(false);
|
|
||||||
const confirmDelete = ref(false);
|
|
||||||
const activeTab = ref('personal');
|
|
||||||
|
|
||||||
// Load profile data
|
|
||||||
onMounted(async () => {
|
|
||||||
const id = route.params.id;
|
|
||||||
loading.value = true;
|
|
||||||
const fetchedProfile = getProfileById(id);
|
|
||||||
|
|
||||||
if (fetchedProfile) {
|
|
||||||
profile.value = { ...fetchedProfile, analysis: null };
|
|
||||||
loading.value = false;
|
|
||||||
} else {
|
|
||||||
toast.error('Profil tidak dijumpai');
|
|
||||||
navigateTo('/BF-PRF/AS/LIST');
|
|
||||||
loading.value = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// New function to be called by a button
|
|
||||||
async function fetchAIAnalysis() {
|
|
||||||
if (!profile.value) {
|
|
||||||
toast.error('Profil data tidak tersedia untuk analisis.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
isLoadingAnalysis.value = true;
|
|
||||||
try {
|
|
||||||
const requestBody = {
|
|
||||||
monthlyIncome: profile.value.monthlyIncome,
|
|
||||||
otherIncome: profile.value.otherIncome,
|
|
||||||
totalIncome: profile.value.totalIncome,
|
|
||||||
occupation: profile.value.occupation,
|
|
||||||
maritalStatus: profile.value.maritalStatus,
|
|
||||||
dependents: profile.value.dependents,
|
|
||||||
// Add other fields you want to send for analysis here
|
|
||||||
};
|
|
||||||
|
|
||||||
const analysisResponse = await $fetch('/api/analyze-asnaf', {
|
|
||||||
method: 'POST',
|
|
||||||
body: requestBody,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (profile.value) {
|
|
||||||
profile.value.analysis = analysisResponse;
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error fetching AI Analysis from /api/analyze-asnaf:", error);
|
|
||||||
toast.error('Gagal memuat analisis AI dari server.');
|
|
||||||
if (profile.value) {
|
|
||||||
profile.value.analysis = {
|
|
||||||
hadKifayahPercentage: 'Ralat',
|
|
||||||
kategoriAsnaf: 'Ralat Server',
|
|
||||||
kategoriKeluarga: 'Ralat Server',
|
|
||||||
cadanganKategori: 'Ralat Server',
|
|
||||||
statusKelayakan: 'Ralat Server',
|
|
||||||
cadanganBantuan: [{ nama: 'Tidak dapat memuat cadangan bantuan', peratusan: 'Ralat' }],
|
|
||||||
ramalanJangkaMasaPulih: 'Ralat Server',
|
|
||||||
rumusan: 'Ralat Server'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
isLoadingAnalysis.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Computed status color
|
|
||||||
const statusColor = computed(() => {
|
|
||||||
if (!profile.value) return '';
|
|
||||||
|
|
||||||
switch (profile.value.status) {
|
|
||||||
case 'Aktif': return 'success';
|
|
||||||
case 'Tidak Aktif': return 'danger';
|
|
||||||
case 'Dalam Semakan': return 'warning';
|
|
||||||
default: return 'secondary';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Computed category color
|
|
||||||
const categoryColor = computed(() => {
|
|
||||||
if (!profile.value) return '';
|
|
||||||
|
|
||||||
switch (profile.value.kategori) {
|
|
||||||
case 'Fakir': return 'danger';
|
|
||||||
case 'Miskin': return 'warning';
|
|
||||||
case 'Mualaf': return 'info';
|
|
||||||
case 'Fi-sabilillah': return 'primary';
|
|
||||||
case 'Gharimin': return 'secondary';
|
|
||||||
case 'Ibnu Sabil': return 'success';
|
|
||||||
default: return 'primary';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Page metadata
|
|
||||||
definePageMeta({
|
|
||||||
title: "Maklumat Asnaf",
|
|
||||||
middleware: ["auth"],
|
|
||||||
requiresAuth: true,
|
|
||||||
breadcrumb: [
|
|
||||||
{
|
|
||||||
name: "Dashboard",
|
|
||||||
path: "/",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "BF-PRF",
|
|
||||||
path: "/BF-PRF",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Asnaf",
|
|
||||||
path: "/BF-PRF/AS",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Senarai",
|
|
||||||
path: "/BF-PRF/AS/LIST",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Maklumat",
|
|
||||||
path: "/BF-PRF/AS/DETAIL",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
// Navigation functions
|
|
||||||
function navigateToList() {
|
|
||||||
navigateTo('/BF-PRF/AS/LIST');
|
|
||||||
}
|
|
||||||
|
|
||||||
function navigateToEdit() {
|
|
||||||
navigateTo(`/BF-PRF/AS/UP/01?id=${profile.value.id}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleDelete() {
|
|
||||||
confirmDelete.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function confirmDeleteProfile() {
|
|
||||||
toast.success('Profil telah dipadamkan');
|
|
||||||
navigateTo('/BF-PRF/AS/LIST');
|
|
||||||
confirmDelete.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function cancelDelete() {
|
|
||||||
confirmDelete.value = false;
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="space-y-6">
|
|
||||||
<LayoutsBreadcrumb />
|
|
||||||
|
|
||||||
<!-- Loading state -->
|
|
||||||
<div v-if="loading" class="flex justify-center items-center py-20">
|
|
||||||
<div class="text-center">
|
|
||||||
<Loading />
|
|
||||||
<p class="mt-4 text-gray-600">Memuat maklumat asnaf...</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else>
|
|
||||||
<!-- Header with actions -->
|
|
||||||
<div class="flex flex-col md:flex-row md:justify-between md:items-center gap-4 mb-6">
|
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
<h1 class="text-2xl font-bold text-primary">{{ profile.nama }}</h1>
|
|
||||||
<rs-badge :variant="statusColor">{{ profile.status }}</rs-badge>
|
|
||||||
<rs-badge :variant="categoryColor">{{ profile.kategori }}</rs-badge>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<rs-button variant="secondary-outline" @click="navigateToList">
|
|
||||||
<Icon name="mdi:arrow-left" size="18" class="mr-1" />
|
|
||||||
Kembali
|
|
||||||
</rs-button>
|
|
||||||
|
|
||||||
<rs-button variant="primary" @click="navigateToEdit">
|
|
||||||
<Icon name="mdi:pencil" size="18" class="mr-1" />
|
|
||||||
Kemaskini
|
|
||||||
</rs-button>
|
|
||||||
|
|
||||||
<rs-button variant="danger" @click="handleDelete">
|
|
||||||
<Icon name="mdi:delete" size="18" class="mr-1" />
|
|
||||||
Padam
|
|
||||||
</rs-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Profile Overview -->
|
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-6">
|
|
||||||
<!-- Profile Photo and Basic Info -->
|
|
||||||
<rs-card class="lg:col-span-1">
|
|
||||||
<div class="p-6 flex flex-col items-center">
|
|
||||||
<div class="w-32 h-32 rounded-full bg-gray-200 flex items-center justify-center mb-4 overflow-hidden">
|
|
||||||
<Icon name="mdi:account" size="64" class="text-gray-400" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2 class="text-xl font-semibold text-center">{{ profile.nama }}</h2>
|
|
||||||
<p class="text-gray-500 text-center mb-4">{{ profile.id }}</p>
|
|
||||||
|
|
||||||
<div class="w-full text-center">
|
|
||||||
<rs-badge :variant="categoryColor" class="mb-2">{{ profile.kategori }}</rs-badge>
|
|
||||||
<p class="text-sm text-gray-600">Didaftarkan pada {{ new Date(profile.tarikhDaftar).toLocaleDateString('ms-MY') }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</rs-card>
|
|
||||||
|
|
||||||
<!-- Personal Information -->
|
|
||||||
<rs-card class="lg:col-span-2">
|
|
||||||
<template #header>
|
|
||||||
<div class="px-4 py-3">
|
|
||||||
<h3 class="text-lg font-semibold text-primary flex items-center">
|
|
||||||
<Icon name="mdi:account-details" size="20" class="mr-2" />
|
|
||||||
Maklumat Peribadi
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #body>
|
|
||||||
<div class="p-4">
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-4">
|
|
||||||
<div>
|
|
||||||
<h4 class="text-sm font-medium text-gray-500">No. Kad Pengenalan</h4>
|
|
||||||
<p>{{ profile.idNumber }}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h4 class="text-sm font-medium text-gray-500">Jantina</h4>
|
|
||||||
<p>{{ profile.gender }}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h4 class="text-sm font-medium text-gray-500">Tarikh Lahir</h4>
|
|
||||||
<p>{{ new Date(profile.birthDate).toLocaleDateString('ms-MY') }}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h4 class="text-sm font-medium text-gray-500">Status Perkahwinan</h4>
|
|
||||||
<p>{{ profile.maritalStatus }}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h4 class="text-sm font-medium text-gray-500">Pekerjaan</h4>
|
|
||||||
<p>{{ profile.occupation }}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h4 class="text-sm font-medium text-gray-500">Pendapatan Bulanan</h4>
|
|
||||||
<p>RM {{ profile.monthlyIncome }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</rs-card>
|
|
||||||
|
|
||||||
<!-- Contact Information -->
|
|
||||||
<rs-card class="lg:col-span-3">
|
|
||||||
<template #header>
|
|
||||||
<div class="px-4 py-3">
|
|
||||||
<h3 class="text-lg font-semibold text-primary flex items-center">
|
|
||||||
<Icon name="mdi:contacts" size="20" class="mr-2" />
|
|
||||||
Maklumat Perhubungan
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #body>
|
|
||||||
<div class="p-4">
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-x-6 gap-y-4">
|
|
||||||
<div>
|
|
||||||
<h4 class="text-sm font-medium text-gray-500">Alamat</h4>
|
|
||||||
<p>{{ profile.alamat || 'Tiada' }}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h4 class="text-sm font-medium text-gray-500">No. Telefon</h4>
|
|
||||||
<p>{{ profile.telefon }}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h4 class="text-sm font-medium text-gray-500">Emel</h4>
|
|
||||||
<p>{{ profile.email }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</rs-card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Tabbed Details -->
|
|
||||||
<rs-card>
|
|
||||||
<template #header>
|
|
||||||
<div class="px-4 py-3 border-b">
|
|
||||||
<div class="flex overflow-x-auto space-x-4">
|
|
||||||
<button
|
|
||||||
@click="activeTab = 'personal'"
|
|
||||||
class="py-2 px-1 border-b-2 font-medium text-sm whitespace-nowrap transition-colors"
|
|
||||||
:class="activeTab === 'personal' ? 'border-primary text-primary' : 'border-transparent text-gray-500 hover:text-gray-700'"
|
|
||||||
>
|
|
||||||
<Icon name="mdi:account-group" size="18" class="mr-1" />
|
|
||||||
Maklumat Keluarga
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
@click="activeTab = 'income'"
|
|
||||||
class="py-2 px-1 border-b-2 font-medium text-sm whitespace-nowrap transition-colors"
|
|
||||||
:class="activeTab === 'income' ? 'border-primary text-primary' : 'border-transparent text-gray-500 hover:text-gray-700'"
|
|
||||||
>
|
|
||||||
<Icon name="mdi:cash" size="18" class="mr-1" />
|
|
||||||
Maklumat Pendapatan
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
@click="activeTab = 'aid'"
|
|
||||||
class="py-2 px-1 border-b-2 font-medium text-sm whitespace-nowrap transition-colors"
|
|
||||||
:class="activeTab === 'aid' ? 'border-primary text-primary' : 'border-transparent text-gray-500 hover:text-gray-700'"
|
|
||||||
>
|
|
||||||
<Icon name="mdi:gift" size="18" class="mr-1" />
|
|
||||||
Maklumat Bantuan
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
@click="activeTab = 'documents'"
|
|
||||||
class="py-2 px-1 border-b-2 font-medium text-sm whitespace-nowrap transition-colors"
|
|
||||||
:class="activeTab === 'documents' ? 'border-primary text-primary' : 'border-transparent text-gray-500 hover:text-gray-700'"
|
|
||||||
>
|
|
||||||
<Icon name="mdi:file-document" size="18" class="mr-1" />
|
|
||||||
Dokumen
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
@click="activeTab = 'analysis'"
|
|
||||||
class="py-2 px-1 border-b-2 font-medium text-sm whitespace-nowrap transition-colors"
|
|
||||||
:class="activeTab === 'analysis' ? 'border-primary text-primary' : 'border-transparent text-gray-500 hover:text-gray-700'"
|
|
||||||
>
|
|
||||||
<Icon name="mdi:chart-bar" size="18" class="mr-1" />
|
|
||||||
Analisis Data
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #body>
|
|
||||||
<!-- Family Information Tab -->
|
|
||||||
<div v-if="activeTab === 'personal'" class="p-6">
|
|
||||||
<div v-if="profile.spouse" class="mb-8">
|
|
||||||
<h3 class="text-lg font-semibold text-primary mb-4">Maklumat Pasangan</h3>
|
|
||||||
<div class="bg-gray-50 p-4 rounded-lg">
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<h4 class="text-sm font-medium text-gray-500">Nama</h4>
|
|
||||||
<p>{{ profile.spouse.name }}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h4 class="text-sm font-medium text-gray-500">No. Kad Pengenalan</h4>
|
|
||||||
<p>{{ profile.spouse.idNumber }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h3 class="text-lg font-semibold text-primary mb-4">Tanggungan</h3>
|
|
||||||
|
|
||||||
<div v-if="profile.dependents && profile.dependents.length > 0">
|
|
||||||
<div class="overflow-x-auto">
|
|
||||||
<table class="min-w-full divide-y divide-gray-200">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th class="px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Bil.</th>
|
|
||||||
<th class="px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Nama</th>
|
|
||||||
<th class="px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Umur</th>
|
|
||||||
<th class="px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Hubungan</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody class="bg-white divide-y divide-gray-200">
|
|
||||||
<tr v-for="(dependent, index) in profile.dependents" :key="index" class="hover:bg-gray-50">
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap">{{ index + 1 }}</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap">{{ dependent.name }}</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap">{{ dependent.age }}</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap">{{ dependent.relationship }}</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else class="bg-gray-50 p-6 rounded-lg text-center">
|
|
||||||
<Icon name="mdi:account-off" size="48" class="text-gray-400 mb-2" />
|
|
||||||
<p class="text-gray-500">Tiada tanggungan</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Income Information Tab -->
|
|
||||||
<div v-if="activeTab === 'income'" class="p-6">
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
||||||
<rs-card>
|
|
||||||
<div class="p-4 text-center">
|
|
||||||
<div class="mb-2">
|
|
||||||
<Icon name="mdi:cash-multiple" size="36" class="text-primary" />
|
|
||||||
</div>
|
|
||||||
<p class="text-sm text-gray-500">Pendapatan Bulanan</p>
|
|
||||||
<p class="text-xl font-bold text-primary">RM {{ profile.monthlyIncome }}</p>
|
|
||||||
</div>
|
|
||||||
</rs-card>
|
|
||||||
|
|
||||||
<rs-card>
|
|
||||||
<div class="p-4 text-center">
|
|
||||||
<div class="mb-2">
|
|
||||||
<Icon name="mdi:cash-plus" size="36" class="text-primary" />
|
|
||||||
</div>
|
|
||||||
<p class="text-sm text-gray-500">Pendapatan Lain</p>
|
|
||||||
<p class="text-xl font-bold text-primary">RM {{ profile.otherIncome }}</p>
|
|
||||||
</div>
|
|
||||||
</rs-card>
|
|
||||||
|
|
||||||
<rs-card>
|
|
||||||
<div class="p-4 text-center">
|
|
||||||
<div class="mb-2">
|
|
||||||
<Icon name="mdi:cash-register" size="36" class="text-primary" />
|
|
||||||
</div>
|
|
||||||
<p class="text-sm text-gray-500">Jumlah Pendapatan</p>
|
|
||||||
<p class="text-xl font-bold text-primary">RM {{ profile.totalIncome }}</p>
|
|
||||||
</div>
|
|
||||||
</rs-card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-6">
|
|
||||||
<h3 class="text-lg font-semibold text-primary mb-4">Butiran Pendapatan</h3>
|
|
||||||
<div class="bg-gray-50 p-4 rounded-lg">
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<h4 class="text-sm font-medium text-gray-500">Pekerjaan</h4>
|
|
||||||
<p>{{ profile.occupation }}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h4 class="text-sm font-medium text-gray-500">Pendapatan Bulanan</h4>
|
|
||||||
<p>RM {{ profile.monthlyIncome }}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h4 class="text-sm font-medium text-gray-500">Pendapatan Lain</h4>
|
|
||||||
<p>RM {{ profile.otherIncome }}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h4 class="text-sm font-medium text-gray-500">Jumlah Pendapatan</h4>
|
|
||||||
<p>RM {{ profile.totalIncome }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Aid Information Tab -->
|
|
||||||
<div v-if="activeTab === 'aid'" class="p-6">
|
|
||||||
<div class="bg-gray-50 p-6 rounded-lg text-center">
|
|
||||||
<Icon name="mdi:gift-off" size="48" class="text-gray-400 mb-2" />
|
|
||||||
<p class="text-gray-500">Tiada maklumat bantuan</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Documents Tab -->
|
|
||||||
<div v-if="activeTab === 'documents'" class="p-6">
|
|
||||||
<h3 class="text-lg font-semibold text-primary mb-4">Dokumen Sokongan</h3>
|
|
||||||
|
|
||||||
<div v-if="profile.documents && profile.documents.length > 0">
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
||||||
<div v-for="(doc, index) in profile.documents" :key="index" class="border rounded-lg overflow-hidden">
|
|
||||||
<div class="bg-gray-50 p-4 flex items-center">
|
|
||||||
<Icon name="mdi:file-document" size="24" class="text-primary mr-3" />
|
|
||||||
<div>
|
|
||||||
<h4 class="font-medium">{{ doc.name }}</h4>
|
|
||||||
<p class="text-sm text-gray-500">{{ doc.size }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="p-3 flex justify-end">
|
|
||||||
<rs-button variant="primary-text" size="sm">
|
|
||||||
<Icon name="mdi:download" size="16" class="mr-1" />
|
|
||||||
Muat Turun
|
|
||||||
</rs-button>
|
|
||||||
<rs-button variant="secondary-text" size="sm">
|
|
||||||
<Icon name="mdi:eye" size="16" class="mr-1" />
|
|
||||||
Papar
|
|
||||||
</rs-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else class="bg-gray-50 p-6 rounded-lg text-center">
|
|
||||||
<Icon name="mdi:file-document-off" size="48" class="text-gray-400 mb-2" />
|
|
||||||
<p class="text-gray-500">Tiada dokumen</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Analysis Tab -->
|
|
||||||
<div v-if="activeTab === 'analysis'" class="p-6">
|
|
||||||
<!-- Button to trigger AI Analysis -->
|
|
||||||
<div v-if="!profile.analysis && !isLoadingAnalysis" class="text-center mb-6">
|
|
||||||
<rs-button variant="primary" @click="fetchAIAnalysis" size="lg">
|
|
||||||
<Icon name="mdi:brain" size="20" class="mr-2" />
|
|
||||||
Jalankan Analisis AI
|
|
||||||
</rs-button>
|
|
||||||
<p class="text-sm text-gray-500 mt-2">Klik untuk mendapatkan penilaian berdasarkan data profil.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Loading State for AI Analysis -->
|
|
||||||
<div v-if="isLoadingAnalysis" class="text-center py-10">
|
|
||||||
<Loading />
|
|
||||||
<p class="mt-4 text-gray-600">Analisis AI sedang dijalankan...</p>
|
|
||||||
<p class="text-sm text-gray-500">Sila tunggu sebentar.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Display Analysis Results -->
|
|
||||||
<div v-if="profile.analysis && !isLoadingAnalysis" class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
||||||
|
|
||||||
<!-- AI Analysis Main Column (takes 2/3 on lg screens) -->
|
|
||||||
<div class="lg:col-span-2 space-y-6">
|
|
||||||
<!-- Card 1: Analisis Had Kifayah & Kelayakan -->
|
|
||||||
<rs-card>
|
|
||||||
<template #header>
|
|
||||||
<h3 class="text-lg font-semibold text-primary p-4 border-b">Analisis Had Kifayah & Kelayakan (AI)</h3>
|
|
||||||
</template>
|
|
||||||
<template #body>
|
|
||||||
<div class="p-4 space-y-4">
|
|
||||||
<div>
|
|
||||||
<h4 class="text-sm font-medium text-gray-500 mb-1">Peratusan Had Kifayah</h4>
|
|
||||||
<div v-if="profile.analysis.hadKifayahPercentage === 'N/A' || profile.analysis.hadKifayahPercentage === 'Ralat'" class="text-gray-500">
|
|
||||||
{{ profile.analysis.hadKifayahPercentage }}
|
|
||||||
</div>
|
|
||||||
<div v-else class="relative pt-1">
|
|
||||||
<div class="overflow-hidden h-4 text-xs flex rounded bg-gray-200">
|
|
||||||
<div
|
|
||||||
:style="{ width: profile.analysis.hadKifayahPercentage }"
|
|
||||||
:class="{
|
|
||||||
'bg-red-500': parseInt(profile.analysis.hadKifayahPercentage) < 60,
|
|
||||||
'bg-yellow-500': parseInt(profile.analysis.hadKifayahPercentage) >= 60 && parseInt(profile.analysis.hadKifayahPercentage) < 80,
|
|
||||||
'bg-green-500': parseInt(profile.analysis.hadKifayahPercentage) >= 80 && parseInt(profile.analysis.hadKifayahPercentage) <= 100,
|
|
||||||
'bg-blue-500': parseInt(profile.analysis.hadKifayahPercentage) > 100
|
|
||||||
}"
|
|
||||||
class="shadow-none flex flex-col text-center whitespace-nowrap text-white justify-center"
|
|
||||||
>
|
|
||||||
{{ profile.analysis.hadKifayahPercentage }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h4 class="text-sm font-medium text-gray-500">Kategori Asnaf (AI)</h4>
|
|
||||||
<p>{{ profile.analysis.kategoriAsnaf }}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h4 class="text-sm font-medium text-gray-500">Kategori Keluarga (AI)</h4>
|
|
||||||
<p>{{ profile.analysis.kategoriKeluarga }}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h4 class="text-sm font-medium text-gray-500">Cadangan Kategori (AI)</h4>
|
|
||||||
<p>{{ profile.analysis.cadanganKategori }}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h4 class="text-sm font-medium text-gray-500">Status Kelayakan (AI)</h4>
|
|
||||||
<p>{{ profile.analysis.statusKelayakan }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</rs-card>
|
|
||||||
|
|
||||||
<!-- Card 2: Cadangan & Rumusan AI -->
|
|
||||||
<rs-card>
|
|
||||||
<template #header>
|
|
||||||
<h3 class="text-lg font-semibold text-primary p-4 border-b">Cadangan & Rumusan (AI)</h3>
|
|
||||||
</template>
|
|
||||||
<template #body>
|
|
||||||
<div class="p-4 space-y-4">
|
|
||||||
<div>
|
|
||||||
<h4 class="text-sm font-medium text-gray-500">Cadangan Bantuan (AI)</h4>
|
|
||||||
<ul v-if="profile.analysis.cadanganBantuan && profile.analysis.cadanganBantuan.length > 0" class="list-disc list-inside space-y-1 mt-1">
|
|
||||||
<li v-for="(bantuan, index) in profile.analysis.cadanganBantuan" :key="index" class="text-gray-700">
|
|
||||||
{{ bantuan.nama }}
|
|
||||||
<span v-if="bantuan.peratusan && bantuan.peratusan !== 'Ralat'" class="font-semibold text-blue-600">({{ bantuan.peratusan }})</span>
|
|
||||||
<span v-else-if="bantuan.peratusan === 'Ralat'" class="text-red-500 text-xs">({{ bantuan.peratusan }})</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<p v-else class="text-gray-500 mt-1">Tiada cadangan bantuan spesifik.</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h4 class="text-sm font-medium text-gray-500">Ramalan Jangka Masa Taraf Hidup Pulih (AI)</h4>
|
|
||||||
<p>{{ profile.analysis.ramalanJangkaMasaPulih }}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h4 class="text-sm font-medium text-gray-500">Rumusan Keseluruhan (AI)</h4>
|
|
||||||
<div class="mt-1 p-3 bg-blue-50 border-l-4 border-blue-500 rounded-r-md">
|
|
||||||
<p class="whitespace-pre-line text-gray-700 text-sm">{{ profile.analysis.rumusan }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</rs-card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Original Data Column (takes 1/3 on lg screens) -->
|
|
||||||
<div class="lg:col-span-1">
|
|
||||||
<rs-card>
|
|
||||||
<template #header>
|
|
||||||
<h3 class="text-lg font-semibold text-gray-700 p-4 border-b">Ringkasan Profil (Data Asal)</h3>
|
|
||||||
</template>
|
|
||||||
<template #body>
|
|
||||||
<div class="p-4 space-y-3">
|
|
||||||
<div>
|
|
||||||
<h4 class="text-sm font-medium text-gray-500">Jenis Kategori (Asal)</h4>
|
|
||||||
<rs-badge :variant="categoryColor" class="mt-1">{{ profile.kategori }}</rs-badge>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h4 class="text-sm font-medium text-gray-500">Status Semasa (Asal)</h4>
|
|
||||||
<rs-badge :variant="statusColor" class="mt-1">{{ profile.status }}</rs-badge>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h4 class="text-sm font-medium text-gray-500">Jumlah Pendapatan (Asal)</h4>
|
|
||||||
<p>RM {{ profile.totalIncome }}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h4 class="text-sm font-medium text-gray-500">Jumlah Tanggungan (Asal)</h4>
|
|
||||||
<p>{{ profile.dependents.length }} orang</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</rs-card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</rs-card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Delete Confirmation Modal -->
|
|
||||||
<rs-modal v-model="confirmDelete">
|
|
||||||
<template #header>
|
|
||||||
<div class="flex items-center">
|
|
||||||
<Icon name="mdi:alert-circle" size="24" class="text-red-500 mr-2" />
|
|
||||||
<h3 class="text-lg font-medium">Padam Profil</h3>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #default>
|
|
||||||
<div class="p-4">
|
|
||||||
<p class="mb-4">Adakah anda pasti ingin memadam profil ini?</p>
|
|
||||||
<p class="text-sm text-gray-500 mb-2">Nama: <span class="font-medium">{{ profile?.nama }}</span></p>
|
|
||||||
<p class="text-sm text-gray-500">ID: <span class="font-medium">{{ profile?.id }}</span></p>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #footer>
|
|
||||||
<div class="flex justify-end gap-2">
|
|
||||||
<rs-button variant="secondary-outline" @click="cancelDelete">
|
|
||||||
Batal
|
|
||||||
</rs-button>
|
|
||||||
<rs-button variant="danger" @click="confirmDeleteProfile">
|
|
||||||
Padam
|
|
||||||
</rs-button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</rs-modal>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
@ -1,337 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { ref, computed, onMounted } from 'vue';
|
|
||||||
import { useAsnafMockData } from '~/composables/useAsnafMockData';
|
|
||||||
|
|
||||||
definePageMeta({
|
|
||||||
title: "Senarai Asnaf",
|
|
||||||
middleware: ["auth"],
|
|
||||||
requiresAuth: true,
|
|
||||||
breadcrumb: [
|
|
||||||
{
|
|
||||||
name: "Dashboard",
|
|
||||||
path: "/",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "BF-PRF",
|
|
||||||
path: "/BF-PRF",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Asnaf",
|
|
||||||
path: "/BF-PRF/AS",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Senarai",
|
|
||||||
path: "/BF-PRF/AS/LIST",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
// Get asnaf data from the composable
|
|
||||||
const { asnafProfiles, statistics, filterProfiles, categories, statuses } = useAsnafMockData();
|
|
||||||
|
|
||||||
// Table reactivity control
|
|
||||||
const tableKey = ref(0);
|
|
||||||
const currentPage = ref(1);
|
|
||||||
const pageSize = ref(10);
|
|
||||||
const totalProfiles = ref(0);
|
|
||||||
const isLoading = ref(false);
|
|
||||||
|
|
||||||
// Search and filter variables
|
|
||||||
const searchQuery = ref('');
|
|
||||||
const selectedStatus = ref('All');
|
|
||||||
const selectedCategory = ref('All');
|
|
||||||
|
|
||||||
// Table data and fields
|
|
||||||
const tableData = computed(() => {
|
|
||||||
return filterProfiles(searchQuery.value, selectedStatus.value, selectedCategory.value);
|
|
||||||
});
|
|
||||||
|
|
||||||
const tableFields = [
|
|
||||||
{ field: 'no', label: 'No.' },
|
|
||||||
{ field: 'id', label: 'ID' },
|
|
||||||
{ field: 'nama', label: 'Nama' },
|
|
||||||
{ field: 'idNumber', label: 'No. ID' },
|
|
||||||
{ field: 'kategori', label: 'Kategori' },
|
|
||||||
{ field: 'status', label: 'Status' },
|
|
||||||
{ field: 'tindakan', label: 'Tindakan' }
|
|
||||||
];
|
|
||||||
|
|
||||||
// Generate table field and data mapping
|
|
||||||
const formattedTableData = computed(() => {
|
|
||||||
return tableData.value.map((profile, index) => ({
|
|
||||||
no: index + 1,
|
|
||||||
id: profile.id,
|
|
||||||
nama: profile.nama,
|
|
||||||
idNumber: profile.idNumber,
|
|
||||||
kategori: profile.kategori,
|
|
||||||
status: profile.status,
|
|
||||||
tindakan: profile.id
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|
||||||
// Helper functions
|
|
||||||
function getBadgeVariantForCategory(category) {
|
|
||||||
switch (category) {
|
|
||||||
case 'Fakir': return 'danger';
|
|
||||||
case 'Miskin': return 'warning';
|
|
||||||
case 'Mualaf': return 'info';
|
|
||||||
case 'Fi-sabilillah': return 'primary';
|
|
||||||
case 'Gharimin': return 'secondary';
|
|
||||||
case 'Ibnu Sabil': return 'success';
|
|
||||||
default: return 'primary';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getBadgeVariantForStatus(status) {
|
|
||||||
switch (status) {
|
|
||||||
case 'Aktif': return 'success';
|
|
||||||
case 'Tidak Aktif': return 'danger';
|
|
||||||
case 'Dalam Semakan': return 'warning';
|
|
||||||
default: return 'secondary';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function navigateToDetail(id) {
|
|
||||||
console.log("Attempting to navigate to detail for ID:", id);
|
|
||||||
if (id) {
|
|
||||||
navigateTo(`/BF-PRF/AS/DETAIL/${id}`);
|
|
||||||
} else {
|
|
||||||
console.error("Navigation failed: ID is undefined or null");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function navigateToRegistration() {
|
|
||||||
navigateTo('/BF-PRF/AS/FR/01');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pagination
|
|
||||||
const handlePageChange = (newPage) => {
|
|
||||||
currentPage.value = newPage;
|
|
||||||
fetchProfiles();
|
|
||||||
};
|
|
||||||
|
|
||||||
// Fetch data
|
|
||||||
async function fetchProfiles() {
|
|
||||||
isLoading.value = true;
|
|
||||||
// Simulate API delay
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 500));
|
|
||||||
|
|
||||||
totalProfiles.value = tableData.value.length;
|
|
||||||
isLoading.value = false;
|
|
||||||
tableKey.value++; // Force table re-render
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lifecycle hooks
|
|
||||||
onMounted(() => {
|
|
||||||
fetchProfiles();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="space-y-6">
|
|
||||||
<LayoutsBreadcrumb />
|
|
||||||
|
|
||||||
<!-- Header -->
|
|
||||||
<div class="flex justify-between items-center">
|
|
||||||
<h1 class="text-2xl font-bold text-primary">Senarai Asnaf</h1>
|
|
||||||
<rs-button
|
|
||||||
variant="primary"
|
|
||||||
class="flex items-center gap-2"
|
|
||||||
@click="navigateToRegistration"
|
|
||||||
>
|
|
||||||
<Icon name="mdi:plus" size="18" />
|
|
||||||
<span>Tambah Asnaf</span>
|
|
||||||
</rs-button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Statistics Cards -->
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
||||||
<rs-card class="transition-all duration-300 hover:shadow-lg">
|
|
||||||
<div class="p-4 flex items-center gap-4">
|
|
||||||
<div class="p-4 flex justify-center items-center bg-blue-100 rounded-xl">
|
|
||||||
<Icon name="mdi:account-group" size="24" class="text-primary" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span class="block text-2xl font-bold text-primary">{{ statistics.total }}</span>
|
|
||||||
<span class="text-sm text-gray-600">Jumlah Asnaf</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</rs-card>
|
|
||||||
|
|
||||||
<rs-card class="transition-all duration-300 hover:shadow-lg">
|
|
||||||
<div class="p-4 flex items-center gap-4">
|
|
||||||
<div class="p-4 flex justify-center items-center bg-green-100 rounded-xl">
|
|
||||||
<Icon name="mdi:check-circle" size="24" class="text-green-600" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span class="block text-2xl font-bold text-green-600">{{ statistics.active }}</span>
|
|
||||||
<span class="text-sm text-gray-600">Aktif</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</rs-card>
|
|
||||||
|
|
||||||
<rs-card class="transition-all duration-300 hover:shadow-lg">
|
|
||||||
<div class="p-4 flex items-center gap-4">
|
|
||||||
<div class="p-4 flex justify-center items-center bg-yellow-100 rounded-xl">
|
|
||||||
<Icon name="mdi:clock-time-four" size="24" class="text-yellow-600" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span class="block text-2xl font-bold text-yellow-600">{{ statistics.review }}</span>
|
|
||||||
<span class="text-sm text-gray-600">Dalam Semakan</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</rs-card>
|
|
||||||
|
|
||||||
<rs-card class="transition-all duration-300 hover:shadow-lg">
|
|
||||||
<div class="p-4 flex items-center gap-4">
|
|
||||||
<div class="p-4 flex justify-center items-center bg-red-100 rounded-xl">
|
|
||||||
<Icon name="mdi:close-circle" size="24" class="text-red-600" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span class="block text-2xl font-bold text-red-600">{{ statistics.inactive }}</span>
|
|
||||||
<span class="text-sm text-gray-600">Tidak Aktif</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</rs-card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Search and Filters -->
|
|
||||||
<rs-card>
|
|
||||||
<div class="p-4">
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-1">Carian</label>
|
|
||||||
<div class="relative rounded-md shadow-sm">
|
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
|
||||||
<Icon name="mdi:magnify" size="18" class="text-gray-400" />
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
v-model="searchQuery"
|
|
||||||
type="text"
|
|
||||||
class="block w-full pl-10 pr-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-primary focus:border-primary"
|
|
||||||
placeholder="Cari dengan nama atau ID..."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-1">Status</label>
|
|
||||||
<select
|
|
||||||
v-model="selectedStatus"
|
|
||||||
class="block w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-primary focus:border-primary"
|
|
||||||
>
|
|
||||||
<option value="All">Semua Status</option>
|
|
||||||
<option v-for="status in statuses" :key="status" :value="status">
|
|
||||||
{{ status }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-1">Kategori</label>
|
|
||||||
<select
|
|
||||||
v-model="selectedCategory"
|
|
||||||
class="block w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-primary focus:border-primary"
|
|
||||||
>
|
|
||||||
<option value="All">Semua Kategori</option>
|
|
||||||
<option v-for="category in categories" :key="category" :value="category">
|
|
||||||
{{ category }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</rs-card>
|
|
||||||
|
|
||||||
<!-- Data Table -->
|
|
||||||
<rs-card>
|
|
||||||
<template #header>
|
|
||||||
<div class="px-4 py-3 flex items-center justify-between">
|
|
||||||
<h2 class="text-lg font-semibold text-primary">Senarai Asnaf</h2>
|
|
||||||
<span class="text-sm text-gray-500">
|
|
||||||
{{ tableData.length }} asnaf dijumpai
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #body>
|
|
||||||
<div v-if="isLoading && tableData.length === 0" class="py-8 text-center">
|
|
||||||
<div class="flex justify-center">
|
|
||||||
<Icon name="mdi:loading" size="2rem" class="text-blue-500 animate-spin" />
|
|
||||||
</div>
|
|
||||||
<p class="mt-2 text-gray-600">Memuat data...</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<rs-table
|
|
||||||
v-else
|
|
||||||
class="mt-4"
|
|
||||||
:key="tableKey"
|
|
||||||
:data="formattedTableData"
|
|
||||||
:columns="tableFields"
|
|
||||||
:pageSize="pageSize"
|
|
||||||
:showNoColumn="true"
|
|
||||||
:options="{
|
|
||||||
variant: 'default',
|
|
||||||
hover: true,
|
|
||||||
striped: true,
|
|
||||||
bordered: true
|
|
||||||
}"
|
|
||||||
:current-page="currentPage"
|
|
||||||
:total-items="totalProfiles"
|
|
||||||
@page-change="handlePageChange"
|
|
||||||
>
|
|
||||||
<template v-slot:no="data">
|
|
||||||
{{ data.text }}
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template v-slot:id="data">
|
|
||||||
{{ data.text }}
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template v-slot:nama="data">
|
|
||||||
<div class="font-medium">{{ data.text }}</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template v-slot:idNumber="data">
|
|
||||||
{{ data.text }}
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template v-slot:kategori="data">
|
|
||||||
<rs-badge :variant="getBadgeVariantForCategory(data.text)">{{ data.text }}</rs-badge>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template v-slot:status="data">
|
|
||||||
<rs-badge :variant="getBadgeVariantForStatus(data.text)">
|
|
||||||
{{ data.text }}
|
|
||||||
</rs-badge>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template v-slot:tindakan="data">
|
|
||||||
<div class="flex gap-2">
|
|
||||||
<rs-button
|
|
||||||
variant="primary"
|
|
||||||
size="sm"
|
|
||||||
class="!px-2 !py-1"
|
|
||||||
@click="() => {
|
|
||||||
navigateToDetail(data.value.tindakan);
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<Icon name="mdi:eye" size="1rem" class="mr-1" />
|
|
||||||
Lihat
|
|
||||||
</rs-button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</rs-table>
|
|
||||||
|
|
||||||
<!-- Empty State -->
|
|
||||||
<div v-if="!isLoading && tableData.length === 0" class="text-center py-8">
|
|
||||||
<div class="flex justify-center mb-4">
|
|
||||||
<Icon name="mdi:magnify" size="4rem" class="text-gray-400" />
|
|
||||||
</div>
|
|
||||||
<h3 class="text-lg font-medium text-gray-500">Tiada Profil Ditemui</h3>
|
|
||||||
<p class="text-gray-500 mt-2">Sila cuba carian lain atau reset penapis.</p>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</rs-card>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
@ -11,328 +11,305 @@ definePageMeta({
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
// Data baru untuk lapangan terbang teratas
|
// Framework information
|
||||||
const topAirports = ref([
|
const frameworkInfo = ref({
|
||||||
{
|
name: "corradAF",
|
||||||
rank: 1,
|
version: "1.0.0",
|
||||||
name: "Lapangan Terbang Antarabangsa Kuala Lumpur (KLIA)",
|
description: "Corrad Application Framework - A comprehensive Nuxt.js template for rapid application development",
|
||||||
visitors: 62000000,
|
features: [
|
||||||
},
|
"Authentication System",
|
||||||
{
|
"User Management",
|
||||||
rank: 2,
|
"Role-based Access Control",
|
||||||
name: "Lapangan Terbang Antarabangsa Kota Kinabalu",
|
"Development Tools Suite",
|
||||||
visitors: 9000000,
|
"API Management",
|
||||||
},
|
"Menu Configuration",
|
||||||
{ rank: 3, name: "Lapangan Terbang Antarabangsa Penang", visitors: 8000000 },
|
"Content Management",
|
||||||
{ rank: 4, name: "Lapangan Terbang Antarabangsa Kuching", visitors: 5500000 },
|
"Code Playground",
|
||||||
{
|
"ORM Integration",
|
||||||
rank: 5,
|
"Responsive Design"
|
||||||
name: "Lapangan Terbang Antarabangsa Langkawi",
|
]
|
||||||
visitors: 3000000,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Data baru untuk kad ringkasan pantas
|
|
||||||
const quickSummary = ref([
|
|
||||||
{ title: "Jumlah Pelawat", value: "10.5 Juta", icon: "ic:outline-people" },
|
|
||||||
{
|
|
||||||
title: "Pendapatan Pelancongan",
|
|
||||||
value: "RM 86.14 Bilion",
|
|
||||||
icon: "ic:outline-attach-money",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Tempoh Penginapan Purata",
|
|
||||||
value: "6.1 Hari",
|
|
||||||
icon: "ic:outline-hotel",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Kepuasan Pelancong",
|
|
||||||
value: "92%",
|
|
||||||
icon: "ic:outline-sentiment-satisfied",
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Data Pelawat Malaysia
|
|
||||||
const visitorData = ref([
|
|
||||||
{
|
|
||||||
name: "Pelawat Tempatan",
|
|
||||||
data: [5000000, 5500000, 6000000, 6500000, 7000000, 7500000],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Pelawat Asing",
|
|
||||||
data: [3000000, 3500000, 4000000, 4500000, 5000000, 5500000],
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Data Pelawat Asing mengikut Negeri
|
|
||||||
const foreignVisitorsByState = ref([
|
|
||||||
{ state: "Selangor", visitors: 1500000 },
|
|
||||||
{ state: "Pulau Pinang", visitors: 1200000 },
|
|
||||||
{ state: "Johor", visitors: 1000000 },
|
|
||||||
{ state: "Sabah", visitors: 800000 },
|
|
||||||
{ state: "Sarawak", visitors: 600000 },
|
|
||||||
{ state: "Melaka", visitors: 500000 },
|
|
||||||
{ state: "Kedah", visitors: 400000 },
|
|
||||||
{ state: "Negeri Sembilan", visitors: 300000 },
|
|
||||||
{ state: "Perak", visitors: 250000 },
|
|
||||||
{ state: "Terengganu", visitors: 200000 },
|
|
||||||
{ state: "Kelantan", visitors: 150000 },
|
|
||||||
{ state: "Pahang", visitors: 100000 },
|
|
||||||
{ state: "Perlis", visitors: 50000 },
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Lapangan Terbang Keberangkatan Teratas
|
|
||||||
const departureData = ref([
|
|
||||||
{ airport: "JFK", departures: 1500 },
|
|
||||||
{ airport: "LHR", departures: 1200 },
|
|
||||||
{ airport: "CDG", departures: 1000 },
|
|
||||||
{ airport: "DXB", departures: 800 },
|
|
||||||
{ airport: "SIN", departures: 600 },
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Data Pelancong Berulang
|
|
||||||
const repeatVisitorsData = ref([
|
|
||||||
{ category: "1-2 kali", percentage: 45 },
|
|
||||||
{ category: "3-5 kali", percentage: 30 },
|
|
||||||
{ category: "6-10 kali", percentage: 15 },
|
|
||||||
{ category: ">10 kali", percentage: 10 },
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Data Negara Asal Pelancong Asing Teratas
|
|
||||||
const topVisitorCountries = ref([
|
|
||||||
{ country: "Singapura", visitors: 1500000 },
|
|
||||||
{ country: "Indonesia", visitors: 1200000 },
|
|
||||||
{ country: "China", visitors: 1000000 },
|
|
||||||
{ country: "Thailand", visitors: 800000 },
|
|
||||||
{ country: "India", visitors: 600000 },
|
|
||||||
]);
|
|
||||||
|
|
||||||
const chartOptionsVisitors = computed(() => ({
|
|
||||||
chart: { height: 350, type: "line" },
|
|
||||||
stroke: { curve: "smooth", width: 2 },
|
|
||||||
xaxis: { categories: ["2018", "2019", "2020", "2021", "2022", "2023"] },
|
|
||||||
yaxis: { title: { text: "Bilangan Pelawat" } },
|
|
||||||
}));
|
|
||||||
|
|
||||||
const chartOptionsForeignVisitors = computed(() => ({
|
|
||||||
chart: { type: "bar" },
|
|
||||||
plotOptions: { bar: { horizontal: true } },
|
|
||||||
xaxis: { categories: foreignVisitorsByState.value.map((item) => item.state) },
|
|
||||||
}));
|
|
||||||
|
|
||||||
const chartOptionsDeparture = computed(() => ({
|
|
||||||
chart: { type: "bar" },
|
|
||||||
plotOptions: { bar: { horizontal: true } },
|
|
||||||
xaxis: { categories: departureData.value.map((item) => item.airport) },
|
|
||||||
}));
|
|
||||||
|
|
||||||
const chartOptionsRepeatVisitors = computed(() => ({
|
|
||||||
chart: { type: "pie" },
|
|
||||||
labels: repeatVisitorsData.value.map((item) => item.category),
|
|
||||||
responsive: [
|
|
||||||
{
|
|
||||||
breakpoint: 480,
|
|
||||||
options: {
|
|
||||||
chart: {
|
|
||||||
width: 200,
|
|
||||||
},
|
|
||||||
legend: {
|
|
||||||
position: "bottom",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}));
|
|
||||||
|
|
||||||
const chartOptionsTopCountries = computed(() => ({
|
|
||||||
chart: { type: "bar" },
|
|
||||||
plotOptions: {
|
|
||||||
bar: { horizontal: false, columnWidth: "55%", endingShape: "rounded" },
|
|
||||||
},
|
|
||||||
dataLabels: { enabled: false },
|
|
||||||
stroke: { show: true, width: 2, colors: ["transparent"] },
|
|
||||||
xaxis: { categories: topVisitorCountries.value.map((item) => item.country) },
|
|
||||||
yaxis: { title: { text: "Bilangan Pelawat" } },
|
|
||||||
fill: { opacity: 1 },
|
|
||||||
tooltip: {
|
|
||||||
y: {
|
|
||||||
formatter: function (val) {
|
|
||||||
return val.toLocaleString() + " pelawat";
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
// Sebarang logik yang diperlukan semasa pemasangan
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Development tools available
|
||||||
|
const devTools = ref([
|
||||||
|
{
|
||||||
|
title: "User Management",
|
||||||
|
description: "Manage users and roles with comprehensive CRUD operations",
|
||||||
|
icon: "mdi:account-group",
|
||||||
|
path: "/devtool/user-management/user",
|
||||||
|
color: "blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Menu Editor",
|
||||||
|
description: "Configure navigation menus and application structure",
|
||||||
|
icon: "mdi:menu",
|
||||||
|
path: "/devtool/menu-editor",
|
||||||
|
color: "green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "API Editor",
|
||||||
|
description: "Design and test API endpoints with interactive tools",
|
||||||
|
icon: "mdi:api",
|
||||||
|
path: "/devtool/api-editor",
|
||||||
|
color: "purple"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Content Editor",
|
||||||
|
description: "Manage dynamic content and templates",
|
||||||
|
icon: "mdi:file-document-edit",
|
||||||
|
path: "/devtool/content-editor",
|
||||||
|
color: "orange"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Code Playground",
|
||||||
|
description: "Test and prototype code snippets in real-time",
|
||||||
|
icon: "mdi:code-braces",
|
||||||
|
path: "/devtool/code-playground",
|
||||||
|
color: "indigo"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "ORM Tools",
|
||||||
|
description: "Database schema management and query tools",
|
||||||
|
icon: "mdi:database",
|
||||||
|
path: "/devtool/orm",
|
||||||
|
color: "red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Configuration",
|
||||||
|
description: "System settings and environment configuration",
|
||||||
|
icon: "mdi:cog",
|
||||||
|
path: "/devtool/config",
|
||||||
|
color: "gray"
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Quick stats
|
||||||
|
const quickStats = ref([
|
||||||
|
{ title: "Dev Tools", value: "7", icon: "mdi:tools" },
|
||||||
|
{ title: "Components", value: "50+", icon: "mdi:view-grid" },
|
||||||
|
{ title: "Auth System", value: "Ready", icon: "mdi:shield-check" },
|
||||||
|
{ title: "Framework", value: "Nuxt 3", icon: "mdi:nuxt" }
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Getting started steps
|
||||||
|
const gettingStarted = ref([
|
||||||
|
{
|
||||||
|
step: 1,
|
||||||
|
title: "Clone Repository",
|
||||||
|
description: "Clone this template to start your new project",
|
||||||
|
command: "git clone <repository-url> your-project-name"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
step: 2,
|
||||||
|
title: "Install Dependencies",
|
||||||
|
description: "Install all required packages",
|
||||||
|
command: "yarn install"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
step: 3,
|
||||||
|
title: "Configure Environment",
|
||||||
|
description: "Set up your environment variables and database",
|
||||||
|
command: "cp .env.example .env"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
step: 4,
|
||||||
|
title: "Start Development",
|
||||||
|
description: "Run the development server",
|
||||||
|
command: "yarn dev"
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
function navigateToTool(path) {
|
||||||
|
navigateTo(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getColorClasses(color) {
|
||||||
|
const colorMap = {
|
||||||
|
blue: 'bg-blue-100 text-blue-600 hover:bg-blue-200',
|
||||||
|
green: 'bg-green-100 text-green-600 hover:bg-green-200',
|
||||||
|
purple: 'bg-purple-100 text-purple-600 hover:bg-purple-200',
|
||||||
|
orange: 'bg-orange-100 text-orange-600 hover:bg-orange-200',
|
||||||
|
indigo: 'bg-indigo-100 text-indigo-600 hover:bg-indigo-200',
|
||||||
|
red: 'bg-red-100 text-red-600 hover:bg-red-200',
|
||||||
|
gray: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
|
||||||
|
};
|
||||||
|
return colorMap[color] || 'bg-gray-100 text-gray-600 hover:bg-gray-200';
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="space-y-8">
|
||||||
<LayoutsBreadcrumb />
|
<LayoutsBreadcrumb />
|
||||||
<!-- Kad Ringkasan Pantas -->
|
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-4 gap-6">
|
<!-- Welcome Header -->
|
||||||
|
<div class="text-center py-12 bg-gradient-to-br from-primary/10 to-secondary/10 rounded-2xl">
|
||||||
|
<div class="max-w-4xl mx-auto px-6">
|
||||||
|
<h1 class="text-4xl md:text-6xl font-bold text-primary mb-4">
|
||||||
|
Welcome to {{ frameworkInfo.name }}
|
||||||
|
</h1>
|
||||||
|
<p class="text-xl text-gray-600 mb-6">
|
||||||
|
{{ frameworkInfo.description }}
|
||||||
|
</p>
|
||||||
|
<div class="flex justify-center gap-4">
|
||||||
|
<rs-badge variant="primary" class="text-sm px-4 py-2">
|
||||||
|
v{{ frameworkInfo.version }}
|
||||||
|
</rs-badge>
|
||||||
|
<rs-badge variant="secondary" class="text-sm px-4 py-2">
|
||||||
|
Nuxt 3 Ready
|
||||||
|
</rs-badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Quick Stats -->
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||||
<rs-card
|
<rs-card
|
||||||
v-for="(item, index) in quickSummary"
|
v-for="(stat, index) in quickStats"
|
||||||
:key="index"
|
:key="index"
|
||||||
class="transition-all duration-300 hover:shadow-lg"
|
class="transition-all duration-300 hover:shadow-lg"
|
||||||
>
|
>
|
||||||
<div class="pt-5 pb-3 px-5 flex items-center gap-4">
|
<div class="p-6 flex items-center gap-4">
|
||||||
|
<div class="p-4 bg-primary/20 rounded-2xl">
|
||||||
|
<Icon :name="stat.icon" size="24" class="text-primary" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="block text-2xl font-bold text-primary">{{ stat.value }}</span>
|
||||||
|
<span class="text-sm text-gray-600">{{ stat.title }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</rs-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Development Tools -->
|
||||||
|
<div>
|
||||||
|
<h2 class="text-2xl font-bold text-primary mb-6">Development Tools</h2>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
<rs-card
|
||||||
|
v-for="(tool, index) in devTools"
|
||||||
|
:key="index"
|
||||||
|
class="transition-all duration-300 hover:shadow-lg cursor-pointer group"
|
||||||
|
@click="navigateToTool(tool.path)"
|
||||||
|
>
|
||||||
|
<div class="p-6">
|
||||||
|
<div class="flex items-start gap-4 mb-4">
|
||||||
<div
|
<div
|
||||||
class="p-5 flex justify-center items-center bg-primary/20 rounded-2xl transition-all duration-300 hover:bg-primary/30"
|
:class="getColorClasses(tool.color)"
|
||||||
|
class="p-3 rounded-xl transition-all duration-300"
|
||||||
>
|
>
|
||||||
<Icon class="text-primary text-3xl" :name="item.icon"></Icon>
|
<Icon :name="tool.icon" size="24" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 truncate">
|
<div class="flex-1">
|
||||||
<span class="block font-bold text-2xl leading-tight text-primary">
|
<h3 class="font-semibold text-lg text-gray-800 group-hover:text-primary transition-colors">
|
||||||
{{ item.value }}
|
{{ tool.title }}
|
||||||
</span>
|
</h3>
|
||||||
<span class="text-sm font-medium text-gray-600">
|
</div>
|
||||||
{{ item.title }}
|
</div>
|
||||||
</span>
|
<p class="text-gray-600 text-sm leading-relaxed">
|
||||||
|
{{ tool.description }}
|
||||||
|
</p>
|
||||||
|
<div class="mt-4 flex items-center text-primary text-sm font-medium group-hover:gap-2 transition-all">
|
||||||
|
<span>Open Tool</span>
|
||||||
|
<Icon name="mdi:arrow-right" size="16" class="ml-1 group-hover:ml-2 transition-all" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</rs-card>
|
</rs-card>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
<!-- Framework Features -->
|
||||||
<!-- Gambaran Keseluruhan Pelawat Malaysia -->
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||||
<rs-card class="col-span-1 lg:col-span-2">
|
<!-- Features List -->
|
||||||
<template #header>
|
|
||||||
<h2 class="text-xl font-bold text-primary">
|
|
||||||
Gambaran Keseluruhan Pelawat
|
|
||||||
</h2>
|
|
||||||
</template>
|
|
||||||
<template #body>
|
|
||||||
<client-only>
|
|
||||||
<VueApexCharts
|
|
||||||
width="100%"
|
|
||||||
height="350"
|
|
||||||
type="line"
|
|
||||||
:options="chartOptionsVisitors"
|
|
||||||
:series="visitorData"
|
|
||||||
></VueApexCharts>
|
|
||||||
</client-only>
|
|
||||||
</template>
|
|
||||||
</rs-card>
|
|
||||||
|
|
||||||
<!-- Pelawat Asing mengikut Negeri -->
|
|
||||||
<rs-card>
|
<rs-card>
|
||||||
<template #header>
|
<div class="p-6">
|
||||||
<h2 class="text-lg font-semibold text-primary">
|
<h3 class="text-xl font-bold text-primary mb-4 flex items-center gap-2">
|
||||||
Pelawat Asing mengikut Negeri
|
<Icon name="mdi:star" size="20" />
|
||||||
</h2>
|
Framework Features
|
||||||
</template>
|
</h3>
|
||||||
<template #body>
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||||
<client-only>
|
<div
|
||||||
<VueApexCharts
|
v-for="(feature, index) in frameworkInfo.features"
|
||||||
width="100%"
|
:key="index"
|
||||||
height="300"
|
class="flex items-center gap-2 text-sm"
|
||||||
type="bar"
|
>
|
||||||
:options="chartOptionsForeignVisitors"
|
<Icon name="mdi:check-circle" size="16" class="text-green-500" />
|
||||||
:series="[
|
<span>{{ feature }}</span>
|
||||||
{ data: foreignVisitorsByState.map((item) => item.visitors) },
|
</div>
|
||||||
]"
|
</div>
|
||||||
></VueApexCharts>
|
</div>
|
||||||
</client-only>
|
|
||||||
</template>
|
|
||||||
</rs-card>
|
</rs-card>
|
||||||
|
|
||||||
<!-- Pelancong Berulang -->
|
<!-- Getting Started -->
|
||||||
<rs-card>
|
<rs-card>
|
||||||
<template #header>
|
<div class="p-6">
|
||||||
<h2 class="text-lg font-semibold text-primary">
|
<h3 class="text-xl font-bold text-primary mb-4 flex items-center gap-2">
|
||||||
Kekerapan Lawatan Pelancong
|
<Icon name="mdi:rocket-launch" size="20" />
|
||||||
</h2>
|
Getting Started
|
||||||
</template>
|
</h3>
|
||||||
<template #body>
|
<div class="space-y-4">
|
||||||
<client-only>
|
<div
|
||||||
<VueApexCharts
|
v-for="(step, index) in gettingStarted"
|
||||||
width="100%"
|
:key="index"
|
||||||
height="300"
|
class="border-l-2 border-primary/20 pl-4"
|
||||||
type="pie"
|
>
|
||||||
:options="chartOptionsRepeatVisitors"
|
<div class="flex items-center gap-2 mb-1">
|
||||||
:series="repeatVisitorsData.map((item) => item.percentage)"
|
<span class="w-6 h-6 bg-primary text-white rounded-full flex items-center justify-center text-xs font-bold">
|
||||||
></VueApexCharts>
|
{{ step.step }}
|
||||||
</client-only>
|
</span>
|
||||||
</template>
|
<h4 class="font-semibold text-gray-800">{{ step.title }}</h4>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-gray-600 mb-2">{{ step.description }}</p>
|
||||||
|
<code class="text-xs bg-gray-100 px-2 py-1 rounded block font-mono">
|
||||||
|
{{ step.command }}
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</rs-card>
|
</rs-card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Negara Asal Pelancong Asing Teratas -->
|
<!-- Documentation Links -->
|
||||||
<rs-card class="mb-6">
|
<rs-card>
|
||||||
<template #header>
|
<div class="p-6">
|
||||||
<h2 class="text-xl font-bold text-primary">
|
<h3 class="text-xl font-bold text-primary mb-4 flex items-center gap-2">
|
||||||
Negara Asal Pelancong Asing Teratas
|
<Icon name="mdi:book-open" size="20" />
|
||||||
</h2>
|
Documentation & Resources
|
||||||
</template>
|
</h3>
|
||||||
<template #body>
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
<client-only>
|
<a
|
||||||
<VueApexCharts
|
href="https://nuxt.com/docs"
|
||||||
width="100%"
|
target="_blank"
|
||||||
height="350"
|
class="flex items-center gap-3 p-4 bg-gray-50 rounded-xl hover:bg-gray-100 transition-colors"
|
||||||
type="bar"
|
|
||||||
:options="chartOptionsTopCountries"
|
|
||||||
:series="[
|
|
||||||
{
|
|
||||||
name: 'Pelawat',
|
|
||||||
data: topVisitorCountries.map((item) => item.visitors),
|
|
||||||
},
|
|
||||||
]"
|
|
||||||
></VueApexCharts>
|
|
||||||
</client-only>
|
|
||||||
</template>
|
|
||||||
</rs-card>
|
|
||||||
|
|
||||||
<rs-card class="mb-6">
|
|
||||||
<template #header>
|
|
||||||
<h2 class="text-xl font-bold text-primary">
|
|
||||||
Lapangan Terbang Teratas dengan Pelawat Terbanyak
|
|
||||||
</h2>
|
|
||||||
</template>
|
|
||||||
<template #body>
|
|
||||||
<div class="overflow-x-auto">
|
|
||||||
<table class="min-w-full divide-y divide-gray-200">
|
|
||||||
<thead class="bg-gray-50">
|
|
||||||
<tr>
|
|
||||||
<th
|
|
||||||
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
|
||||||
>
|
>
|
||||||
Kedudukan
|
<Icon name="mdi:nuxt" size="24" class="text-green-500" />
|
||||||
</th>
|
<div>
|
||||||
<th
|
<div class="font-semibold">Nuxt 3 Docs</div>
|
||||||
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
<div class="text-sm text-gray-600">Official documentation</div>
|
||||||
>
|
</div>
|
||||||
Nama Lapangan Terbang
|
</a>
|
||||||
</th>
|
<a
|
||||||
<th
|
href="https://tailwindcss.com/docs"
|
||||||
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
target="_blank"
|
||||||
>
|
class="flex items-center gap-3 p-4 bg-gray-50 rounded-xl hover:bg-gray-100 transition-colors"
|
||||||
Jumlah Pelawat
|
>
|
||||||
</th>
|
<Icon name="mdi:tailwind" size="24" class="text-blue-500" />
|
||||||
</tr>
|
<div>
|
||||||
</thead>
|
<div class="font-semibold">Tailwind CSS</div>
|
||||||
<tbody class="bg-white divide-y divide-gray-200">
|
<div class="text-sm text-gray-600">Utility-first CSS</div>
|
||||||
<tr
|
</div>
|
||||||
v-for="airport in topAirports"
|
</a>
|
||||||
:key="airport.rank"
|
<a
|
||||||
class="hover:bg-gray-50 transition-colors duration-200"
|
href="https://github.com"
|
||||||
>
|
target="_blank"
|
||||||
<td class="px-6 py-4 whitespace-nowrap font-medium">
|
class="flex items-center gap-3 p-4 bg-gray-50 rounded-xl hover:bg-gray-100 transition-colors"
|
||||||
{{ airport.rank }}
|
>
|
||||||
</td>
|
<Icon name="mdi:github" size="24" class="text-gray-700" />
|
||||||
<td class="px-6 py-4 whitespace-nowrap">{{ airport.name }}</td>
|
<div>
|
||||||
<td
|
<div class="font-semibold">Source Code</div>
|
||||||
class="px-6 py-4 whitespace-nowrap font-semibold text-primary"
|
<div class="text-sm text-gray-600">View on GitHub</div>
|
||||||
>
|
</div>
|
||||||
{{ airport.visitors.toLocaleString() }}
|
</a>
|
||||||
</td>
|
</div>
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
|
||||||
</rs-card>
|
</rs-card>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -1,124 +0,0 @@
|
|||||||
import { defineEventHandler, readBody } from 'h3';
|
|
||||||
|
|
||||||
// Define an interface for the expected request body (subset of AsnafProfile)
|
|
||||||
interface AsnafAnalysisRequest {
|
|
||||||
monthlyIncome: string;
|
|
||||||
otherIncome: string;
|
|
||||||
totalIncome: string;
|
|
||||||
occupation: string;
|
|
||||||
maritalStatus: string;
|
|
||||||
dependents: Array<any>; // Or a more specific type if you have one for dependents
|
|
||||||
// Add any other fields you deem necessary for OpenAI to analyze
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AidSuggestion {
|
|
||||||
nama: string;
|
|
||||||
peratusan: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Define an interface for the expected OpenAI response structure (and our API response)
|
|
||||||
interface AsnafAnalysisResponse {
|
|
||||||
hadKifayahPercentage: string;
|
|
||||||
kategoriAsnaf: string;
|
|
||||||
kategoriKeluarga: string;
|
|
||||||
cadanganKategori: string;
|
|
||||||
statusKelayakan: string;
|
|
||||||
cadanganBantuan: AidSuggestion[];
|
|
||||||
ramalanJangkaMasaPulih: string;
|
|
||||||
rumusan: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default defineEventHandler(async (event): Promise<AsnafAnalysisResponse> => {
|
|
||||||
const body = await readBody<AsnafAnalysisRequest>(event);
|
|
||||||
|
|
||||||
// --- Placeholder for Actual OpenAI API Call ---
|
|
||||||
// In a real application, you would:
|
|
||||||
// 1. Retrieve your OpenAI API key securely (e.g., from environment variables)
|
|
||||||
const openAIApiKey = process.env.OPENAI_API_KEY;
|
|
||||||
if (!openAIApiKey) {
|
|
||||||
console.error('OpenAI API key not configured. Please set OPENAI_API_KEY in your .env file.');
|
|
||||||
throw createError({ statusCode: 500, statusMessage: 'OpenAI API key not configured' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Construct the prompt for OpenAI using the data from `body`.
|
|
||||||
// IMPORTANT: Sanitize or carefully construct any data from `body` included in the prompt to prevent prompt injection.
|
|
||||||
const prompt = `You are an expert Zakat administrator. Based on the following applicant data: monthlyIncome: ${body.monthlyIncome}, totalIncome: ${body.totalIncome}, occupation: ${body.occupation}, maritalStatus: ${body.maritalStatus}, dependents: ${body.dependents.length}.
|
|
||||||
Return JSON with keys: hadKifayahPercentage, kategoriAsnaf, kategoriKeluarga, cadanganKategori, statusKelayakan, cadanganBantuan, ramalanJangkaMasaPulih, rumusan.
|
|
||||||
For 'cadanganBantuan', provide a JSON array of objects, where each object has a 'nama' (string, name of the aid) and 'peratusan' (string, e.g., '85%', representing suitability). Suggest 2-3 most relevant aid types.
|
|
||||||
Example for cadanganBantuan: [{"nama": "Bantuan Kewangan Bulanan", "peratusan": "90%"}, {"nama": "Bantuan Makanan Asas", "peratusan": "75%"}].
|
|
||||||
Full JSON Example: {"hadKifayahPercentage": "75%", ..., "cadanganBantuan": [{"nama": "Bantuan Kewangan Bulanan", "peratusan": "90%"}], ...}`;
|
|
||||||
// Adjust the prompt to be more detailed and specific to your needs and desired JSON output structure.
|
|
||||||
|
|
||||||
// 3. Make the API call to OpenAI
|
|
||||||
try {
|
|
||||||
const openAIResponse = await fetch('https://api.openai.com/v1/chat/completions', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Authorization': `Bearer ${openAIApiKey}`,
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
model: 'gpt-3.5-turbo', // Or your preferred model like gpt-4
|
|
||||||
messages: [{ role: 'user', content: prompt }],
|
|
||||||
// For more consistent JSON output, consider using a model version that officially supports JSON mode if available
|
|
||||||
// and set response_format: { type: "json_object" }, (check OpenAI documentation for model compatibility)
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!openAIResponse.ok) {
|
|
||||||
const errorData = await openAIResponse.text();
|
|
||||||
console.error('OpenAI API Error details:', errorData);
|
|
||||||
throw createError({ statusCode: openAIResponse.status, statusMessage: `Failed to get analysis from OpenAI: ${openAIResponse.statusText}` });
|
|
||||||
}
|
|
||||||
|
|
||||||
const openAIData = await openAIResponse.json();
|
|
||||||
|
|
||||||
// Parse the content from the response - structure might vary slightly based on OpenAI model/API version
|
|
||||||
// It's common for the JSON string to be in openAIData.choices[0].message.content
|
|
||||||
if (openAIData.choices && openAIData.choices[0] && openAIData.choices[0].message && openAIData.choices[0].message.content) {
|
|
||||||
const analysisResult = JSON.parse(openAIData.choices[0].message.content) as AsnafAnalysisResponse;
|
|
||||||
return analysisResult;
|
|
||||||
} else {
|
|
||||||
console.error('OpenAI response structure not as expected:', openAIData);
|
|
||||||
throw createError({ statusCode: 500, statusMessage: 'Unexpected response structure from OpenAI' });
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error during OpenAI API call or parsing:', error);
|
|
||||||
// Avoid exposing detailed internal errors to the client if they are not createError objects
|
|
||||||
if (typeof error === 'object' && error !== null && 'statusCode' in error) {
|
|
||||||
// We can infer error has statusCode here, but to be super safe with TS:
|
|
||||||
const e = error as { statusCode: number };
|
|
||||||
if (e.statusCode) throw e;
|
|
||||||
}
|
|
||||||
throw createError({ statusCode: 500, statusMessage: 'Internal server error during AI analysis' });
|
|
||||||
}
|
|
||||||
// --- End of Actual OpenAI API Call ---
|
|
||||||
|
|
||||||
// The simulated response below this line should be REMOVED once the actual OpenAI call is implemented and working.
|
|
||||||
/*
|
|
||||||
console.log('Received for analysis in server route:', body);
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 2000)); // Simulate API delay
|
|
||||||
|
|
||||||
const totalIncomeNumeric = parseFloat(body.totalIncome);
|
|
||||||
let percentage = '50%';
|
|
||||||
if (totalIncomeNumeric < 1000) percentage = '30%';
|
|
||||||
else if (totalIncomeNumeric < 2000) percentage = '65%';
|
|
||||||
else if (totalIncomeNumeric < 3000) percentage = '85%';
|
|
||||||
else percentage = '110%';
|
|
||||||
|
|
||||||
return {
|
|
||||||
hadKifayahPercentage: percentage,
|
|
||||||
kategoriAsnaf: 'Simulated - Miskin',
|
|
||||||
kategoriKeluarga: 'Simulated - Miskin (50-100% HK)',
|
|
||||||
cadanganKategori: 'Simulated - Miskin',
|
|
||||||
statusKelayakan: 'Simulated - Layak (Miskin)',
|
|
||||||
cadanganBantuan: [
|
|
||||||
{ nama: 'Simulated - Bantuan Kewangan Bulanan', peratusan: '80%' },
|
|
||||||
{ nama: 'Simulated - Bantuan Pendidikan Anak', peratusan: '65%' }
|
|
||||||
],
|
|
||||||
ramalanJangkaMasaPulih: 'Simulated - 6 bulan',
|
|
||||||
rumusan: 'Simulated - Pemohon memerlukan perhatian segera.'
|
|
||||||
};
|
|
||||||
*/
|
|
||||||
});
|
|
Loading…
x
Reference in New Issue
Block a user