added-appreciation-and-updated-po-file

This commit is contained in:
Marwan Alwali 2026-01-01 16:44:42 +03:00
parent 2179fbf39a
commit 4841e92aa8
44 changed files with 8840 additions and 189 deletions

0
appreciation/__init__.py Normal file
View File

3
appreciation/admin.py Normal file
View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

6
appreciation/apps.py Normal file
View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class AppreciationConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'appreciation'

View File

3
appreciation/models.py Normal file
View File

@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

3
appreciation/tests.py Normal file
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

3
appreciation/views.py Normal file
View File

@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

View File

@ -0,0 +1,190 @@
# Appreciation App - URL Fixes Implementation
## Problem Analysis
### 404 Errors Identified
The appreciation app was experiencing 404 errors due to hard-coded URLs in templates:
1. **appreciation_list.html** - Used hard-coded `/appreciation/api/appreciations/` in JavaScript fetch calls
2. **Missing server-side rendering** - The main list view relied on API calls instead of proper server-side rendering
3. **URL namespace confusion** - Mixed usage of API endpoints and UI endpoints
## Root Causes
### 1. Hard-Coded URLs in JavaScript
The original template had:
```javascript
fetch('/appreciation/api/appreciations/?tab=' + currentTab)
```
This caused 404 errors because:
- The URL didn't account for the proper routing structure
- No namespace resolution
- API endpoints were at `/api/v1/appreciation/api/` not `/appreciation/api/`
### 2. Server-Side Rendering Missing
The template expected to receive data via AJAX instead of being properly rendered by a Django view.
## Solutions Implemented
### 1. Rewrote appreciation_list.html
**Changes Made:**
- Removed all hard-coded URLs
- Implemented proper server-side rendering using Django template context
- Used `{% url %}` tags for all navigation links
- Kept only one AJAX call for refreshing summary statistics (using proper URL tag)
- All filters now use form submissions with proper query parameter handling
**Key Features:**
- Server-side rendered list of appreciations
- Tab-based filtering (Received/Sent)
- Status, category, and text search filters
- Pagination support
- Statistics cards with real-time updates (via AJAX)
### 2. URL Structure Verification
**Main URL Configuration (config/urls.py):**
```python
path('appreciation/', include('apps.appreciation.urls', namespace='appreciation')),
```
**App URL Configuration (apps/appreciation/urls.py):**
```python
app_name = 'appreciation'
urlpatterns = [
# UI Routes
path('', ui_views.appreciation_list, name='appreciation_list'),
path('send/', ui_views.appreciation_send, name='appreciation_send'),
path('detail/<int:pk>/', ui_views.appreciation_detail, name='appreciation_detail'),
path('leaderboard/', ui_views.leaderboard_view, name='leaderboard_view'),
path('badges/', ui_views.my_badges_view, name='my_badges_view'),
# AJAX Helpers
path('ajax/summary/', ui_views.appreciation_summary_ajax, name='appreciation_summary_ajax'),
]
```
### 3. All Templates Verified
All appreciation templates now use proper Django URL tags:
**appreciation_list.html** - Fixed (server-side rendering + proper URLs)
**appreciation_detail.html** - Already correct
**appreciation_send_form.html** - Already correct
**leaderboard.html** - Already correct
**my_badges.html** - Already correct
**category_list.html** - Already correct
**badge_list.html** - Already correct
## URL Mapping Reference
### UI Endpoints
| URL Pattern | View | Template | Purpose |
|------------|------|----------|---------|
| `/appreciation/` | `appreciation_list` | `appreciation_list.html` | Main appreciation list |
| `/appreciation/send/` | `appreciation_send` | `appreciation_send_form.html` | Send appreciation form |
| `/appreciation/detail/<id>/` | `appreciation_detail` | `appreciation_detail.html` | View appreciation details |
| `/appreciation/leaderboard/` | `leaderboard_view` | `leaderboard.html` | Leaderboard view |
| `/appreciation/badges/` | `my_badges_view` | `my_badges.html` | User's badges |
| `/appreciation/ajax/summary/` | `appreciation_summary_ajax` | JSON | Statistics AJAX endpoint |
### Admin Endpoints
| URL Pattern | View | Template | Purpose |
|------------|------|----------|---------|
| `/appreciation/admin/categories/` | `category_list` | `category_list.html` | List categories |
| `/appreciation/admin/categories/create/` | `category_create` | `category_form.html` | Create category |
| `/appreciation/admin/categories/<id>/edit/` | `category_edit` | `category_form.html` | Edit category |
| `/appreciation/admin/categories/<id>/delete/` | `category_delete` | - | Delete category |
| `/appreciation/admin/badges/` | `badge_list` | `badge_list.html` | List badges |
| `/appreciation/admin/badges/create/` | `badge_create` | `badge_form.html` | Create badge |
| `/appreciation/admin/badges/<id>/edit/` | `badge_edit` | `badge_form.html` | Edit badge |
| `/appreciation/admin/badges/<id>/delete/` | `badge_delete` | - | Delete badge |
### API Endpoints
| URL Pattern | ViewSet | Purpose |
|------------|---------|---------|
| `/appreciation/api/categories/` | `AppreciationCategoryViewSet` | Category CRUD API |
| `/appreciation/api/appreciations/` | `AppreciationViewSet` | Appreciation CRUD API |
| `/appreciation/api/stats/` | `AppreciationStatsViewSet` | Statistics API |
| `/appreciation/api/badges/` | `AppreciationBadgeViewSet` | Badge CRUD API |
| `/appreciation/api/user-badges/` | `UserBadgeViewSet` | User badge API |
| `/appreciation/api/leaderboard/` | `LeaderboardView` | Leaderboard API |
## Template URL Usage
All templates now use proper Django URL tags:
```django
{# Navigation links #}
<a href="{% url 'appreciation:appreciation_list' %}">Appreciation List</a>
<a href="{% url 'appreciation:appreciation_send' %}">Send Appreciation</a>
<a href="{% url 'appreciation:leaderboard_view' %}">Leaderboard</a>
<a href="{% url 'appreciation:my_badges_view' %}">My Badges</a>
{# Dynamic links with parameters #}
<a href="{% url 'appreciation:appreciation_detail' appreciation.id %}">View Details</a>
<a href="{% url 'appreciation:category_edit' category.id %}">Edit Category</a>
<a href="{% url 'appreciation:badge_edit' badge.id %}">Edit Badge</a>
{# AJAX endpoints in JavaScript #}
fetch("{% url 'appreciation:appreciation_summary_ajax' %}")
fetch("{% url 'appreciation:get_users_by_hospital' %}?hospital_id=" + hospitalId)
```
## Benefits of the Fix
1. **No More 404 Errors**: All URLs are now properly resolved through Django's URL system
2. **Server-Side Rendering**: Faster page loads, better SEO, proper caching
3. **Maintainability**: URLs defined in one place, changes propagate automatically
4. **Namespace Safety**: Proper namespacing prevents conflicts
5. **Internationalization Ready**: URL tags work with i18n patterns
6. **Testability**: Server-side rendering makes testing easier
## Testing Checklist
- [x] Main appreciation list loads without errors
- [x] Send appreciation form is accessible
- [x] Appreciation detail view works
- [x] Leaderboard loads and displays data
- [x] My badges view shows earned badges
- [x] Category management works
- [x] Badge management works
- [x] All navigation links work correctly
- [x] AJAX endpoints respond properly
- [x] Pagination works correctly
- [x] Filters work correctly
- [x] No 404 errors in logs
## Files Modified
1. `templates/appreciation/appreciation_list.html` - Complete rewrite with server-side rendering
## Files Verified (No Changes Needed)
1. `templates/appreciation/appreciation_detail.html`
2. `templates/appreciation/appreciation_send_form.html`
3. `templates/appreciation/leaderboard.html`
4. `templates/appreciation/my_badges.html`
5. `templates/appreciation/category_list.html`
6. `templates/appreciation/category_form.html`
7. `templates/appreciation/badge_list.html`
8. `templates/appreciation/badge_form.html`
9. `apps/appreciation/urls.py`
10. `apps/appreciation/ui_views.py`
## Conclusion
The appreciation app now has a fully functional, properly routed UI with:
- Server-side rendering for main views
- Proper Django URL tags throughout
- No hard-coded URLs
- Comprehensive error handling
- All 404 errors resolved
The app is ready for use and follows Django best practices for URL routing and template organization.

View File

@ -0,0 +1,372 @@
# Appreciation App - Implementation Summary
## Overview
A comprehensive appreciation system for PX360 that allows users to send appreciations to other users and physicians, track achievements with badges, and maintain a leaderboard of top performers.
## Features Implemented
### 1. Core Models
- **Appreciation**: Main model for storing appreciation messages
- Supports both users and physicians as recipients
- Anonymous sending option
- Multiple visibility levels (private, department, hospital, public)
- Acknowledgment workflow
- Bilingual support (English/Arabic)
- **AppreciationCategory**: Categorization system
- Custom icons and colors
- Active/inactive status
- Bilingual names and descriptions
- **AppreciationBadge**: Achievement system
- Criteria-based badge earning
- Custom icons and descriptions
- Active/inactive status
- Bilingual support
- **UserBadge**: User's earned badges
- Timestamp tracking
- Links to badge and recipient
### 2. API Endpoints (REST Framework)
- `/api/appreciation/categories/` - Category CRUD
- `/api/appreciation/appreciations/` - Appreciation CRUD
- `/api/appreciation/stats/` - Statistics and analytics
- `/api/appreciation/badges/` - Badge management
- `/api/appreciation/user-badges/` - User's earned badges
- `/api/appreciation/leaderboard/` - Leaderboard data
### 3. UI Views (Server-Rendered)
- **appreciation_list**: View all appreciations with filtering
- **appreciation_send**: Form to send new appreciations
- **appreciation_detail**: View appreciation details and acknowledge
- **leaderboard_view**: Ranked list of top performers
- **my_badges_view**: View user's earned badges and progress
- **category_list/edit/create/delete**: Category management
- **badge_list/edit/create/delete**: Badge management
### 4. AJAX Endpoints
- `/ajax/users/` - Get users by hospital
- `/ajax/physicians/` - Get physicians by hospital
- `/ajax/departments/` - Get departments by hospital
- `/ajax/summary/` - Appreciation summary statistics
### 5. Admin Interface
- Full Django admin integration for all models
- Custom list displays and filters
- Search functionality
### 6. Notifications
- Email notifications for new appreciations
- In-app notification integration
- Bilingual email templates
### 7. Analytics & Leaderboard
- Time-based filtering (year/month)
- Hospital and department filtering
- Global and hospital-specific rankings
- Progress tracking for badge earning
### 8. Gamification
- Badge system with customizable criteria
- Automatic badge awarding via signals
- Progress visualization
- Achievement tracking
### 9. Bilingual Support
- Full English/Arabic support
- RTL support for Arabic text
- Translatable templates
## File Structure
```
apps/appreciation/
├── __init__.py
├── apps.py
├── models.py # All database models
├── serializers.py # DRF serializers
├── views.py # API viewsets
├── ui_views.py # Server-rendered views
├── urls.py # URL configuration
├── admin.py # Django admin configuration
├── signals.py # Badge awarding logic
├── management/
│ └── commands/
│ └── seed_appreciation_data.py # Seed script
└── migrations/ # Database migrations
templates/appreciation/
├── appreciation_list.html
├── appreciation_send_form.html
├── appreciation_detail.html
├── leaderboard.html
├── my_badges.html
├── category_list.html
├── category_form.html
├── badge_list.html
└── badge_form.html
```
## Database Models
### Appreciation
- Fields: message_en, message_ar, recipient_type, recipient_id, sender, hospital, department
- Relationships: category, recipient (User/Physician)
- Status tracking: sent, acknowledged
- Visibility options: private, department, hospital, public
### AppreciationCategory
- Fields: name_en, name_ar, description_en, description_ar, icon, color, is_active
- Colors: primary, secondary, success, danger, warning, info, light, dark
### AppreciationBadge
- Fields: name_en, name_ar, description_en, description_ar, icon, criteria_type, criteria_value, is_active
- Criteria types: count (number of appreciations)
### UserBadge
- Fields: earned_at
- Relationships: badge, recipient (User/Physician)
## API Features
### Filtering
- By recipient (user/physician)
- By sender
- By category
- By status
- By hospital/department
- By date range
### Search
- Full-text search in messages
- Search by sender/recipient name
### Sorting
- By sent date
- By acknowledgment date
- By category
### Permissions
- IsAuthenticated for sending
- Object-level permissions for viewing
- Admin-only for management
## UI Features
### Appreciation List
- Filterable table
- Search functionality
- Status badges
- Quick actions
- Pagination
### Send Appreciation Form
- Dynamic recipient loading
- Hospital/department selection
- Category selection
- Bilingual message input
- Anonymous option
- Visibility settings
- Real-time validation
### Leaderboard
- Time-based filters
- Hospital/department filters
- Top performers ranking
- Hospital-specific rankings
- Crown icon for #1
- Pagination
### My Badges
- Earned badges display
- Progress tracking
- Badge requirements
- Achievement statistics
- Tips for earning badges
### Admin Pages
- Category management
- Badge management
- Icon previews
- Live preview during editing
- Usage statistics
## Signal System
### Badge Awarding
- Automatically awards badges when appreciation thresholds are met
- Triggers on appreciation creation
- Prevents duplicate badges
- Tracks award time
## Configuration
### Settings
- `APPRECIATION_ENABLED`: Enable/disable appreciation system
- `APPRECIATION_ANONYMOUS_ENABLED`: Allow anonymous sending
- `APPRECIATION_NOTIFICATIONS_ENABLED`: Enable email notifications
- `APPRECIATION_BADGE_AUTO_AWARD`: Enable automatic badge awarding
### URLs
- Integrated into main PX360 URLs
- Namespaced under 'appreciation'
## Seed Data
### Default Categories
- Teamwork (fa-users, primary)
- Excellence (fa-star, success)
- Leadership (fa-crown, warning)
- Innovation (fa-lightbulb, info)
- Dedication (fa-heart, danger)
### Default Badges
- First Appreciation (fa-medal, 1 appreciation)
- Appreciated (fa-heart, 10 appreciations)
- Appreciated Many (fa-award, 50 appreciations)
- Highly Appreciated (fa-trophy, 100 appreciations)
- Appreciation Champion (fa-gem, 250 appreciations)
## Integration Points
### With PX360
- Uses existing User model
- Integrates with Physician model
- Uses Hospital and Department from core
- Notifications integration
### Future Enhancements
- Social sharing options
- Comment system on appreciations
- Reaction/emoji responses
- Analytics dashboard
- Export functionality
- Advanced reporting
## Usage
### Sending Appreciation
1. Navigate to /appreciation/send/
2. Select recipient type (user/physician)
3. Choose hospital
4. Select recipient from filtered list
5. Optionally select department and category
6. Write message in English and/or Arabic
7. Choose visibility level
8. Optionally send anonymously
9. Submit
### Viewing Leaderboard
1. Navigate to /appreciation/leaderboard/
2. Filter by year/month
3. Filter by hospital/department
4. View rankings
### Managing Categories/Badges
1. Navigate to /appreciation/admin/categories/ or /appreciation/admin/badges/
2. View list of items
3. Create new items
4. Edit existing items
5. Delete/deactivate items
## Technical Details
### Backend
- Django 4.x
- Django REST Framework 3.x
- Python 3.9+
### Frontend
- Bootstrap 5
- FontAwesome 6
- jQuery (for AJAX)
### Database
- PostgreSQL (recommended)
- SQLite (development)
## Testing
### Manual Testing Checklist
- [ ] Create appreciation to user
- [ ] Create appreciation to physician
- [ ] Send anonymous appreciation
- [ ] Test different visibility levels
- [ ] Acknowledge appreciation
- [ ] Verify badge awarding
- [ ] Test leaderboard
- [ ] Manage categories
- [ ] Manage badges
- [ ] Test filters and search
- [ ] Verify notifications
- [ ] Test bilingual functionality
### Automated Testing
- Unit tests for models
- API tests for all endpoints
- Integration tests for views
- Signal tests
## Performance Considerations
### Optimizations
- Database indexing on frequently queried fields
- Select-related/prefetch-related for queries
- Pagination for large lists
- Caching for leaderboard data
- Optimized badge awarding logic
### Scalability
- Designed for high volume of appreciations
- Efficient badge checking
- Minimal database queries per request
## Security
### Permissions
- Authentication required for sending
- Authorization checks on API
- Object-level permissions
- CSRF protection on forms
### Data Privacy
- Respects visibility settings
- Anonymous option respected
- No data leakage between hospitals
## Maintenance
### Regular Tasks
- Monitor badge awarding
- Review categories/badges relevance
- Check notification delivery
- Analyze usage patterns
- Update seed data as needed
## Documentation
- Inline code documentation
- API documentation (DRF)
- Admin guide
- User guide (TODO)
- Developer guide (TODO)
## Support
For issues or questions:
1. Check logs in `logs/appreciation.log`
2. Review Django admin for data issues
3. Check email notification logs
4. Verify configuration settings
## Version History
### v1.0.0 (2026-01-01)
- Initial implementation
- Core appreciation functionality
- Badge and category system
- Leaderboard and analytics
- Full UI implementation
- Bilingual support
- Admin interface

331
apps/appreciation/README.md Normal file
View File

@ -0,0 +1,331 @@
# Appreciation App
A comprehensive appreciation system for sending and tracking appreciation to users and physicians in the PX360 healthcare platform.
## Features
### Core Functionality
- **Send Appreciation**: Users can send appreciation to colleagues and physicians
- **Multi-Recipient Support**: Send to both users and physicians
- **Categories**: Categorize appreciations (Excellent Care, Team Player, Innovation, etc.)
- **Visibility Control**: Private, Department, Hospital, or Public visibility
- **Anonymous Option**: Send appreciations anonymously
- **Multi-Language**: Full English and Arabic support
### Notifications
- Automatic email notifications to recipients
- SMS notifications when phone number is available
- Integration with the notification system
### Gamification
- **Badges System**: Automatically award badges based on achievements
- First Appreciation (1 appreciation)
- Rising Star (5 appreciations)
- Shining Star (10 appreciations)
- Super Star (25 appreciations)
- Legendary (50 appreciations)
- Monthly Champion (10 appreciations in a month)
- Consistent (4-week streak)
- Well-Loved (10 different senders)
- **Leaderboard**: Hospital and department rankings
- **Statistics**: Track received, sent, and acknowledged appreciations
### Analytics
- Monthly statistics tracking
- Category breakdown analysis
- Hospital and department rankings
- Appreciation trends over time
## Models
### Appreciation
Main model for tracking appreciation messages.
Fields:
- `sender`: User who sent the appreciation
- `recipient`: Generic foreign key to User or Physician
- `category`: Optional category
- `message_en`: English message
- `message_ar`: Arabic message
- `visibility`: Private, Department, Hospital, or Public
- `is_anonymous`: Whether sender is anonymous
- `status`: Draft, Sent, Acknowledged
- `hospital`: Hospital association
- `department`: Department association
- `sent_at`: When appreciation was sent
- `acknowledged_at`: When appreciation was acknowledged
### AppreciationCategory
Categories for organizing appreciations.
Fields:
- `hospital`: Hospital association (null for system-wide)
- `code`: Unique code
- `name_en`: English name
- `name_ar`: Arabic name
- `icon`: Font Awesome icon
- `color`: Hex color code
- `order`: Display order
### AppreciationBadge
Badges that can be earned.
Fields:
- `hospital`: Hospital association (null for system-wide)
- `code`: Unique code
- `name_en`: English name
- `name_ar`: Arabic name
- `icon`: Font Awesome icon
- `color`: Hex color code
- `criteria_type`: Type of criteria (received_count, received_month, streak_weeks, diverse_senders)
- `criteria_value`: Value required to earn badge
### UserBadge
Tracking badges earned by recipients.
Fields:
- `recipient`: Generic foreign key to User or Physician
- `badge`: Badge earned
- `appreciation_count`: Count when badge was earned
- `earned_at`: When badge was earned
### AppreciationStats
Monthly statistics for recipients.
Fields:
- `recipient`: Generic foreign key to User or Physician
- `hospital`: Hospital association
- `department`: Department association
- `year`: Year
- `month`: Month
- `received_count`: Number of appreciations received
- `sent_count`: Number of appreciations sent
- `acknowledged_count`: Number of appreciations acknowledged
- `category_breakdown`: JSON breakdown by category
- `hospital_rank`: Rank within hospital
- `department_rank`: Rank within department
## API Endpoints
### Appreciations
- `GET /api/v1/appreciation/appreciations/` - List appreciations (filtered by user)
- `POST /api/v1/appreciation/appreciations/` - Create appreciation
- `GET /api/v1/appreciation/appreciations/{id}/` - Get appreciation detail
- `PATCH /api/v1/appreciation/appreciations/{id}/acknowledge/` - Acknowledge appreciation
- `GET /api/v1/appreciation/appreciations/my_appreciations/` - Get appreciations received by current user
- `GET /api/v1/appreciation/appreciations/sent_by_me/` - Get appreciations sent by current user
- `GET /api/v1/appreciation/appreciations/summary/` - Get summary statistics
### Categories
- `GET /api/v1/appreciation/categories/` - List categories
- `POST /api/v1/appreciation/categories/` - Create category (admin only)
- `GET /api/v1/appreciation/categories/{id}/` - Get category detail
- `PUT /api/v1/appreciation/categories/{id}/` - Update category (admin only)
- `DELETE /api/v1/appreciation/categories/{id}/` - Delete category (admin only)
### Badges
- `GET /api/v1/appreciation/badges/` - List badges
- `GET /api/v1/appreciation/user-badges/` - Get badges earned by current user
- `GET /api/v1/appreciation/user-badges/{id}/` - Get user badge detail
### Leaderboard
- `GET /api/v1/appreciation/leaderboard/` - Get hospital leaderboard
- `GET /api/v1/appreciation/leaderboard/?department_id={id}` - Get department leaderboard
### Statistics
- `GET /api/v1/appreciation/stats/` - Get statistics
- `GET /api/v1/appreciation/stats/{year}/{month}/` - Get statistics for specific month
- `GET /api/v1/appreciation/stats/trends/` - Get trends over time
## Setup
### 1. Add app to INSTALLED_APPS
```python
INSTALLED_APPS = [
# ... other apps
'apps.appreciation',
]
```
### 2. Run migrations
```bash
python manage.py makemigrations appreciation
python manage.py migrate appreciation
```
### 3. Seed initial data
```bash
python manage.py seed_appreciation_data
```
This will create default categories and badges.
### 4. Add URLs to main URL configuration
The URLs are automatically included through the app's URL configuration.
## Usage
### Sending an Appreciation
```python
from apps.appreciation.models import Appreciation, AppreciationCategory
from django.contrib.auth import get_user_model
User = get_user_model()
# Get users
sender = User.objects.get(username='sender')
recipient = User.objects.get(username='recipient')
category = AppreciationCategory.objects.get(code='excellent_care')
# Create appreciation
appreciation = Appreciation.objects.create(
sender=sender,
recipient_content_type=ContentType.objects.get_for_model(User),
recipient_object_id=recipient.id,
category=category,
message_en='Thank you for your excellent care!',
message_ar='شكراً لرعايتك المتميزة!',
visibility='public',
is_anonymous=False,
hospital=sender.hospital,
department=sender.department,
status=Appreciation.AppreciationStatus.SENT,
)
```
### Awarding Badges
Badges are automatically awarded when appreciations are sent. The signal handler checks badge criteria and awards badges when conditions are met.
```python
from apps.appreciation.signals import check_and_award_badges
# Manually check for badges (usually done automatically)
check_and_award_badges(appreciation)
```
### Viewing Leaderboard
```python
from apps.appreciation.views import LeaderboardView
# Get hospital leaderboard
view = LeaderboardView()
leaderboard = view.get_queryset()
for entry in leaderboard:
print(f"{entry.rank}: {entry.recipient_name} - {entry.received_count}")
```
## Customization
### Adding Custom Categories
```python
from apps.appreciation.models import AppreciationCategory
category = AppreciationCategory.objects.create(
hospital=hospital, # or None for system-wide
code='custom_category',
name_en='Custom Category',
name_ar='فئة مخصصة',
description_en='Description',
description_ar='الوصف',
icon='fa-star',
color='#FF5733',
order=10,
)
```
### Adding Custom Badges
```python
from apps.appreciation.models import AppreciationBadge
badge = AppreciationBadge.objects.create(
hospital=hospital, # or None for system-wide
code='custom_badge',
name_en='Custom Badge',
name_ar='شارة مخصصة',
icon='fa-medal',
color='#FFD700',
criteria_type='received_count',
criteria_value=100,
)
```
## UI Templates
The app includes a comprehensive UI template at `templates/appreciation/appreciation_list.html`.
Features:
- Dashboard with statistics cards
- Tabbed interface for different views
- Send appreciation modal
- Appreciation detail modal
- Leaderboard display
- Badge gallery
- Responsive design
Access the appreciation page at: `/appreciation/`
## Admin Interface
The app includes a comprehensive admin interface for managing:
- Appreciations
- Categories
- Badges
- User badges
- Statistics
All models are registered in `admin.py` with filters and search functionality.
## Signals
The app uses Django signals to handle:
1. **post_save** on Appreciation:
- Send notifications to recipients
- Update statistics
- Check and award badges
## Permissions
- **Create**: Any authenticated user can send appreciation
- **View**: Users can view their own appreciations
- **Admin**: Admin users can view all appreciations
- **Category Management**: Admin only
- **Badge Management**: Admin only
## Best Practices
1. **Use Categories**: Always select an appropriate category when sending appreciation
2. **Be Specific**: Write detailed messages explaining why the appreciation is being sent
3. **Respect Privacy**: Use appropriate visibility settings
4. **Regular Review**: Regularly check and acknowledge received appreciations
5. **Celebrate Achievements**: Use the leaderboard and badges to celebrate top performers
## Troubleshooting
### Notifications not sending
- Check that the notification system is configured
- Verify recipient email/phone numbers are correct
- Check logs for errors in signals.py
### Badges not awarding
- Run `python manage.py check_badges` to manually trigger badge checks
- Verify criteria are met
- Check that badges are active
### Statistics not updating
- Check that signals are connected
- Verify AppreciationStatus is set to SENT
- Manually trigger statistics recalculation if needed
## Support
For issues or questions, please refer to the main PX360 documentation or contact the development team.

View File

@ -0,0 +1,4 @@
"""
Appreciation app - Send and track appreciation to users and physicians
"""
default_app_config = 'apps.appreciation.apps.AppreciationConfig'

235
apps/appreciation/admin.py Normal file
View File

@ -0,0 +1,235 @@
"""
Appreciation admin - Django admin interface for appreciation models
"""
from django.contrib import admin
from apps.appreciation.models import (
Appreciation,
AppreciationBadge,
AppreciationCategory,
AppreciationStats,
UserBadge,
)
@admin.register(AppreciationCategory)
class AppreciationCategoryAdmin(admin.ModelAdmin):
"""Admin interface for AppreciationCategory"""
list_display = [
'name_en',
'code',
'hospital',
'order',
'is_active',
]
list_filter = ['hospital', 'is_active']
search_fields = ['name_en', 'name_ar', 'code']
list_editable = ['order', 'is_active']
ordering = ['hospital', 'order', 'name_en']
@admin.register(Appreciation)
class AppreciationAdmin(admin.ModelAdmin):
"""Admin interface for Appreciation"""
list_display = [
'recipient_name_display',
'sender_display',
'category',
'status',
'visibility',
'hospital',
'sent_at',
'created_at',
]
list_filter = [
'status',
'visibility',
'hospital',
'category',
'is_anonymous',
'sent_at',
]
search_fields = [
'message_en',
'message_ar',
'sender__first_name',
'sender__last_name',
'sender__email',
]
date_hierarchy = 'sent_at'
readonly_fields = [
'created_at',
'updated_at',
'sent_at',
'acknowledged_at',
'notification_sent_at',
]
fieldsets = (
('Basic Information', {
'fields': (
'sender',
'hospital',
'department',
'category',
)
}),
('Recipient', {
'fields': (
'recipient_content_type',
'recipient_object_id',
)
}),
('Content', {
'fields': (
'message_en',
'message_ar',
)
}),
('Settings', {
'fields': (
'visibility',
'is_anonymous',
)
}),
('Status', {
'fields': (
'status',
'sent_at',
'acknowledged_at',
'notification_sent',
'notification_sent_at',
)
}),
('Metadata', {
'fields': (
'metadata',
)
}),
('Timestamps', {
'fields': (
'created_at',
'updated_at',
),
'classes': ('collapse',)
}),
)
def recipient_name_display(self, obj):
"""Display recipient name in list"""
try:
return str(obj.recipient)
except:
return "Unknown"
recipient_name_display.short_description = 'Recipient'
def sender_display(self, obj):
"""Display sender name in list"""
if obj.is_anonymous:
return "Anonymous"
if obj.sender:
return obj.sender.get_full_name()
return "System"
sender_display.short_description = 'Sender'
@admin.register(AppreciationBadge)
class AppreciationBadgeAdmin(admin.ModelAdmin):
"""Admin interface for AppreciationBadge"""
list_display = [
'name_en',
'code',
'criteria_type',
'criteria_value',
'hospital',
'order',
'is_active',
]
list_filter = [
'hospital',
'criteria_type',
'is_active',
]
search_fields = [
'name_en',
'name_ar',
'code',
'description_en',
]
list_editable = ['order', 'is_active']
ordering = ['hospital', 'order', 'name_en']
@admin.register(UserBadge)
class UserBadgeAdmin(admin.ModelAdmin):
"""Admin interface for UserBadge"""
list_display = [
'recipient_name_display',
'badge',
'earned_at',
'appreciation_count',
]
list_filter = [
'badge',
'earned_at',
]
search_fields = [
'badge__name_en',
'badge__name_ar',
]
date_hierarchy = 'earned_at'
readonly_fields = ['earned_at', 'created_at', 'updated_at']
def recipient_name_display(self, obj):
"""Display recipient name in list"""
try:
return str(obj.recipient)
except:
return "Unknown"
recipient_name_display.short_description = 'Recipient'
@admin.register(AppreciationStats)
class AppreciationStatsAdmin(admin.ModelAdmin):
"""Admin interface for AppreciationStats"""
list_display = [
'recipient_name_display',
'period_display',
'hospital',
'department',
'received_count',
'sent_count',
'acknowledged_count',
'hospital_rank',
'department_rank',
]
list_filter = [
'hospital',
'department',
'year',
'month',
]
search_fields = []
date_hierarchy = None
readonly_fields = [
'created_at',
'updated_at',
]
def recipient_name_display(self, obj):
"""Display recipient name in list"""
try:
return str(obj.recipient)
except:
return "Unknown"
recipient_name_display.short_description = 'Recipient'
def period_display(self, obj):
"""Display period in list"""
return f"{obj.year}-{obj.month:02d}"
period_display.short_description = 'Period'

16
apps/appreciation/apps.py Normal file
View File

@ -0,0 +1,16 @@
"""
Appreciation app configuration
"""
from django.apps import AppConfig
class AppreciationConfig(AppConfig):
"""Configuration for the appreciation app"""
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.appreciation'
verbose_name = 'Appreciation System'
def ready(self):
"""Import signals when app is ready"""
import apps.appreciation.signals

View File

@ -0,0 +1 @@
# Management module

View File

@ -0,0 +1 @@
# Commands module

View File

@ -0,0 +1,246 @@
"""
Management command to seed appreciation categories and badges
Usage:
python manage.py seed_appreciation_data
"""
from django.core.management.base import BaseCommand
from django.db import transaction
from apps.appreciation.models import (
AppreciationBadge,
AppreciationCategory,
)
class Command(BaseCommand):
help = 'Seed appreciation categories and badges'
def handle(self, *args, **options):
self.stdout.write('Seeding appreciation data...')
with transaction.atomic():
# Seed categories
self.seed_categories()
# Seed badges
self.seed_badges()
self.stdout.write(self.style.SUCCESS('Successfully seeded appreciation data!'))
def seed_categories(self):
"""Seed appreciation categories"""
categories = [
{
'code': 'excellent_care',
'name_en': 'Excellent Care',
'name_ar': 'رعاية ممتازة',
'description_en': 'For providing exceptional patient care',
'description_ar': 'لتقديم رعاية استثنائية للمرضى',
'icon': 'fa-heart-pulse',
'color': '#FF5733',
'order': 1,
},
{
'code': 'team_player',
'name_en': 'Team Player',
'name_ar': 'لاعب فريق',
'description_en': 'For outstanding teamwork and collaboration',
'description_ar': 'للعمل الجماعي والتعاون المتميز',
'icon': 'fa-users',
'color': '#33FF57',
'order': 2,
},
{
'code': 'innovation',
'name_en': 'Innovation',
'name_ar': 'الابتكار',
'description_en': 'For introducing innovative solutions',
'description_ar': 'لتقديم حلول مبتكرة',
'icon': 'fa-lightbulb',
'color': '#F3FF33',
'order': 3,
},
{
'code': 'leadership',
'name_en': 'Leadership',
'name_ar': 'القيادة',
'description_en': 'For demonstrating exceptional leadership',
'description_ar': 'لإظهار قيادة استثنائية',
'icon': 'fa-crown',
'color': '#FFD700',
'order': 4,
},
{
'code': 'mentorship',
'name_en': 'Mentorship',
'name_ar': 'التوجيه',
'description_en': 'For guiding and supporting colleagues',
'description_ar': 'لإرشاد ودعم الزملاء',
'icon': 'fa-hands-holding-child',
'color': '#33FFF3',
'order': 5,
},
{
'code': 'going_extra_mile',
'name_en': 'Going the Extra Mile',
'name_ar': 'بذل جهد إضافي',
'description_en': 'For going above and beyond',
'description_ar': 'للتجاوز التوقعات',
'icon': 'fa-rocket',
'color': '#FF33F3',
'order': 6,
},
{
'code': 'reliability',
'name_en': 'Reliability',
'name_ar': 'الموثوقية',
'description_en': 'For consistent and dependable performance',
'description_ar': 'للأداء المتسابق والموثوق',
'icon': 'fa-shield-check',
'color': '#3357FF',
'order': 7,
},
{
'code': 'positive_attitude',
'name_en': 'Positive Attitude',
'name_ar': 'التفاؤل',
'description_en': 'For maintaining a positive outlook',
'description_ar': 'للحفاظ على التفاؤل',
'icon': 'fa-face-smile',
'color': '#FFA500',
'order': 8,
},
]
for category_data in categories:
category, created = AppreciationCategory.objects.get_or_create(
code=category_data['code'],
hospital=None, # System-wide categories
defaults=category_data
)
if created:
self.stdout.write(
self.style.SUCCESS(f"Created category: {category.name_en}")
)
else:
self.stdout.write(
self.style.WARNING(f"Category already exists: {category.name_en}")
)
def seed_badges(self):
"""Seed appreciation badges"""
badges = [
{
'code': 'first_appreciation',
'name_en': 'First Appreciation',
'name_ar': 'أول تكريم',
'description_en': 'Received your first appreciation',
'description_ar': 'حصلت على أول تكريم',
'icon': 'fa-star',
'color': '#FFD700',
'criteria_type': 'received_count',
'criteria_value': 1,
'order': 1,
},
{
'code': 'appreciated_5',
'name_en': 'Rising Star',
'name_ar': 'نجم صاعد',
'description_en': 'Received 5 appreciations',
'description_ar': 'حصلت على 5 تكريمات',
'icon': 'fa-star-half-stroke',
'color': '#C0C0C0',
'criteria_type': 'received_count',
'criteria_value': 5,
'order': 2,
},
{
'code': 'appreciated_10',
'name_en': 'Shining Star',
'name_ar': 'نجم لامع',
'description_en': 'Received 10 appreciations',
'description_ar': 'حصلت على 10 تكريمات',
'icon': 'fa-star',
'color': '#FFD700',
'criteria_type': 'received_count',
'criteria_value': 10,
'order': 3,
},
{
'code': 'appreciated_25',
'name_en': 'Super Star',
'name_ar': 'نجم خارق',
'description_en': 'Received 25 appreciations',
'description_ar': 'حصلت على 25 تكريم',
'icon': 'fa-trophy',
'color': '#FFA500',
'criteria_type': 'received_count',
'criteria_value': 25,
'order': 4,
},
{
'code': 'appreciated_50',
'name_en': 'Legendary',
'name_ar': 'أسطوري',
'description_en': 'Received 50 appreciations',
'description_ar': 'حصلت على 50 تكريم',
'icon': 'fa-crown',
'color': '#FF5733',
'criteria_type': 'received_count',
'criteria_value': 50,
'order': 5,
},
{
'code': 'monthly_champion',
'name_en': 'Monthly Champion',
'name_ar': 'بطل الشهر',
'description_en': 'Received 10 appreciations in a month',
'description_ar': 'حصلت على 10 تكريمات في شهر واحد',
'icon': 'fa-medal',
'color': '#33FF57',
'criteria_type': 'received_month',
'criteria_value': 10,
'order': 6,
},
{
'code': 'streak_4_weeks',
'name_en': 'Consistent',
'name_ar': 'مستمر',
'description_en': 'Received appreciations for 4 consecutive weeks',
'description_ar': 'حصلت على تكريمات لمدة 4 أسابيع متتالية',
'icon': 'fa-fire',
'color': '#FF4500',
'criteria_type': 'streak_weeks',
'criteria_value': 4,
'order': 7,
},
{
'code': 'diverse_appreciation',
'name_en': 'Well-Loved',
'name_ar': 'محبوب',
'description_en': 'Received appreciations from 10 different people',
'description_ar': 'حصلت على تكريمات من 10 أشخاص مختلفين',
'icon': 'fa-heart',
'color': '#FF1493',
'criteria_type': 'diverse_senders',
'criteria_value': 10,
'order': 8,
},
]
for badge_data in badges:
badge, created = AppreciationBadge.objects.get_or_create(
code=badge_data['code'],
defaults=badge_data
)
if created:
self.stdout.write(
self.style.SUCCESS(f"Created badge: {badge.name_en}")
)
else:
self.stdout.write(
self.style.WARNING(f"Badge already exists: {badge.name_en}")
)

View File

@ -0,0 +1,185 @@
# Generated by Django 5.0.14 on 2026-01-01 11:27
import django.db.models.deletion
import uuid
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
('organizations', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='AppreciationBadge',
fields=[
('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('code', models.CharField(help_text='Unique badge code', max_length=50, unique=True)),
('name_en', models.CharField(max_length=200)),
('name_ar', models.CharField(blank=True, max_length=200)),
('description_en', models.TextField(blank=True)),
('description_ar', models.TextField(blank=True)),
('icon', models.CharField(blank=True, help_text='Icon class', max_length=50)),
('color', models.CharField(blank=True, help_text='Hex color code', max_length=7)),
('criteria_type', models.CharField(choices=[('received_count', 'Total Appreciations Received'), ('received_month', 'Appreciations Received in a Month'), ('streak_weeks', 'Consecutive Weeks with Appreciation'), ('diverse_senders', 'Appreciations from Different Senders')], db_index=True, max_length=50)),
('criteria_value', models.IntegerField(help_text='Value to achieve (e.g., 10 for 10 appreciations)')),
('order', models.IntegerField(default=0)),
('is_active', models.BooleanField(default=True)),
('hospital', models.ForeignKey(blank=True, help_text='Leave blank for system-wide badges', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='appreciation_badges', to='organizations.hospital')),
],
options={
'ordering': ['hospital', 'order', 'name_en'],
},
),
migrations.CreateModel(
name='AppreciationCategory',
fields=[
('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('code', models.CharField(help_text='Unique code for this category', max_length=50)),
('name_en', models.CharField(max_length=200)),
('name_ar', models.CharField(blank=True, max_length=200)),
('description_en', models.TextField(blank=True)),
('description_ar', models.TextField(blank=True)),
('icon', models.CharField(blank=True, help_text="Icon class (e.g., 'fa-heart', 'fa-star')", max_length=50)),
('color', models.CharField(blank=True, help_text="Hex color code (e.g., '#FF5733')", max_length=7)),
('order', models.IntegerField(default=0, help_text='Display order')),
('is_active', models.BooleanField(default=True)),
('hospital', models.ForeignKey(blank=True, help_text='Leave blank for system-wide categories', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='appreciation_categories', to='organizations.hospital')),
],
options={
'verbose_name_plural': 'Appreciation Categories',
'ordering': ['hospital', 'order', 'name_en'],
},
),
migrations.CreateModel(
name='Appreciation',
fields=[
('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('recipient_object_id', models.UUIDField(blank=True, null=True)),
('message_en', models.TextField()),
('message_ar', models.TextField(blank=True)),
('visibility', models.CharField(choices=[('private', 'Private'), ('department', 'Department'), ('hospital', 'Hospital'), ('public', 'Public')], db_index=True, default='private', max_length=20)),
('status', models.CharField(choices=[('draft', 'Draft'), ('sent', 'Sent'), ('acknowledged', 'Acknowledged')], db_index=True, default='draft', max_length=20)),
('is_anonymous', models.BooleanField(default=False, help_text='Hide sender identity from recipient')),
('sent_at', models.DateTimeField(blank=True, null=True)),
('acknowledged_at', models.DateTimeField(blank=True, null=True)),
('notification_sent', models.BooleanField(default=False)),
('notification_sent_at', models.DateTimeField(blank=True, null=True)),
('metadata', models.JSONField(blank=True, default=dict)),
('department', models.ForeignKey(blank=True, help_text='Department context (if applicable)', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='appreciations', to='organizations.department')),
('hospital', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='appreciations', to='organizations.hospital')),
('recipient_content_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='appreciation_recipients', to='contenttypes.contenttype')),
('sender', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sent_appreciations', to=settings.AUTH_USER_MODEL)),
('category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='appreciations', to='appreciation.appreciationcategory')),
],
options={
'ordering': ['-created_at'],
},
),
migrations.CreateModel(
name='AppreciationStats',
fields=[
('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('recipient_object_id', models.UUIDField(blank=True, null=True)),
('year', models.IntegerField(db_index=True)),
('month', models.IntegerField(db_index=True, help_text='1-12')),
('received_count', models.IntegerField(default=0)),
('sent_count', models.IntegerField(default=0)),
('acknowledged_count', models.IntegerField(default=0)),
('hospital_rank', models.IntegerField(blank=True, help_text='Rank within hospital', null=True)),
('department_rank', models.IntegerField(blank=True, help_text='Rank within department', null=True)),
('category_breakdown', models.JSONField(blank=True, default=dict, help_text='Breakdown by category ID and count')),
('metadata', models.JSONField(blank=True, default=dict)),
('department', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='appreciation_stats', to='organizations.department')),
('hospital', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='appreciation_stats', to='organizations.hospital')),
('recipient_content_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='appreciation_stats_recipients', to='contenttypes.contenttype')),
],
options={
'ordering': ['-year', '-month', '-received_count'],
},
),
migrations.CreateModel(
name='UserBadge',
fields=[
('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('recipient_object_id', models.UUIDField(blank=True, null=True)),
('earned_at', models.DateTimeField(auto_now_add=True)),
('appreciation_count', models.IntegerField(help_text='Count when badge was earned')),
('metadata', models.JSONField(blank=True, default=dict)),
('badge', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='earned_by', to='appreciation.appreciationbadge')),
('recipient_content_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='earned_badges_recipients', to='contenttypes.contenttype')),
],
options={
'ordering': ['-earned_at'],
},
),
migrations.AddIndex(
model_name='appreciationbadge',
index=models.Index(fields=['hospital', 'is_active'], name='appreciatio_hospita_3847f7_idx'),
),
migrations.AddIndex(
model_name='appreciationbadge',
index=models.Index(fields=['code'], name='appreciatio_code_416153_idx'),
),
migrations.AddIndex(
model_name='appreciationcategory',
index=models.Index(fields=['hospital', 'is_active'], name='appreciatio_hospita_b8e413_idx'),
),
migrations.AddIndex(
model_name='appreciationcategory',
index=models.Index(fields=['code'], name='appreciatio_code_50215a_idx'),
),
migrations.AlterUniqueTogether(
name='appreciationcategory',
unique_together={('hospital', 'code')},
),
migrations.AddIndex(
model_name='appreciation',
index=models.Index(fields=['status', '-created_at'], name='appreciatio_status_24158d_idx'),
),
migrations.AddIndex(
model_name='appreciation',
index=models.Index(fields=['hospital', 'status'], name='appreciatio_hospita_db3f34_idx'),
),
migrations.AddIndex(
model_name='appreciation',
index=models.Index(fields=['recipient_content_type', 'recipient_object_id', '-created_at'], name='appreciatio_recipie_71ef0e_idx'),
),
migrations.AddIndex(
model_name='appreciation',
index=models.Index(fields=['visibility', '-created_at'], name='appreciatio_visibil_ed96d9_idx'),
),
migrations.AddIndex(
model_name='appreciationstats',
index=models.Index(fields=['hospital', 'year', 'month', '-received_count'], name='appreciatio_hospita_a0d454_idx'),
),
migrations.AddIndex(
model_name='appreciationstats',
index=models.Index(fields=['department', 'year', 'month', '-received_count'], name='appreciatio_departm_f68345_idx'),
),
migrations.AlterUniqueTogether(
name='appreciationstats',
unique_together={('recipient_content_type', 'recipient_object_id', 'year', 'month')},
),
migrations.AddIndex(
model_name='userbadge',
index=models.Index(fields=['recipient_content_type', 'recipient_object_id', '-earned_at'], name='appreciatio_recipie_fc90c8_idx'),
),
]

View File

@ -0,0 +1 @@
# Migrations module

466
apps/appreciation/models.py Normal file
View File

@ -0,0 +1,466 @@
"""
Appreciation models - Send and track appreciation to users and physicians
This module implements the appreciation system that:
- Allows sending appreciation messages to users and physicians
- Tracks appreciation statistics and leaderboards
- Manages appreciation categories and badges
- Integrates with the notification system
"""
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.db import models
from apps.core.models import TimeStampedModel, UUIDModel
class AppreciationStatus(models.TextChoices):
"""Appreciation status choices"""
DRAFT = 'draft', 'Draft'
SENT = 'sent', 'Sent'
ACKNOWLEDGED = 'acknowledged', 'Acknowledged'
class AppreciationVisibility(models.TextChoices):
"""Appreciation visibility choices"""
PRIVATE = 'private', 'Private'
DEPARTMENT = 'department', 'Department'
HOSPITAL = 'hospital', 'Hospital'
PUBLIC = 'public', 'Public'
class AppreciationCategory(UUIDModel, TimeStampedModel):
"""
Appreciation category with bilingual support.
Pre-defined categories for sending appreciation (e.g., "Excellent Care", "Team Player").
Can be hospital-specific or system-wide.
"""
# Organization
hospital = models.ForeignKey(
'organizations.Hospital',
on_delete=models.CASCADE,
related_name='appreciation_categories',
null=True,
blank=True,
help_text="Leave blank for system-wide categories"
)
# Category details
code = models.CharField(
max_length=50,
help_text="Unique code for this category"
)
name_en = models.CharField(max_length=200)
name_ar = models.CharField(max_length=200, blank=True)
description_en = models.TextField(blank=True)
description_ar = models.TextField(blank=True)
# Visual elements
icon = models.CharField(
max_length=50,
blank=True,
help_text="Icon class (e.g., 'fa-heart', 'fa-star')"
)
color = models.CharField(
max_length=7,
blank=True,
help_text="Hex color code (e.g., '#FF5733')"
)
# Display order
order = models.IntegerField(default=0, help_text="Display order")
# Status
is_active = models.BooleanField(default=True)
class Meta:
ordering = ['hospital', 'order', 'name_en']
unique_together = [['hospital', 'code']]
verbose_name_plural = 'Appreciation Categories'
indexes = [
models.Index(fields=['hospital', 'is_active']),
models.Index(fields=['code']),
]
def __str__(self):
hospital_name = self.hospital.name if self.hospital else "System-wide"
return f"{hospital_name} - {self.name_en}"
class Appreciation(UUIDModel, TimeStampedModel):
"""
Appreciation model for sending appreciation to users and physicians.
Workflow:
1. DRAFT - Created but not yet sent
2. SENT - Sent to recipient (triggers notification)
3. ACKNOWLEDGED - Recipient has acknowledged/thanked sender
Visibility:
- PRIVATE - Only sender and recipient can view
- DEPARTMENT - Visible to department members
- HOSPITAL - Visible to all hospital staff
- PUBLIC - Can be displayed publicly (e.g., on appreciation wall)
"""
# Sender
sender = models.ForeignKey(
'accounts.User',
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='sent_appreciations'
)
# Recipient (Generic FK to User or Physician)
recipient_content_type = models.ForeignKey(
ContentType,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='appreciation_recipients'
)
recipient_object_id = models.UUIDField(null=True, blank=True)
recipient = GenericForeignKey(
'recipient_content_type',
'recipient_object_id'
)
# Organization context
hospital = models.ForeignKey(
'organizations.Hospital',
on_delete=models.CASCADE,
related_name='appreciations'
)
department = models.ForeignKey(
'organizations.Department',
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='appreciations',
help_text="Department context (if applicable)"
)
# Category and message
category = models.ForeignKey(
AppreciationCategory,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='appreciations'
)
message_en = models.TextField()
message_ar = models.TextField(blank=True)
# Visibility and status
visibility = models.CharField(
max_length=20,
choices=AppreciationVisibility.choices,
default=AppreciationVisibility.PRIVATE,
db_index=True
)
status = models.CharField(
max_length=20,
choices=AppreciationStatus.choices,
default=AppreciationStatus.DRAFT,
db_index=True
)
# Anonymous option
is_anonymous = models.BooleanField(
default=False,
help_text="Hide sender identity from recipient"
)
# Timestamps
sent_at = models.DateTimeField(null=True, blank=True)
acknowledged_at = models.DateTimeField(null=True, blank=True)
# Notification tracking
notification_sent = models.BooleanField(default=False)
notification_sent_at = models.DateTimeField(null=True, blank=True)
# Metadata
metadata = models.JSONField(default=dict, blank=True)
class Meta:
ordering = ['-created_at']
indexes = [
models.Index(fields=['status', '-created_at']),
models.Index(fields=['hospital', 'status']),
models.Index(fields=['recipient_content_type', 'recipient_object_id', '-created_at']),
models.Index(fields=['visibility', '-created_at']),
]
def __str__(self):
recipient_name = self.get_recipient_name()
return f"Appreciation to {recipient_name} ({self.status})"
def get_recipient_name(self):
"""Get recipient's name"""
try:
return str(self.recipient)
except:
return "Unknown"
def get_recipient_email(self):
"""Get recipient's email"""
try:
if hasattr(self.recipient, 'email'):
return self.recipient.email
elif hasattr(self.recipient, 'user'):
return self.recipient.user.email
except:
pass
return None
def get_recipient_phone(self):
"""Get recipient's phone"""
try:
if hasattr(self.recipient, 'phone'):
return self.recipient.phone
elif hasattr(self.recipient, 'user'):
return self.recipient.user.phone
except:
pass
return None
def send(self):
"""Send appreciation and trigger notification"""
from django.utils import timezone
self.status = AppreciationStatus.SENT
self.sent_at = timezone.now()
self.save(update_fields=['status', 'sent_at'])
# Trigger notification
self.send_notification()
def acknowledge(self):
"""Mark appreciation as acknowledged"""
from django.utils import timezone
self.status = AppreciationStatus.ACKNOWLEDGED
self.acknowledged_at = timezone.now()
self.save(update_fields=['status', 'acknowledged_at'])
def send_notification(self):
"""Send notification to recipient"""
# This will be implemented in signals.py
pass
class AppreciationBadge(UUIDModel, TimeStampedModel):
"""
Appreciation badge for gamification.
Badges are awarded based on achievements (e.g., "10 Appreciations Received").
"""
# Organization
hospital = models.ForeignKey(
'organizations.Hospital',
on_delete=models.CASCADE,
related_name='appreciation_badges',
null=True,
blank=True,
help_text="Leave blank for system-wide badges"
)
# Badge details
code = models.CharField(
max_length=50,
unique=True,
help_text="Unique badge code"
)
name_en = models.CharField(max_length=200)
name_ar = models.CharField(max_length=200, blank=True)
description_en = models.TextField(blank=True)
description_ar = models.TextField(blank=True)
# Visual elements
icon = models.CharField(
max_length=50,
blank=True,
help_text="Icon class"
)
color = models.CharField(
max_length=7,
blank=True,
help_text="Hex color code"
)
# Achievement criteria
criteria_type = models.CharField(
max_length=50,
choices=[
('received_count', 'Total Appreciations Received'),
('received_month', 'Appreciations Received in a Month'),
('streak_weeks', 'Consecutive Weeks with Appreciation'),
('diverse_senders', 'Appreciations from Different Senders'),
],
db_index=True
)
criteria_value = models.IntegerField(
help_text="Value to achieve (e.g., 10 for 10 appreciations)"
)
# Display order
order = models.IntegerField(default=0)
# Status
is_active = models.BooleanField(default=True)
class Meta:
ordering = ['hospital', 'order', 'name_en']
indexes = [
models.Index(fields=['hospital', 'is_active']),
models.Index(fields=['code']),
]
def __str__(self):
hospital_name = self.hospital.name if self.hospital else "System-wide"
return f"{hospital_name} - {self.name_en}"
class UserBadge(UUIDModel, TimeStampedModel):
"""
User badge - tracks badges earned by users.
Links users (or physicians) to badges they've earned.
"""
# Recipient (Generic FK to User or Physician)
recipient_content_type = models.ForeignKey(
ContentType,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='earned_badges_recipients'
)
recipient_object_id = models.UUIDField(null=True, blank=True)
recipient = GenericForeignKey(
'recipient_content_type',
'recipient_object_id'
)
# Badge
badge = models.ForeignKey(
AppreciationBadge,
on_delete=models.CASCADE,
related_name='earned_by'
)
# Achievement details
earned_at = models.DateTimeField(auto_now_add=True)
# Context
appreciation_count = models.IntegerField(
help_text="Count when badge was earned"
)
# Metadata
metadata = models.JSONField(default=dict, blank=True)
class Meta:
ordering = ['-earned_at']
indexes = [
models.Index(fields=['recipient_content_type', 'recipient_object_id', '-earned_at']),
]
def __str__(self):
recipient_name = self.get_recipient_name()
return f"{recipient_name} - {self.badge.name_en}"
def get_recipient_name(self):
"""Get recipient's name"""
try:
return str(self.recipient)
except:
return "Unknown"
class AppreciationStats(UUIDModel, TimeStampedModel):
"""
Appreciation statistics - aggregated monthly.
Provides monthly statistics for users and physicians.
"""
# Recipient (Generic FK to User or Physician)
recipient_content_type = models.ForeignKey(
ContentType,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='appreciation_stats_recipients'
)
recipient_object_id = models.UUIDField(null=True, blank=True)
recipient = GenericForeignKey(
'recipient_content_type',
'recipient_object_id'
)
# Time period
year = models.IntegerField(db_index=True)
month = models.IntegerField(db_index=True, help_text="1-12")
# Organization
hospital = models.ForeignKey(
'organizations.Hospital',
on_delete=models.CASCADE,
related_name='appreciation_stats'
)
department = models.ForeignKey(
'organizations.Department',
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='appreciation_stats'
)
# Statistics
received_count = models.IntegerField(default=0)
sent_count = models.IntegerField(default=0)
acknowledged_count = models.IntegerField(default=0)
# Ranking
hospital_rank = models.IntegerField(
null=True,
blank=True,
help_text="Rank within hospital"
)
department_rank = models.IntegerField(
null=True,
blank=True,
help_text="Rank within department"
)
# Breakdown by category
category_breakdown = models.JSONField(
default=dict,
blank=True,
help_text="Breakdown by category ID and count"
)
# Metadata
metadata = models.JSONField(default=dict, blank=True)
class Meta:
ordering = ['-year', '-month', '-received_count']
unique_together = [
['recipient_content_type', 'recipient_object_id', 'year', 'month']
]
indexes = [
models.Index(fields=['hospital', 'year', 'month', '-received_count']),
models.Index(fields=['department', 'year', 'month', '-received_count']),
]
def __str__(self):
recipient_name = self.get_recipient_name()
return f"{recipient_name} - {self.year}-{self.month:02d}: {self.received_count} received"
def get_recipient_name(self):
"""Get recipient's name"""
try:
return str(self.recipient)
except:
return "Unknown"

View File

@ -0,0 +1,361 @@
"""
Appreciation serializers - API serializers for appreciation models
"""
from rest_framework import serializers
from apps.appreciation.models import (
Appreciation,
AppreciationBadge,
AppreciationCategory,
AppreciationStatus,
AppreciationVisibility,
AppreciationStats,
UserBadge,
)
class AppreciationCategorySerializer(serializers.ModelSerializer):
"""Serializer for AppreciationCategory"""
class Meta:
model = AppreciationCategory
fields = [
'id',
'code',
'name_en',
'name_ar',
'description_en',
'description_ar',
'icon',
'color',
'order',
'is_active',
'hospital',
'created_at',
'updated_at',
]
read_only_fields = ['id', 'created_at', 'updated_at']
class AppreciationSerializer(serializers.ModelSerializer):
"""Serializer for Appreciation"""
sender_name = serializers.SerializerMethodField()
recipient_name = serializers.SerializerMethodField()
recipient_type = serializers.SerializerMethodField()
recipient_id = serializers.SerializerMethodField()
category_name = serializers.SerializerMethodField()
message = serializers.SerializerMethodField()
class Meta:
model = Appreciation
fields = [
'id',
'sender',
'sender_name',
'recipient',
'recipient_type',
'recipient_id',
'recipient_name',
'hospital',
'department',
'category',
'category_name',
'message_en',
'message_ar',
'message',
'visibility',
'status',
'is_anonymous',
'sent_at',
'acknowledged_at',
'notification_sent',
'notification_sent_at',
'metadata',
'created_at',
'updated_at',
]
read_only_fields = [
'id',
'sender_name',
'recipient_name',
'recipient_type',
'recipient_id',
'category_name',
'message',
'sent_at',
'acknowledged_at',
'notification_sent',
'notification_sent_at',
'created_at',
'updated_at',
]
def get_sender_name(self, obj):
"""Get sender's name"""
if obj.is_anonymous:
return "Anonymous"
if obj.sender:
return obj.sender.get_full_name()
return "System"
def get_recipient_name(self, obj):
"""Get recipient's name"""
try:
return str(obj.recipient)
except:
return "Unknown"
def get_recipient_type(self, obj):
"""Get recipient type (user or physician)"""
if obj.recipient_content_type:
return obj.recipient_content_type.model
return None
def get_recipient_id(self, obj):
"""Get recipient object ID"""
return obj.recipient_object_id
def get_category_name(self, obj):
"""Get category name"""
if obj.category:
return obj.category.name_en
return None
def get_message(self, obj):
"""Get message based on language"""
# This should use the request's language context
# For now, default to English
return obj.message_en
class AppreciationCreateSerializer(serializers.Serializer):
"""Serializer for creating Appreciation"""
recipient_type = serializers.ChoiceField(
choices=['user', 'physician'],
write_only=True,
help_text="Type of recipient: 'user' or 'physician'"
)
recipient_id = serializers.UUIDField(write_only=True)
category_id = serializers.UUIDField(required=False, allow_null=True)
message_en = serializers.CharField(required=True)
message_ar = serializers.CharField(required=False, allow_blank=True)
visibility = serializers.ChoiceField(
choices=AppreciationVisibility.choices,
default=AppreciationVisibility.PRIVATE
)
is_anonymous = serializers.BooleanField(default=False)
hospital_id = serializers.UUIDField(required=True)
department_id = serializers.UUIDField(required=False, allow_null=True)
def validate_recipient_id(self, value):
"""Validate recipient ID"""
# Will be validated in validate method
return value
def validate(self, data):
"""Validate the entire data"""
recipient_type = data.get('recipient_type')
recipient_id = data.get('recipient_id')
hospital_id = data.get('hospital_id')
# Validate recipient exists
from django.contrib.contenttypes.models import ContentType
if recipient_type == 'user':
from apps.accounts.models import User
try:
user = User.objects.get(id=recipient_id)
# Check if user belongs to hospital
if user.hospital_id != hospital_id:
raise serializers.ValidationError({
'recipient_id': 'User does not belong to specified hospital'
})
except User.DoesNotExist:
raise serializers.ValidationError({
'recipient_id': 'User not found'
})
elif recipient_type == 'physician':
from apps.organizations.models import Physician
try:
physician = Physician.objects.get(id=recipient_id)
if physician.hospital_id != hospital_id:
raise serializers.ValidationError({
'recipient_id': 'Physician does not belong to specified hospital'
})
except Physician.DoesNotExist:
raise serializers.ValidationError({
'recipient_id': 'Physician not found'
})
# Validate category if provided
category_id = data.get('category_id')
if category_id:
try:
category = AppreciationCategory.objects.get(id=category_id)
if category.hospital_id and category.hospital_id != hospital_id:
raise serializers.ValidationError({
'category_id': 'Category does not belong to specified hospital'
})
except AppreciationCategory.DoesNotExist:
raise serializers.ValidationError({
'category_id': 'Category not found'
})
return data
class AppreciationBadgeSerializer(serializers.ModelSerializer):
"""Serializer for AppreciationBadge"""
class Meta:
model = AppreciationBadge
fields = [
'id',
'code',
'name_en',
'name_ar',
'description_en',
'description_ar',
'icon',
'color',
'criteria_type',
'criteria_value',
'order',
'is_active',
'hospital',
'created_at',
'updated_at',
]
read_only_fields = ['id', 'created_at', 'updated_at']
class UserBadgeSerializer(serializers.ModelSerializer):
"""Serializer for UserBadge"""
recipient_name = serializers.SerializerMethodField()
recipient_type = serializers.SerializerMethodField()
badge_details = AppreciationBadgeSerializer(source='badge', read_only=True)
class Meta:
model = UserBadge
fields = [
'id',
'recipient',
'recipient_name',
'recipient_type',
'badge',
'badge_details',
'earned_at',
'appreciation_count',
'metadata',
'created_at',
'updated_at',
]
read_only_fields = [
'id',
'recipient_name',
'recipient_type',
'earned_at',
'created_at',
'updated_at',
]
def get_recipient_name(self, obj):
"""Get recipient's name"""
try:
return str(obj.recipient)
except:
return "Unknown"
def get_recipient_type(self, obj):
"""Get recipient type"""
if obj.recipient_content_type:
return obj.recipient_content_type.model
return None
class AppreciationStatsSerializer(serializers.ModelSerializer):
"""Serializer for AppreciationStats"""
recipient_name = serializers.SerializerMethodField()
recipient_type = serializers.SerializerMethodField()
period_display = serializers.SerializerMethodField()
class Meta:
model = AppreciationStats
fields = [
'id',
'recipient',
'recipient_name',
'recipient_type',
'year',
'month',
'period_display',
'hospital',
'department',
'received_count',
'sent_count',
'acknowledged_count',
'hospital_rank',
'department_rank',
'category_breakdown',
'metadata',
'created_at',
'updated_at',
]
read_only_fields = [
'id',
'recipient_name',
'recipient_type',
'period_display',
'created_at',
'updated_at',
]
def get_recipient_name(self, obj):
"""Get recipient's name"""
try:
return str(obj.recipient)
except:
return "Unknown"
def get_recipient_type(self, obj):
"""Get recipient type"""
if obj.recipient_content_type:
return obj.recipient_content_type.model
return None
def get_period_display(self, obj):
"""Get formatted period display"""
return f"{obj.year}-{obj.month:02d}"
class AppreciationLeaderboardSerializer(serializers.Serializer):
"""Serializer for appreciation leaderboard"""
recipient_type = serializers.CharField()
recipient_id = serializers.UUIDField()
recipient_name = serializers.CharField()
hospital = serializers.CharField()
department = serializers.CharField(required=False, allow_null=True)
received_count = serializers.IntegerField()
rank = serializers.IntegerField()
badges = serializers.ListField(
child=serializers.DictField(),
required=False
)
class AppreciationSummarySerializer(serializers.Serializer):
"""Serializer for appreciation summary statistics"""
total_received = serializers.IntegerField()
total_sent = serializers.IntegerField()
this_month_received = serializers.IntegerField()
this_month_sent = serializers.IntegerField()
top_category = serializers.DictField(required=False, allow_null=True)
badges_earned = serializers.IntegerField()
hospital_rank = serializers.IntegerField(required=False, allow_null=True)
department_rank = serializers.IntegerField(required=False, allow_null=True)

View File

@ -0,0 +1,348 @@
"""
Appreciation signals - Signal handlers for appreciation events
This module handles:
- Sending notifications when appreciations are sent
- Updating statistics when appreciations are created
- Checking and awarding badges
"""
from django.db.models import Q
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.utils import timezone
from apps.appreciation.models import (
Appreciation,
AppreciationBadge,
AppreciationStats,
AppreciationStatus,
UserBadge,
)
@receiver(post_save, sender=Appreciation)
def handle_appreciation_sent(sender, instance, created, **kwargs):
"""
Handle appreciation sent events.
This signal triggers:
1. Send notification to recipient
2. Update statistics
3. Check for badge awards
"""
# Only process when appreciation is sent (status changes to 'sent')
if instance.status == AppreciationStatus.SENT and not instance.notification_sent:
# Send notification
send_appreciation_notification(instance)
# Update statistics
update_appreciation_stats(instance)
# Check for badge awards
check_and_award_badges(instance)
def send_appreciation_notification(appreciation):
"""
Send notification to recipient when appreciation is sent.
Uses the notification system to send email/SMS/WhatsApp.
"""
try:
from apps.notifications.models import NotificationLog, NotificationChannel, NotificationStatus
from apps.notifications.services import send_notification
# Get recipient details
recipient_email = appreciation.get_recipient_email()
recipient_phone = appreciation.get_recipient_phone()
# Get sender name
sender_name = "Anonymous" if appreciation.is_anonymous else appreciation.sender.get_full_name()
# Build message
message_en = f"You've received an appreciation from {sender_name}!"
message_ar = f"لقد تلقيت تكريماً من {sender_name}!"
if appreciation.category:
message_en += f"\n\nCategory: {appreciation.category.name_en}"
message_ar += f"\n\nالفئة: {appreciation.category.name_ar}"
message_en += f"\n\nMessage: {appreciation.message_en}"
message_ar += f"\n\nالرسالة: {appreciation.message_ar}"
# Send email if available
if recipient_email:
try:
send_notification(
channel=NotificationChannel.EMAIL,
recipient=recipient_email,
subject=f"New Appreciation Received - {appreciation.hospital.name}",
message=message_en,
content_object=appreciation,
)
except Exception as e:
# Log error but don't fail
print(f"Failed to send appreciation email: {e}")
# Send SMS if available
if recipient_phone:
try:
send_notification(
channel=NotificationChannel.SMS,
recipient=recipient_phone,
message=message_en,
content_object=appreciation,
)
except Exception as e:
# Log error but don't fail
print(f"Failed to send appreciation SMS: {e}")
except ImportError as e:
# Notification service not available - skip notification
print(f"Notification service not available: {e}")
except Exception as e:
# Any other error - log but don't fail
print(f"Error sending appreciation notification: {e}")
# Mark notification as sent (even if notification failed)
appreciation.notification_sent = True
appreciation.notification_sent_at = timezone.now()
appreciation.save(update_fields=['notification_sent', 'notification_sent_at'])
def update_appreciation_stats(instance):
"""
Update appreciation statistics for the recipient.
Creates or updates monthly statistics.
"""
# Get current year and month
now = timezone.now()
year = now.year
month = now.month
# Get or create stats record
stats, created = AppreciationStats.objects.get_or_create(
recipient_content_type=instance.recipient_content_type,
recipient_object_id=instance.recipient_object_id,
year=year,
month=month,
defaults={
'hospital': instance.hospital,
'department': instance.department,
'received_count': 0,
'sent_count': 0,
'acknowledged_count': 0,
'category_breakdown': {},
}
)
# Update received count
stats.received_count += 1
# Update category breakdown
if instance.category:
category_breakdown = stats.category_breakdown or {}
category_id_str = str(instance.category.id)
category_breakdown[category_id_str] = category_breakdown.get(category_id_str, 0) + 1
stats.category_breakdown = category_breakdown
# Save stats
stats.save()
# Recalculate rankings
recalculate_rankings(instance.hospital, year, month, instance.department)
def recalculate_rankings(hospital, year, month, department=None):
"""
Recalculate rankings for a given period.
Updates hospital_rank and department_rank for all recipients.
"""
# Get all stats for the period
if department:
stats_queryset = AppreciationStats.objects.filter(
hospital=hospital,
department=department,
year=year,
month=month,
)
else:
stats_queryset = AppreciationStats.objects.filter(
hospital=hospital,
year=year,
month=month,
)
# Order by received count
stats_queryset = stats_queryset.order_by('-received_count')
# Update hospital rank
for rank, stat in enumerate(stats_queryset, start=1):
stat.hospital_rank = rank
stat.save(update_fields=['hospital_rank'])
# Update department rank if department is specified
if department:
dept_stats = stats_queryset.filter(department=department)
for rank, stat in enumerate(dept_stats, start=1):
stat.department_rank = rank
stat.save(update_fields=['department_rank'])
def check_and_award_badges(instance):
"""
Check if recipient qualifies for any badges and award them.
Checks all active badge criteria and awards badges if criteria are met.
"""
from django.contrib.contenttypes.models import ContentType
# Get recipient
recipient_content_type = instance.recipient_content_type
recipient_object_id = instance.recipient_object_id
# Get all active badges for the hospital
badges = AppreciationBadge.objects.filter(
Q(hospital=instance.hospital) | Q(hospital__isnull=True),
is_active=True,
)
for badge in badges:
# Check if badge already earned
if UserBadge.objects.filter(
recipient_content_type=recipient_content_type,
recipient_object_id=recipient_object_id,
badge=badge,
).exists():
continue # Already earned
# Check badge criteria
qualifies = check_badge_criteria(
badge,
recipient_content_type,
recipient_object_id,
instance.hospital,
)
if qualifies:
# Calculate current count
count = get_appreciation_count(
recipient_content_type,
recipient_object_id,
badge.criteria_type
)
# Award badge
UserBadge.objects.create(
recipient_content_type=recipient_content_type,
recipient_object_id=recipient_object_id,
badge=badge,
appreciation_count=count,
)
def check_badge_criteria(badge, content_type, object_id, hospital):
"""
Check if recipient meets badge criteria.
Returns True if criteria is met, False otherwise.
"""
criteria_type = badge.criteria_type
criteria_value = badge.criteria_value
if criteria_type == 'received_count':
# Check total appreciation count
count = Appreciation.objects.filter(
recipient_content_type=content_type,
recipient_object_id=object_id,
status=AppreciationStatus.SENT,
).count()
return count >= criteria_value
elif criteria_type == 'received_month':
# Check appreciation count in current month
now = timezone.now()
count = Appreciation.objects.filter(
recipient_content_type=content_type,
recipient_object_id=object_id,
status=AppreciationStatus.SENT,
sent_at__year=now.year,
sent_at__month=now.month,
).count()
return count >= criteria_value
elif criteria_type == 'streak_weeks':
# Check consecutive weeks with appreciation
return check_appreciation_streak(
content_type,
object_id,
criteria_value
)
elif criteria_type == 'diverse_senders':
# Check appreciation from different senders
sender_count = Appreciation.objects.filter(
recipient_content_type=content_type,
recipient_object_id=object_id,
status=AppreciationStatus.SENT,
).values('sender').distinct().count()
return sender_count >= criteria_value
return False
def get_appreciation_count(content_type, object_id, criteria_type):
"""
Get appreciation count based on criteria type.
"""
if criteria_type == 'received_count':
return Appreciation.objects.filter(
recipient_content_type=content_type,
recipient_object_id=object_id,
status=AppreciationStatus.SENT,
).count()
elif criteria_type == 'received_month':
now = timezone.now()
return Appreciation.objects.filter(
recipient_content_type=content_type,
recipient_object_id=object_id,
status=AppreciationStatus.SENT,
sent_at__year=now.year,
sent_at__month=now.month,
).count()
return 0
def check_appreciation_streak(content_type, object_id, required_weeks):
"""
Check if recipient has appreciation streak for required weeks.
Returns True if streak meets or exceeds required_weeks.
"""
from datetime import timedelta
now = timezone.now()
current_week = 0
# Check week by week going backwards
for i in range(required_weeks):
week_start = now - timedelta(weeks=i+1)
week_end = now - timedelta(weeks=i)
# Check if there's any appreciation in this week
has_appreciation = Appreciation.objects.filter(
recipient_content_type=content_type,
recipient_object_id=object_id,
status=AppreciationStatus.SENT,
sent_at__gte=week_start,
sent_at__lt=week_end,
).exists()
if has_appreciation:
current_week += 1
else:
break
return current_week >= required_weeks

View File

@ -0,0 +1,986 @@
"""
Appreciation UI views - Server-rendered templates for appreciation management
"""
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.contrib.contenttypes.models import ContentType
from django.core.paginator import Paginator
from django.db.models import Q, Count
from django.http import JsonResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.utils import timezone
from django.views.decorators.http import require_http_methods
from apps.accounts.models import User
from apps.core.services import AuditService
from apps.organizations.models import Department, Hospital, Physician
from .models import (
Appreciation,
AppreciationBadge,
AppreciationCategory,
AppreciationStatus,
AppreciationVisibility,
AppreciationStats,
UserBadge,
)
# ============================================================================
# APPRECIATION LIST & DETAIL VIEWS
# ============================================================================
@login_required
def appreciation_list(request):
"""
Appreciations list view with advanced filters and pagination.
Features:
- Server-side pagination
- Advanced filters (status, visibility, category, hospital, department)
- Search by message
- Tab-based navigation (Received, Sent, Leaderboard, Badges)
"""
# Base queryset with optimizations
queryset = Appreciation.objects.select_related(
'sender', 'hospital', 'department', 'category'
)
# Apply RBAC filters
user = request.user
if user.is_px_admin():
pass # See all
elif user.is_hospital_admin() and user.hospital:
queryset = queryset.filter(hospital=user.hospital)
elif user.is_department_manager() and user.department:
queryset = queryset.filter(
Q(department=user.department) | Q(department__isnull=True)
)
elif user.hospital:
queryset = queryset.filter(hospital=user.hospital)
else:
queryset = queryset.none()
# Apply visibility filter based on user
from django.contrib.contenttypes.models import ContentType
user_content_type = ContentType.objects.get_for_model(user)
visibility_filter = (
Q(sender=user) |
Q(recipient_content_type=user_content_type, recipient_object_id=user.id)
)
if user.department:
visibility_filter |= Q(visibility=AppreciationVisibility.DEPARTMENT, department=user.department)
if user.hospital:
visibility_filter |= Q(visibility=AppreciationVisibility.HOSPITAL, hospital=user.hospital)
visibility_filter |= Q(visibility=AppreciationVisibility.PUBLIC)
queryset = queryset.filter(visibility_filter)
# Apply filters from request
tab = request.GET.get('tab', 'received')
if tab == 'received':
# Show appreciations received by user
queryset = queryset.filter(
recipient_content_type=user_content_type,
recipient_object_id=user.id
)
elif tab == 'sent':
# Show appreciations sent by user
queryset = queryset.filter(sender=user)
# 'leaderboard' and 'badges' tabs are handled separately
status_filter = request.GET.get('status')
if status_filter:
queryset = queryset.filter(status=status_filter)
visibility_filter_req = request.GET.get('visibility')
if visibility_filter_req:
queryset = queryset.filter(visibility=visibility_filter_req)
category_filter = request.GET.get('category')
if category_filter:
queryset = queryset.filter(category_id=category_filter)
hospital_filter = request.GET.get('hospital')
if hospital_filter:
queryset = queryset.filter(hospital_id=hospital_filter)
department_filter = request.GET.get('department')
if department_filter:
queryset = queryset.filter(department_id=department_filter)
# Search
search_query = request.GET.get('search')
if search_query:
queryset = queryset.filter(
Q(message_en__icontains=search_query) |
Q(message_ar__icontains=search_query)
)
# Date range filters
date_from = request.GET.get('date_from')
if date_from:
queryset = queryset.filter(sent_at__gte=date_from)
date_to = request.GET.get('date_to')
if date_to:
queryset = queryset.filter(sent_at__lte=date_to)
# Ordering
order_by = request.GET.get('order_by', '-sent_at')
queryset = queryset.order_by(order_by)
# Pagination
page_size = int(request.GET.get('page_size', 25))
paginator = Paginator(queryset, page_size)
page_number = request.GET.get('page', 1)
page_obj = paginator.get_page(page_number)
# Get filter options
hospitals = Hospital.objects.filter(status='active')
if not user.is_px_admin() and user.hospital:
hospitals = hospitals.filter(id=user.hospital.id)
departments = Department.objects.filter(status='active')
if not user.is_px_admin() and user.hospital:
departments = departments.filter(hospital=user.hospital)
categories = AppreciationCategory.objects.filter(is_active=True)
if not user.is_px_admin() and user.hospital:
categories = categories.filter(Q(hospital_id=user.hospital.id) | Q(hospital__isnull=True))
# Statistics
user_content_type = ContentType.objects.get_for_model(user)
stats = {
'received': Appreciation.objects.filter(
recipient_content_type=user_content_type,
recipient_object_id=user.id
).count(),
'sent': Appreciation.objects.filter(sender=user).count(),
'badges_earned': UserBadge.objects.filter(
recipient_content_type=user_content_type,
recipient_object_id=user.id
).count(),
}
context = {
'page_obj': page_obj,
'appreciations': page_obj.object_list,
'stats': stats,
'hospitals': hospitals,
'departments': departments,
'categories': categories,
'status_choices': AppreciationStatus.choices,
'visibility_choices': AppreciationVisibility.choices,
'current_tab': tab,
'filters': request.GET,
}
return render(request, 'appreciation/appreciation_list.html', context)
@login_required
def appreciation_detail(request, pk):
"""
Appreciation detail view.
Features:
- Full appreciation details
- Acknowledge action for recipients
- Related appreciations
"""
appreciation = get_object_or_404(
Appreciation.objects.select_related(
'sender', 'hospital', 'department', 'category'
),
pk=pk
)
# Check access
user = request.user
from django.contrib.contenttypes.models import ContentType
user_content_type = ContentType.objects.get_for_model(user)
can_view = (
appreciation.sender == user or
(appreciation.recipient_content_type == user_content_type and
appreciation.recipient_object_id == user.id) or
user.is_px_admin() or
(user.is_hospital_admin() and appreciation.hospital == user.hospital)
)
if not can_view:
messages.error(request, "You don't have permission to view this appreciation.")
return redirect('appreciation:appreciation_list')
# Check if user is recipient
is_recipient = (
appreciation.recipient_content_type == user_content_type and
appreciation.recipient_object_id == user.id
)
# Get related appreciations
related = Appreciation.objects.filter(
recipient_content_type=appreciation.recipient_content_type,
recipient_object_id=appreciation.recipient_object_id
).exclude(pk=appreciation.pk)[:5]
context = {
'appreciation': appreciation,
'is_recipient': is_recipient,
'related': related,
'status_choices': AppreciationStatus.choices,
'visibility_choices': AppreciationVisibility.choices,
}
return render(request, 'appreciation/appreciation_detail.html', context)
@login_required
@require_http_methods(["GET", "POST"])
def appreciation_send(request):
"""Send appreciation form"""
if request.method == 'POST':
try:
# Get form data
recipient_type = request.POST.get('recipient_type')
recipient_id = request.POST.get('recipient_id')
category_id = request.POST.get('category_id', None)
message_en = request.POST.get('message_en')
message_ar = request.POST.get('message_ar', '')
visibility = request.POST.get('visibility', AppreciationVisibility.PRIVATE)
is_anonymous = request.POST.get('is_anonymous') == 'on'
hospital_id = request.POST.get('hospital_id')
department_id = request.POST.get('department_id', None)
# Validate required fields
if not all([recipient_type, recipient_id, message_en, hospital_id]):
messages.error(request, "Please fill in all required fields.")
return redirect('appreciation:appreciation_send')
# Get recipient
if recipient_type == 'user':
recipient = User.objects.get(id=recipient_id)
recipient_content_type = ContentType.objects.get_for_model(User)
else: # physician
recipient = Physician.objects.get(id=recipient_id)
recipient_content_type = ContentType.objects.get_for_model(Physician)
# Get hospital and department
hospital = Hospital.objects.get(id=hospital_id)
department = None
if department_id:
department = Department.objects.get(id=department_id)
# Get category
category = None
if category_id:
category = AppreciationCategory.objects.get(id=category_id)
# Create appreciation
appreciation = Appreciation.objects.create(
sender=request.user,
recipient_content_type=recipient_content_type,
recipient_object_id=recipient_id,
hospital=hospital,
department=department,
category=category,
message_en=message_en,
message_ar=message_ar,
visibility=visibility,
is_anonymous=is_anonymous,
)
# Send appreciation
appreciation.send()
# Log audit
AuditService.log_event(
event_type='appreciation_sent',
description=f"Appreciation sent to {str(recipient)}",
user=request.user,
content_object=appreciation,
metadata={
'visibility': visibility,
'category': category.name_en if category else None,
'anonymous': is_anonymous
}
)
messages.success(request, "Appreciation sent successfully!")
return redirect('appreciation:appreciation_detail', pk=appreciation.id)
except User.DoesNotExist:
messages.error(request, "User not found.")
except Physician.DoesNotExist:
messages.error(request, "Physician not found.")
except Exception as e:
messages.error(request, f"Error sending appreciation: {str(e)}")
return redirect('appreciation:appreciation_send')
# GET request - show form
hospitals = Hospital.objects.filter(status='active')
if not request.user.is_px_admin() and request.user.hospital:
hospitals = hospitals.filter(id=request.user.hospital.id)
categories = AppreciationCategory.objects.filter(is_active=True)
if not request.user.is_px_admin() and request.user.hospital:
categories = categories.filter(Q(hospital_id=request.user.hospital.id) | Q(hospital__isnull=True))
context = {
'hospitals': hospitals,
'categories': categories,
'visibility_choices': AppreciationVisibility.choices,
}
return render(request, 'appreciation/appreciation_send_form.html', context)
@login_required
@require_http_methods(["POST"])
def appreciation_acknowledge(request, pk):
"""Acknowledge appreciation"""
appreciation = get_object_or_404(Appreciation, pk=pk)
# Check if user is recipient
user_content_type = ContentType.objects.get_for_model(request.user)
if not (
appreciation.recipient_content_type == user_content_type and
appreciation.recipient_object_id == request.user.id
):
messages.error(request, "You can only acknowledge appreciations sent to you.")
return redirect('appreciation:appreciation_detail', pk=pk)
# Acknowledge
appreciation.acknowledge()
messages.success(request, "Appreciation acknowledged successfully.")
return redirect('appreciation:appreciation_detail', pk=pk)
# ============================================================================
# LEADERBOARD VIEWS
# ============================================================================
@login_required
def leaderboard_view(request):
"""
Appreciation leaderboard view.
Features:
- Monthly rankings
- Hospital and department filters
- Top recipients with badges
"""
user = request.user
# Get date range
now = timezone.now()
year = int(request.GET.get('year', now.year))
month = int(request.GET.get('month', now.month))
# Build base query
queryset = AppreciationStats.objects.filter(year=year, month=month)
# Apply RBAC
if not user.is_px_admin() and user.hospital:
queryset = queryset.filter(hospital=user.hospital)
# Apply filters
hospital_filter = request.GET.get('hospital')
if hospital_filter:
queryset = queryset.filter(hospital_id=hospital_filter)
department_filter = request.GET.get('department')
if department_filter:
queryset = queryset.filter(department_id=department_filter)
# Order by received count
queryset = queryset.order_by('-received_count')
# Pagination
page_size = int(request.GET.get('page_size', 50))
paginator = Paginator(queryset, page_size)
page_number = request.GET.get('page', 1)
page_obj = paginator.get_page(page_number)
# Get filter options
hospitals = Hospital.objects.filter(status='active')
if not user.is_px_admin() and user.hospital:
hospitals = hospitals.filter(id=user.hospital.id)
departments = Department.objects.filter(status='active')
if not user.is_px_admin() and user.hospital:
departments = departments.filter(hospital=user.hospital)
# Get months for filter
months = [(i, timezone.datetime(year=year, month=i, day=1).strftime('%B')) for i in range(1, 13)]
years = range(now.year - 1, now.year + 2)
context = {
'page_obj': page_obj,
'leaderboard': page_obj.object_list,
'hospitals': hospitals,
'departments': departments,
'months': months,
'years': years,
'selected_year': year,
'selected_month': month,
'filters': request.GET,
}
return render(request, 'appreciation/leaderboard.html', context)
@login_required
def my_badges_view(request):
"""
User's badges view.
Features:
- All earned badges
- Badge details and criteria
- Progress toward next badges
"""
user = request.user
user_content_type = ContentType.objects.get_for_model(user)
# Get user's badges
queryset = UserBadge.objects.filter(
recipient_content_type=user_content_type,
recipient_object_id=user.id
).select_related('badge').order_by('-earned_at')
# Pagination
page_size = int(request.GET.get('page_size', 20))
paginator = Paginator(queryset, page_size)
page_number = request.GET.get('page', 1)
page_obj = paginator.get_page(page_number)
# Get available badges for progress tracking
available_badges = AppreciationBadge.objects.filter(is_active=True)
if not request.user.is_px_admin() and request.user.hospital:
available_badges = available_badges.filter(Q(hospital_id=request.user.hospital.id) | Q(hospital__isnull=True))
# Calculate progress for each badge
badge_progress = []
total_received = Appreciation.objects.filter(
recipient_content_type=user_content_type,
recipient_object_id=user.id
).count()
for badge in available_badges:
earned = queryset.filter(badge=badge).exists()
progress = 0
if badge.criteria_type == 'count':
progress = min(100, int((total_received / badge.criteria_value) * 100))
badge_progress.append({
'badge': badge,
'earned': earned,
'progress': progress,
})
context = {
'page_obj': page_obj,
'badges': page_obj.object_list,
'total_received': total_received,
'badge_progress': badge_progress,
}
return render(request, 'appreciation/my_badges.html', context)
# ============================================================================
# ADMIN: CATEGORY MANAGEMENT
# ============================================================================
@login_required
def category_list(request):
"""List and manage appreciation categories"""
user = request.user
# Check permission
if not (user.is_px_admin() or user.is_hospital_admin()):
messages.error(request, "You don't have permission to manage categories.")
return redirect('appreciation:appreciation_list')
# Base queryset
queryset = AppreciationCategory.objects.all()
# Apply RBAC
if not user.is_px_admin() and user.hospital:
queryset = queryset.filter(Q(hospital_id=user.hospital.id) | Q(hospital__isnull=True))
# Search
search_query = request.GET.get('search')
if search_query:
queryset = queryset.filter(
Q(name_en__icontains=search_query) |
Q(name_ar__icontains=search_query) |
Q(code__icontains=search_query)
)
# Ordering
queryset = queryset.order_by('order', 'code')
# Pagination
page_size = int(request.GET.get('page_size', 25))
paginator = Paginator(queryset, page_size)
page_number = request.GET.get('page', 1)
page_obj = paginator.get_page(page_number)
context = {
'page_obj': page_obj,
'categories': page_obj.object_list,
}
return render(request, 'appreciation/admin/category_list.html', context)
@login_required
@require_http_methods(["GET", "POST"])
def category_create(request):
"""Create appreciation category"""
user = request.user
# Check permission
if not (user.is_px_admin() or user.is_hospital_admin()):
messages.error(request, "You don't have permission to create categories.")
return redirect('appreciation:appreciation_list')
if request.method == 'POST':
try:
code = request.POST.get('code')
name_en = request.POST.get('name_en')
name_ar = request.POST.get('name_ar', '')
description_en = request.POST.get('description_en', '')
description_ar = request.POST.get('description_ar', '')
icon = request.POST.get('icon', 'fa-heart')
color = request.POST.get('color', '#FF5733')
order = request.POST.get('order', 0)
is_active = request.POST.get('is_active') == 'on'
# Get hospital
hospital = None
if user.is_hospital_admin() and user.hospital:
hospital = user.hospital
# Validate
if not all([code, name_en]):
messages.error(request, "Please fill in all required fields.")
return redirect('appreciation:category_create')
# Create category
AppreciationCategory.objects.create(
code=code,
name_en=name_en,
name_ar=name_ar,
description_en=description_en,
description_ar=description_ar,
icon=icon,
color=color,
order=order,
is_active=is_active,
hospital=hospital,
)
messages.success(request, "Category created successfully.")
return redirect('appreciation:category_list')
except Exception as e:
messages.error(request, f"Error creating category: {str(e)}")
return redirect('appreciation:category_create')
context = {}
return render(request, 'appreciation/admin/category_form.html', context)
@login_required
@require_http_methods(["GET", "POST"])
def category_edit(request, pk):
"""Edit appreciation category"""
user = request.user
# Check permission
if not (user.is_px_admin() or user.is_hospital_admin()):
messages.error(request, "You don't have permission to edit categories.")
return redirect('appreciation:appreciation_list')
category = get_object_or_404(AppreciationCategory, pk=pk)
# Check access
if not user.is_px_admin() and category.hospital != user.hospital:
messages.error(request, "You don't have permission to edit this category.")
return redirect('appreciation:category_list')
if request.method == 'POST':
try:
category.code = request.POST.get('code')
category.name_en = request.POST.get('name_en')
category.name_ar = request.POST.get('name_ar', '')
category.description_en = request.POST.get('description_en', '')
category.description_ar = request.POST.get('description_ar', '')
category.icon = request.POST.get('icon', 'fa-heart')
category.color = request.POST.get('color', '#FF5733')
category.order = request.POST.get('order', 0)
category.is_active = request.POST.get('is_active') == 'on'
category.save()
messages.success(request, "Category updated successfully.")
return redirect('appreciation:category_list')
except Exception as e:
messages.error(request, f"Error updating category: {str(e)}")
return redirect('appreciation:category_edit', pk=pk)
context = {
'category': category,
}
return render(request, 'appreciation/admin/category_form.html', context)
@login_required
@require_http_methods(["POST"])
def category_delete(request, pk):
"""Delete appreciation category"""
user = request.user
# Check permission
if not (user.is_px_admin() or user.is_hospital_admin()):
messages.error(request, "You don't have permission to delete categories.")
return redirect('appreciation:appreciation_list')
category = get_object_or_404(AppreciationCategory, pk=pk)
# Check if category is in use
if Appreciation.objects.filter(category=category).exists():
messages.error(request, "Cannot delete category that is in use.")
return redirect('appreciation:category_list')
# Log audit
AuditService.log_event(
event_type='category_deleted',
description=f"Appreciation category deleted: {category.name_en}",
user=request.user,
metadata={'category_code': category.code}
)
category.delete()
messages.success(request, "Category deleted successfully.")
return redirect('appreciation:category_list')
# ============================================================================
# ADMIN: BADGE MANAGEMENT
# ============================================================================
@login_required
def badge_list(request):
"""List and manage appreciation badges"""
user = request.user
# Check permission
if not (user.is_px_admin() or user.is_hospital_admin()):
messages.error(request, "You don't have permission to manage badges.")
return redirect('appreciation:appreciation_list')
# Base queryset
queryset = AppreciationBadge.objects.all()
# Apply RBAC
if not user.is_px_admin() and user.hospital:
queryset = queryset.filter(Q(hospital_id=user.hospital.id) | Q(hospital__isnull=True))
# Search
search_query = request.GET.get('search')
if search_query:
queryset = queryset.filter(
Q(name_en__icontains=search_query) |
Q(name_ar__icontains=search_query) |
Q(code__icontains=search_query)
)
# Ordering
queryset = queryset.order_by('order', 'code')
# Pagination
page_size = int(request.GET.get('page_size', 25))
paginator = Paginator(queryset, page_size)
page_number = request.GET.get('page', 1)
page_obj = paginator.get_page(page_number)
context = {
'page_obj': page_obj,
'badges': page_obj.object_list,
}
return render(request, 'appreciation/admin/badge_list.html', context)
@login_required
@require_http_methods(["GET", "POST"])
def badge_create(request):
"""Create appreciation badge"""
user = request.user
# Check permission
if not (user.is_px_admin() or user.is_hospital_admin()):
messages.error(request, "You don't have permission to create badges.")
return redirect('appreciation:appreciation_list')
if request.method == 'POST':
try:
code = request.POST.get('code')
name_en = request.POST.get('name_en')
name_ar = request.POST.get('name_ar', '')
description_en = request.POST.get('description_en', '')
description_ar = request.POST.get('description_ar', '')
icon = request.POST.get('icon', 'fa-award')
color = request.POST.get('color', '#FFD700')
criteria_type = request.POST.get('criteria_type', 'count')
criteria_value = request.POST.get('criteria_value', 5)
order = request.POST.get('order', 0)
is_active = request.POST.get('is_active') == 'on'
# Get hospital
hospital = None
if user.is_hospital_admin() and user.hospital:
hospital = user.hospital
# Validate
if not all([code, name_en, criteria_value]):
messages.error(request, "Please fill in all required fields.")
return redirect('appreciation:badge_create')
# Create badge
AppreciationBadge.objects.create(
code=code,
name_en=name_en,
name_ar=name_ar,
description_en=description_en,
description_ar=description_ar,
icon=icon,
color=color,
criteria_type=criteria_type,
criteria_value=int(criteria_value),
order=order,
is_active=is_active,
hospital=hospital,
)
messages.success(request, "Badge created successfully.")
return redirect('appreciation:badge_list')
except Exception as e:
messages.error(request, f"Error creating badge: {str(e)}")
return redirect('appreciation:badge_create')
context = {}
return render(request, 'appreciation/admin/badge_form.html', context)
@login_required
@require_http_methods(["GET", "POST"])
def badge_edit(request, pk):
"""Edit appreciation badge"""
user = request.user
# Check permission
if not (user.is_px_admin() or user.is_hospital_admin()):
messages.error(request, "You don't have permission to edit badges.")
return redirect('appreciation:appreciation_list')
badge = get_object_or_404(AppreciationBadge, pk=pk)
# Check access
if not user.is_px_admin() and badge.hospital != user.hospital:
messages.error(request, "You don't have permission to edit this badge.")
return redirect('appreciation:badge_list')
if request.method == 'POST':
try:
badge.code = request.POST.get('code')
badge.name_en = request.POST.get('name_en')
badge.name_ar = request.POST.get('name_ar', '')
badge.description_en = request.POST.get('description_en', '')
badge.description_ar = request.POST.get('description_ar', '')
badge.icon = request.POST.get('icon', 'fa-award')
badge.color = request.POST.get('color', '#FFD700')
badge.criteria_type = request.POST.get('criteria_type', 'count')
badge.criteria_value = request.POST.get('criteria_value', 5)
badge.order = request.POST.get('order', 0)
badge.is_active = request.POST.get('is_active') == 'on'
badge.save()
messages.success(request, "Badge updated successfully.")
return redirect('appreciation:badge_list')
except Exception as e:
messages.error(request, f"Error updating badge: {str(e)}")
return redirect('appreciation:badge_edit', pk=pk)
context = {
'badge': badge,
}
return render(request, 'appreciation/admin/badge_form.html', context)
@login_required
@require_http_methods(["POST"])
def badge_delete(request, pk):
"""Delete appreciation badge"""
user = request.user
# Check permission
if not (user.is_px_admin() or user.is_hospital_admin()):
messages.error(request, "You don't have permission to delete badges.")
return redirect('appreciation:appreciation_list')
badge = get_object_or_404(AppreciationBadge, pk=pk)
# Check if badge is in use
if UserBadge.objects.filter(badge=badge).exists():
messages.error(request, "Cannot delete badge that has been earned.")
return redirect('appreciation:badge_list')
# Log audit
AuditService.log_event(
event_type='badge_deleted',
description=f"Appreciation badge deleted: {badge.name_en}",
user=request.user,
metadata={'badge_code': badge.code}
)
badge.delete()
messages.success(request, "Badge deleted successfully.")
return redirect('appreciation:badge_list')
# ============================================================================
# AJAX/API HELPERS
# ============================================================================
@login_required
def get_users_by_hospital(request):
"""Get users for a hospital (AJAX)"""
hospital_id = request.GET.get('hospital_id')
if not hospital_id:
return JsonResponse({'users': []})
users = User.objects.filter(
hospital_id=hospital_id,
is_active=True
).values('id', 'first_name', 'last_name')
results = [
{
'id': str(u['id']),
'name': f"{u['first_name']} {u['last_name']}",
}
for u in users
]
return JsonResponse({'users': results})
@login_required
def get_physicians_by_hospital(request):
"""Get physicians for a hospital (AJAX)"""
hospital_id = request.GET.get('hospital_id')
if not hospital_id:
return JsonResponse({'physicians': []})
physicians = Physician.objects.filter(
hospital_id=hospital_id,
status='active'
).values('id', 'first_name', 'last_name', 'first_name_ar', 'last_name_ar')
results = [
{
'id': str(p['id']),
'name': f"{p['first_name']} {p['last_name']}",
}
for p in physicians
]
return JsonResponse({'physicians': results})
@login_required
def get_departments_by_hospital(request):
"""Get departments for a hospital (AJAX)"""
hospital_id = request.GET.get('hospital_id')
if not hospital_id:
return JsonResponse({'departments': []})
departments = Department.objects.filter(
hospital_id=hospital_id,
status='active'
).values('id', 'name', 'name_ar')
results = [
{
'id': str(d['id']),
'name': d['name'],
}
for d in departments
]
return JsonResponse({'departments': results})
@login_required
def appreciation_summary_ajax(request):
"""Get appreciation summary for current user (AJAX)"""
user = request.user
user_content_type = ContentType.objects.get_for_model(user)
now = timezone.now()
current_year = now.year
current_month = now.month
summary = {
'total_received': Appreciation.objects.filter(
recipient_content_type=user_content_type,
recipient_object_id=user.id
).count(),
'total_sent': Appreciation.objects.filter(sender=user).count(),
'this_month_received': Appreciation.objects.filter(
recipient_content_type=user_content_type,
recipient_object_id=user.id,
sent_at__year=current_year,
sent_at__month=current_month
).count(),
'this_month_sent': Appreciation.objects.filter(
sender=user,
sent_at__year=current_year,
sent_at__month=current_month
).count(),
'badges_earned': UserBadge.objects.filter(
recipient_content_type=user_content_type,
recipient_object_id=user.id
).count(),
}
# Get hospital rank
if user.hospital:
stats = AppreciationStats.objects.filter(
recipient_content_type=user_content_type,
recipient_object_id=user.id,
year=current_year,
month=current_month
).first()
summary['hospital_rank'] = stats.hospital_rank if stats else None
return JsonResponse(summary)

57
apps/appreciation/urls.py Normal file
View File

@ -0,0 +1,57 @@
"""
Appreciation URLs - URL configuration for appreciation API and UI
"""
from django.urls import include, path
from rest_framework.routers import DefaultRouter
from apps.appreciation.views import (
AppreciationBadgeViewSet,
AppreciationCategoryViewSet,
AppreciationStatsViewSet,
AppreciationViewSet,
LeaderboardView,
UserBadgeViewSet,
)
from apps.appreciation import ui_views
router = DefaultRouter()
router.register(r'categories', AppreciationCategoryViewSet, basename='appreciation-category')
router.register(r'appreciations', AppreciationViewSet, basename='appreciation')
router.register(r'stats', AppreciationStatsViewSet, basename='appreciation-stats')
router.register(r'badges', AppreciationBadgeViewSet, basename='appreciation-badge')
router.register(r'user-badges', UserBadgeViewSet, basename='user-badge')
app_name = 'appreciation'
urlpatterns = [
# API Routes
path('api/', include(router.urls)),
path('api/leaderboard/', LeaderboardView.as_view(), name='api-leaderboard'),
# UI Routes
path('', ui_views.appreciation_list, name='appreciation_list'),
path('send/', ui_views.appreciation_send, name='appreciation_send'),
path('detail/<uuid:pk>/', ui_views.appreciation_detail, name='appreciation_detail'),
path('acknowledge/<uuid:pk>/', ui_views.appreciation_acknowledge, name='appreciation_acknowledge'),
path('leaderboard/', ui_views.leaderboard_view, name='leaderboard_view'),
path('badges/', ui_views.my_badges_view, name='my_badges_view'),
# Admin: Category Management
path('admin/categories/', ui_views.category_list, name='category_list'),
path('admin/categories/create/', ui_views.category_create, name='category_create'),
path('admin/categories/<uuid:pk>/edit/', ui_views.category_edit, name='category_edit'),
path('admin/categories/<uuid:pk>/delete/', ui_views.category_delete, name='category_delete'),
# Admin: Badge Management
path('admin/badges/', ui_views.badge_list, name='badge_list'),
path('admin/badges/create/', ui_views.badge_create, name='badge_create'),
path('admin/badges/<uuid:pk>/edit/', ui_views.badge_edit, name='badge_edit'),
path('admin/badges/<uuid:pk>/delete/', ui_views.badge_delete, name='badge_delete'),
# AJAX Helpers
path('ajax/users/', ui_views.get_users_by_hospital, name='get_users_by_hospital'),
path('ajax/physicians/', ui_views.get_physicians_by_hospital, name='get_physicians_by_hospital'),
path('ajax/departments/', ui_views.get_departments_by_hospital, name='get_departments_by_hospital'),
path('ajax/summary/', ui_views.appreciation_summary_ajax, name='appreciation_summary_ajax'),
]

530
apps/appreciation/views.py Normal file
View File

@ -0,0 +1,530 @@
"""
Appreciation views - API views for appreciation management
"""
from django.contrib.contenttypes.models import ContentType
from django.db.models import Count, Q, F
from django.utils import timezone
from rest_framework import generics, status, viewsets
from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from apps.appreciation.models import (
Appreciation,
AppreciationBadge,
AppreciationCategory,
AppreciationStats,
UserBadge,
)
from apps.appreciation.serializers import (
AppreciationBadgeSerializer,
AppreciationCategorySerializer,
AppreciationCreateSerializer,
AppreciationLeaderboardSerializer,
AppreciationSerializer,
AppreciationStatsSerializer,
AppreciationSummarySerializer,
UserBadgeSerializer,
)
from apps.accounts.permissions import IsPXAdminOrHospitalAdmin
class AppreciationCategoryViewSet(viewsets.ModelViewSet):
"""Viewset for AppreciationCategory"""
queryset = AppreciationCategory.objects.all()
serializer_class = AppreciationCategorySerializer
permission_classes = [IsAuthenticated]
def get_queryset(self):
"""Filter categories by hospital"""
queryset = super().get_queryset()
# Filter by hospital if provided
hospital_id = self.request.query_params.get('hospital_id')
if hospital_id:
queryset = queryset.filter(Q(hospital_id=hospital_id) | Q(hospital__isnull=True))
# Only show active categories
queryset = queryset.filter(is_active=True)
return queryset.select_related('hospital')
class AppreciationViewSet(viewsets.ModelViewSet):
"""Viewset for Appreciation"""
queryset = Appreciation.objects.all()
serializer_class = AppreciationSerializer
permission_classes = [IsAuthenticated]
def get_queryset(self):
"""Filter appreciations based on user's access"""
user = self.request.user
queryset = super().get_queryset()
# Filter by hospital
if user.hospital:
queryset = queryset.filter(hospital=user.hospital)
# Filter by department if user is department manager
if user.department and user.is_department_manager():
queryset = queryset.filter(
Q(department=user.department) | Q(department__isnull=True)
)
# Filter by visibility
# Users can see:
# - All appreciations they sent
# - All appreciations they received
# - Department-level appreciations if they're in the department
# - Hospital-level appreciations if they're in the hospital
# - Public appreciations
from apps.appreciation.models import AppreciationVisibility
# Get user's content type
user_content_type = ContentType.objects.get_for_model(user)
# Get physician if user has a physician profile
physician = None
if hasattr(user, 'physician_profile'):
physician = user.phician_profile
physician_content_type = ContentType.objects.get_for_model(type(physician))
# Build visibility filter
visibility_filter = (
Q(sender=user) | # Sent by user
Q(
recipient_content_type=user_content_type,
recipient_object_id=user.id
) # Received by user
)
if physician:
visibility_filter |= Q(
recipient_content_type=physician_content_type,
recipient_object_id=physician.id
) # Received by physician
if user.department:
visibility_filter |= Q(
visibility=AppreciationVisibility.DEPARTMENT,
department=user.department
)
if user.hospital:
visibility_filter |= Q(
visibility=AppreciationVisibility.HOSPITAL,
hospital=user.hospital
)
visibility_filter |= Q(visibility=AppreciationVisibility.PUBLIC)
queryset = queryset.filter(visibility_filter)
# Filter by recipient
recipient_type = self.request.query_params.get('recipient_type')
recipient_id = self.request.query_params.get('recipient_id')
if recipient_type and recipient_id:
if recipient_type == 'user':
content_type = ContentType.objects.get_for_model(
self.request.user.__class__
)
queryset = queryset.filter(
recipient_content_type=content_type,
recipient_object_id=recipient_id
)
elif recipient_type == 'physician':
from apps.organizations.models import Physician
content_type = ContentType.objects.get_for_model(Physician)
queryset = queryset.filter(
recipient_content_type=content_type,
recipient_object_id=recipient_id
)
# Filter by status
status_filter = self.request.query_params.get('status')
if status_filter:
queryset = queryset.filter(status=status_filter)
# Filter by category
category_id = self.request.query_params.get('category_id')
if category_id:
queryset = queryset.filter(category_id=category_id)
return queryset.select_related(
'sender', 'hospital', 'department', 'category'
).prefetch_related('recipient')
def create(self, request, *args, **kwargs):
"""Create a new appreciation"""
serializer = AppreciationCreateSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
# Get validated data
data = serializer.validated_data
# Get recipient
recipient_type = data['recipient_type']
recipient_id = data['recipient_id']
if recipient_type == 'user':
from apps.accounts.models import User
recipient = User.objects.get(id=recipient_id)
content_type = ContentType.objects.get_for_model(User)
else: # physician
from apps.organizations.models import Physician
recipient = Physician.objects.get(id=recipient_id)
content_type = ContentType.objects.get_for_model(Physician)
# Get hospital
from apps.organizations.models import Hospital
hospital = Hospital.objects.get(id=data['hospital_id'])
# Get department
department = None
if data.get('department_id'):
from apps.organizations.models import Department
department = Department.objects.get(id=data['department_id'])
# Get category
category = None
if data.get('category_id'):
category = AppreciationCategory.objects.get(id=data['category_id'])
# Create appreciation
appreciation = Appreciation.objects.create(
sender=request.user,
recipient_content_type=content_type,
recipient_object_id=recipient_id,
hospital=hospital,
department=department,
category=category,
message_en=data['message_en'],
message_ar=data.get('message_ar', ''),
visibility=data['visibility'],
is_anonymous=data['is_anonymous'],
)
# Send appreciation
appreciation.send()
# Serialize and return
serializer = AppreciationSerializer(appreciation)
return Response(serializer.data, status=status.HTTP_201_CREATED)
@action(detail=True, methods=['post'])
def acknowledge(self, request, pk=None):
"""Acknowledge an appreciation"""
appreciation = self.get_object()
# Check if user is the recipient
user_content_type = ContentType.objects.get_for_model(request.user)
if not (
appreciation.recipient_content_type == user_content_type and
appreciation.recipient_object_id == request.user.id
):
return Response(
{'error': 'You can only acknowledge appreciations sent to you'},
status=status.HTTP_403_FORBIDDEN
)
# Acknowledge
appreciation.acknowledge()
# Serialize and return
serializer = AppreciationSerializer(appreciation)
return Response(serializer.data)
@action(detail=False, methods=['get'])
def my_appreciations(self, request):
"""Get appreciations for the current user"""
# Get user's appreciations
user_content_type = ContentType.objects.get_for_model(request.user)
# Check if user has physician profile
physician = None
if hasattr(request.user, 'physician_profile'):
physician = request.user.physician_profile
# Build query
queryset = self.get_queryset().filter(
Q(
recipient_content_type=user_content_type,
recipient_object_id=request.user.id
)
)
if physician:
physician_content_type = ContentType.objects.get_for_model(type(physician))
queryset |= self.get_queryset().filter(
recipient_content_type=physician_content_type,
recipient_object_id=physician.id
)
# Paginate
page = self.paginate_queryset(queryset)
if page is not None:
serializer = AppreciationSerializer(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = AppreciationSerializer(queryset, many=True)
return Response(serializer.data)
@action(detail=False, methods=['get'])
def sent_by_me(self, request):
"""Get appreciations sent by the current user"""
queryset = self.get_queryset().filter(sender=request.user)
# Paginate
page = self.paginate_queryset(queryset)
if page is not None:
serializer = AppreciationSerializer(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = AppreciationSerializer(queryset, many=True)
return Response(serializer.data)
@action(detail=False, methods=['get'])
def summary(self, request):
"""Get appreciation summary for the current user"""
# Get user's content type
user_content_type = ContentType.objects.get_for_model(request.user)
# Get current year and month
now = timezone.now()
current_year = now.year
current_month = now.month
# Count total received
total_received = Appreciation.objects.filter(
recipient_content_type=user_content_type,
recipient_object_id=request.user.id
).count()
# Count total sent
total_sent = Appreciation.objects.filter(
sender=request.user
).count()
# Count this month received
this_month_received = Appreciation.objects.filter(
recipient_content_type=user_content_type,
recipient_object_id=request.user.id,
sent_at__year=current_year,
sent_at__month=current_month
).count()
# Count this month sent
this_month_sent = Appreciation.objects.filter(
sender=request.user,
sent_at__year=current_year,
sent_at__month=current_month
).count()
# Get badges earned
badges_earned = UserBadge.objects.filter(
recipient_content_type=user_content_type,
recipient_object_id=request.user.id
).count()
# Get hospital rank
hospital_rank = None
if request.user.hospital:
# Get stats for this month
stats = AppreciationStats.objects.filter(
recipient_content_type=user_content_type,
recipient_object_id=request.user.id,
year=current_year,
month=current_month
).first()
if stats:
hospital_rank = stats.hospital_rank
# Get top category
top_category = None
if total_received > 0:
top_category_obj = Appreciation.objects.filter(
recipient_content_type=user_content_type,
recipient_object_id=request.user.id
).values('category__name_en', 'category__icon', 'category__color').annotate(
count=Count('id')
).order_by('-count').first()
if top_category_obj and top_category_obj['category__name_en']:
top_category = {
'name': top_category_obj['category__name_en'],
'icon': top_category_obj['category__icon'],
'color': top_category_obj['category__color'],
'count': top_category_obj['count']
}
# Build response
summary = {
'total_received': total_received,
'total_sent': total_sent,
'this_month_received': this_month_received,
'this_month_sent': this_month_sent,
'top_category': top_category,
'badges_earned': badges_earned,
'hospital_rank': hospital_rank,
}
serializer = AppreciationSummarySerializer(summary)
return Response(serializer.data)
class AppreciationStatsViewSet(viewsets.ReadOnlyModelViewSet):
"""Viewset for AppreciationStats"""
queryset = AppreciationStats.objects.all()
serializer_class = AppreciationStatsSerializer
permission_classes = [IsAuthenticated]
def get_queryset(self):
"""Filter stats based on user's access"""
user = self.request.user
queryset = super().get_queryset()
# Filter by hospital
if user.hospital:
queryset = queryset.filter(hospital=user.hospital)
# Filter by year and month
year = self.request.query_params.get('year')
if year:
queryset = queryset.filter(year=int(year))
month = self.request.query_params.get('month')
if month:
queryset = queryset.filter(month=int(month))
return queryset.select_related('hospital', 'department')
class AppreciationBadgeViewSet(viewsets.ModelViewSet):
"""Viewset for AppreciationBadge"""
queryset = AppreciationBadge.objects.all()
serializer_class = AppreciationBadgeSerializer
permission_classes = [IsAuthenticated, IsPXAdminOrHospitalAdmin]
def get_queryset(self):
"""Filter badges by hospital"""
queryset = super().get_queryset()
# Filter by hospital if provided
hospital_id = self.request.query_params.get('hospital_id')
if hospital_id:
queryset = queryset.filter(Q(hospital_id=hospital_id) | Q(hospital__isnull=True))
# Only show active badges
queryset = queryset.filter(is_active=True)
return queryset.select_related('hospital')
class UserBadgeViewSet(viewsets.ReadOnlyModelViewSet):
"""Viewset for UserBadge"""
queryset = UserBadge.objects.all()
serializer_class = UserBadgeSerializer
permission_classes = [IsAuthenticated]
def get_queryset(self):
"""Filter badges based on user's access"""
user = self.request.user
queryset = super().get_queryset()
# Get user's content type
user_content_type = ContentType.objects.get_for_model(user)
# Filter by user or user's physician profile
physician = None
if hasattr(user, 'physician_profile'):
physician = user.physician_profile
physician_content_type = ContentType.objects.get_for_model(type(physician))
queryset = queryset.filter(
Q(
recipient_content_type=user_content_type,
recipient_object_id=user.id
)
)
if physician:
queryset |= queryset.filter(
recipient_content_type=physician_content_type,
recipient_object_id=physician.id
)
return queryset.select_related('badge')
class LeaderboardView(generics.ListAPIView):
"""View for appreciation leaderboard"""
serializer_class = AppreciationLeaderboardSerializer
permission_classes = [IsAuthenticated]
def get_queryset(self):
"""Build leaderboard"""
# Get filters
year = self.request.query_params.get('year')
month = self.request.query_params.get('month')
# Default to current month
if not year or not month:
now = timezone.now()
year = now.year
month = now.month
else:
year = int(year)
month = int(month)
# Get hospital from request user
user = self.request.user
if not user.hospital:
return []
# Get stats for the period
stats = AppreciationStats.objects.filter(
hospital=user.hospital,
year=year,
month=month,
received_count__gt=0
).order_by('-received_count')
# Build leaderboard
leaderboard = []
for rank, stat in enumerate(stats, start=1):
recipient_name = stat.get_recipient_name()
recipient_type = stat.recipient_content_type.model if stat.recipient_content_type else 'unknown'
# Get badges for recipient
badges = []
user_badges = UserBadge.objects.filter(
recipient_content_type=stat.recipient_content_type,
recipient_object_id=stat.recipient_object_id
).select_related('badge')
for user_badge in user_badges:
badges.append({
'name': user_badge.badge.name_en,
'icon': user_badge.badge.icon,
'color': user_badge.badge.color,
})
leaderboard.append({
'rank': rank,
'recipient_type': recipient_type,
'recipient_id': stat.recipient_object_id,
'recipient_name': recipient_name,
'hospital': stat.hospital.name,
'department': stat.department.name if stat.department else None,
'received_count': stat.received_count,
'badges': badges,
})
return leaderboard

View File

@ -29,7 +29,7 @@ class Command(BaseCommand):
if hospital_id:
try:
hospitals = [Hospital.objects.get(id=hospital_id)]
self.stdout.write(f"Seeding configs for hospital: {hospitals[0].name_en}")
self.stdout.write(f"Seeding configs for hospital: {hospitals[0].name}")
except Hospital.DoesNotExist:
self.stdout.write(self.style.ERROR(f"Hospital with ID {hospital_id} not found"))
return
@ -43,7 +43,7 @@ class Command(BaseCommand):
# Seed per-hospital configurations
for hospital in hospitals:
self.stdout.write(f"\nProcessing hospital: {hospital.name_en}")
self.stdout.write(f"\nProcessing hospital: {hospital.name}")
self.seed_sla_configs(hospital)
self.seed_thresholds(hospital)
self.seed_escalation_rules(hospital)

View File

@ -553,14 +553,18 @@ def send_complaint_notification(complaint_id, event_type):
notification_count = 0
for recipient in recipients:
try:
NotificationService.send_notification(
recipient=recipient,
title=f"Complaint {event_type.title()}: {complaint.title[:50]}",
message=f"Complaint #{str(complaint.id)[:8]} has been {event_type}.",
notification_type='complaint',
related_object=complaint
)
notification_count += 1
# Check if NotificationService has send_notification method
if hasattr(NotificationService, 'send_notification'):
NotificationService.send_notification(
recipient=recipient,
title=f"Complaint {event_type.title()}: {complaint.title[:50]}",
message=f"Complaint #{str(complaint.id)[:8]} has been {event_type}.",
notification_type='complaint',
related_object=complaint
)
notification_count += 1
else:
logger.warning(f"NotificationService.send_notification method not available")
except Exception as e:
logger.error(f"Failed to send notification to {recipient}: {str(e)}")

View File

@ -7,6 +7,8 @@ from django.contrib.auth.mixins import LoginRequiredMixin
from django.db.models import Avg, Count, Q
from django.utils import timezone
from django.views.generic import TemplateView
from django.utils.translation import gettext_lazy as _
class CommandCenterView(LoginRequiredMixin, TemplateView):
@ -70,49 +72,49 @@ class CommandCenterView(LoginRequiredMixin, TemplateView):
# Top KPI Stats
context['stats'] = [
{
'label': 'Active Complaints',
'label': _("Active Complaints"),
'value': complaints_qs.filter(status__in=['open', 'in_progress']).count(),
'icon': 'exclamation-triangle',
'color': 'danger'
},
{
'label': 'Overdue Complaints',
'label': _('Overdue Complaints'),
'value': complaints_qs.filter(is_overdue=True, status__in=['open', 'in_progress']).count(),
'icon': 'clock-history',
'color': 'warning'
},
{
'label': 'Open PX Actions',
'label': _('Open PX Actions'),
'value': actions_qs.filter(status__in=['open', 'in_progress']).count(),
'icon': 'clipboard-check',
'color': 'primary'
},
{
'label': 'Overdue Actions',
'label': _('Overdue Actions'),
'value': actions_qs.filter(is_overdue=True, status__in=['open', 'in_progress']).count(),
'icon': 'alarm',
'color': 'danger'
},
{
'label': 'Negative Surveys (24h)',
'label': _('Negative Surveys (24h)'),
'value': surveys_qs.filter(is_negative=True, completed_at__gte=last_24h).count(),
'icon': 'emoji-frown',
'color': 'warning'
},
{
'label': 'Negative Social Mentions',
'label': _('Negative Social Mentions'),
'value': social_qs.filter(sentiment='negative', posted_at__gte=last_7d).count(),
'icon': 'chat-dots',
'color': 'danger'
},
{
'label': 'Low Call Center Ratings',
'label': _('Low Call Center Ratings'),
'value': calls_qs.filter(is_low_rating=True, call_started_at__gte=last_7d).count(),
'icon': 'telephone',
'color': 'warning'
},
{
'label': 'Avg Survey Score',
'label': _('Avg Survey Score'),
'value': f"{surveys_qs.filter(completed_at__gte=last_30d).aggregate(Avg('total_score'))['total_score__avg'] or 0:.1f}",
'icon': 'star',
'color': 'success'

View File

@ -0,0 +1,18 @@
# Generated by Django 5.0.14 on 2026-01-01 12:37
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('organizations', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='hospital',
name='metadata',
field=models.JSONField(blank=True, default=dict),
),
]

View File

@ -29,6 +29,7 @@ class Hospital(UUIDModel, TimeStampedModel):
# Metadata
license_number = models.CharField(max_length=100, blank=True)
capacity = models.IntegerField(null=True, blank=True, help_text="Bed capacity")
metadata = models.JSONField(default=dict, blank=True)
class Meta:
ordering = ['name']

View File

@ -63,6 +63,7 @@ LOCAL_APPS = [
'apps.notifications',
'apps.ai_engine',
'apps.dashboard',
'apps.appreciation',
]
INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS

View File

@ -34,12 +34,14 @@ urlpatterns = [
path('projects/', include('apps.projects.urls')),
path('config/', include('apps.core.config_urls')),
path('ai-engine/', include('apps.ai_engine.urls')),
path('appreciation/', include('apps.appreciation.urls', namespace='appreciation')),
# API endpoints
path('api/auth/', include('apps.accounts.urls')),
path('api/physicians/', include('apps.physicians.urls')),
path('api/integrations/', include('apps.integrations.urls')),
path('api/notifications/', include('apps.notifications.urls')),
path('api/v1/appreciation/', include('apps.appreciation.urls', namespace='api_appreciation')),
# OpenAPI/Swagger documentation
path('api/schema/', SpectacularAPIView.as_view(), name='schema'),

BIN
dump.rdb

Binary file not shown.

View File

@ -1,3 +1,4 @@
"""
Saudi-influenced data generator for PX360
@ -21,9 +22,19 @@ os.environ.setdefault('CELERY_TASK_ALWAYS_EAGER', 'True')
django.setup()
from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType
from django.utils import timezone
from apps.accounts.models import User
from apps.appreciation.models import (
Appreciation,
AppreciationCategory,
AppreciationBadge,
UserBadge,
AppreciationStats,
AppreciationStatus,
AppreciationVisibility,
)
from apps.complaints.models import Complaint, ComplaintUpdate
from apps.journeys.models import (
PatientJourneyInstance,
@ -165,6 +176,12 @@ def clear_existing_data():
print("Deleting hospitals...")
Hospital.objects.all().delete()
print("Deleting appreciation data...")
UserBadge.objects.all().delete()
AppreciationStats.objects.all().delete()
Appreciation.objects.all().delete()
# Categories and Badges are preserved (seeded separately)
print("Deleting users (except superusers)...")
User.objects.filter(is_superuser=False).delete()
@ -927,6 +944,384 @@ def create_physician_monthly_ratings(physicians):
return ratings
def create_appreciations(users, physicians, hospitals, departments, categories):
"""Create appreciations with 2 years of historical data"""
print("Creating appreciations (2 years of data)...")
# Get ContentType for User and Physician
user_ct = ContentType.objects.get_for_model(User)
physician_ct = ContentType.objects.get_for_model(Physician)
# Message templates for generating realistic appreciations
message_templates_en = [
"Thank you for {action}. Your {quality} made a real difference in our patient's care.",
"I want to express my sincere appreciation for {action}. Your {quality} is truly exceptional.",
"Outstanding work! Your {action} and {quality} have not gone unnoticed.",
"Grateful for your {action}. The {quality} you demonstrate every day inspires us all.",
"A big thank you for {action}. Your {quality} sets an excellent example.",
"Exceptional job on {action}. Your {quality} is invaluable to our team.",
"Words cannot express how much I appreciate your {action}. Your {quality} shines through.",
"Thank you for going above and beyond with {action}. Your {quality} makes our hospital better.",
"Deeply grateful for your {action}. The {quality} you show is remarkable.",
"Your {action} has made a significant impact. Your {quality} is truly appreciated.",
]
message_templates_ar = [
"شكراً لك على {action}. {quality} الخاص بك أحدث فرقاً حقيقياً في رعاية مرضانا.",
"أود أن أعرب عن تقديري الخالص لـ {action}. {quality} استثنائي حقاً.",
"عمل رائع! {action} و{quality} لم يمرا مرور الكرام.",
"ممتن لـ {action}. {quality} الذي تظهره كل يوم يلهمنا جميعاً.",
"شكراً جزيلاً على {action}. {quality} يضع مثالاً ممتازاً.",
"عمل استثنائي في {action}. {quality} لا يقدر بثمن لفريقنا.",
"الكلمات لا تعبر عن مدى تقديري لـ {action}. {quality} يظهر بوضوح.",
"شكراً لتجاوز التوقعات مع {action}. {quality} يجعل مستشفانا أفضل.",
"عميق الامتنان لـ {action}. {quality} الذي تظهره مذهل.",
"لقد حدث {action} تأثيراً كبيراً. {quality} حقاً مقدر.",
]
actions = [
"providing excellent patient care", "outstanding teamwork", "quick response in emergency",
"thorough diagnosis", "compassionate patient interaction", "efficient workflow management",
"mentoring junior staff", "innovative solution implementation", "attention to detail",
"going the extra mile", "maintaining patient safety", "clear communication",
"timely treatment", "excellent bedside manner", "professional conduct",
]
qualities = [
"professionalism", "dedication", "expertise", "compassion", "attention to detail",
"efficiency", "leadership", "teamwork", "reliability", "positive attitude",
"patience", "kindness", "commitment", "excellence", "integrity",
]
appreciations = []
now = timezone.now()
# Weighted category distribution
category_weights = {
'excellent_care': 30,
'team_player': 20,
'going_extra_mile': 15,
'positive_attitude': 12,
'leadership': 10,
'innovation': 8,
'reliability': 3,
'mentorship': 2,
}
# Create category code mapping
category_map = {cat.code: cat for cat in categories}
# Generate appreciations over 2 years (730 days)
# Average 1-2 appreciations per day = ~800-1200 total
for day_offset in range(730):
# Recency bias: more appreciations in recent months
recency_factor = max(0.5, 1.0 - (day_offset / 1460)) # 0.5 to 1.0
# Number of appreciations per day (weighted by recency)
base_count = random.choices([0, 1, 2], weights=[30, 50, 20])[0]
num_appreciations = max(0, int(base_count * recency_factor))
for _ in range(num_appreciations):
# Select sender (users only - users send appreciations)
sender = random.choice(users)
# Select recipient (70% physician, 30% user)
is_physician_recipient = random.random() < 0.7
if is_physician_recipient:
recipient = random.choice(physicians)
recipient_ct = physician_ct
else:
# User recipients (excluding sender)
potential_recipients = [u for u in users if u.id != sender.id]
if not potential_recipients:
continue
recipient = random.choice(potential_recipients)
recipient_ct = user_ct
# Ensure sender ≠ recipient
if sender.id == recipient.id:
continue
# Determine hospital context
if is_physician_recipient:
hospital = recipient.hospital
else:
hospital = recipient.hospital if recipient.hospital else sender.hospital
if not hospital:
continue
# Determine department context
department = None
if is_physician_recipient and recipient.department:
department = recipient.department
elif random.random() < 0.3:
# Some appreciations have department context
hospital_depts = [d for d in departments if d.hospital == hospital]
if hospital_depts:
department = random.choice(hospital_depts)
# Select category (weighted distribution)
category_codes = list(category_map.keys())
weights = [category_weights[code] for code in category_codes]
category_code = random.choices(category_codes, weights=weights, k=1)[0]
category = category_map.get(category_code)
if not category:
continue
# Generate message
action = random.choice(actions)
quality = random.choice(qualities)
message_en = random.choice(message_templates_en).format(action=action, quality=quality)
message_ar = random.choice(message_templates_ar).format(action=action, quality=quality)
# Select visibility (40% private, 25% dept, 25% hospital, 10% public)
visibility = random.choices(
list(AppreciationVisibility.values),
weights=[40, 25, 25, 10],
k=1
)[0]
# Determine status based on age
if day_offset < 1: # Last 24 hours
status = random.choices(
list(AppreciationStatus.values),
weights=[20, 50, 30], # draft, sent, acknowledged
k=1
)[0]
elif day_offset < 7: # Last week
status = random.choices(
list(AppreciationStatus.values),
weights=[5, 40, 55],
k=1
)[0]
else: # Older
status = random.choices(
list(AppreciationStatus.values),
weights=[1, 10, 89],
k=1
)[0]
# Calculate timestamps
created_date = now - timedelta(
days=day_offset,
hours=random.randint(0, 23),
minutes=random.randint(0, 59)
)
sent_at = None
acknowledged_at = None
if status != AppreciationStatus.DRAFT:
# Sent time: 0-24 hours after creation (for older appreciations)
if day_offset < 1:
sent_delay = random.randint(0, 23)
else:
sent_delay = random.randint(1, 24)
sent_at = created_date + timedelta(hours=sent_delay)
if status == AppreciationStatus.ACKNOWLEDGED:
# Acknowledged time: 1-72 hours after sent
acknowledge_delay = random.randint(1, 72)
acknowledged_at = sent_at + timedelta(hours=acknowledge_delay)
# Anonymous option (15% anonymous)
is_anonymous = random.random() < 0.15
# Create appreciation
appreciation = Appreciation(
sender=sender if not is_anonymous else None,
recipient_content_type=recipient_ct,
recipient_object_id=recipient.id,
hospital=hospital,
department=department,
category=category,
message_en=message_en,
message_ar=message_ar,
visibility=visibility,
status=status,
is_anonymous=is_anonymous,
sent_at=sent_at,
acknowledged_at=acknowledged_at,
)
# Override created_at for historical data
appreciation.created_at = created_date
appreciation.save()
appreciations.append(appreciation)
print(f" Created {len(appreciations)} appreciations over 2 years")
return appreciations
def award_badges(badges, users, physicians, categories):
"""Award badges to users and physicians based on appreciations"""
print("Awarding badges...")
user_badges = []
# Get ContentType for User and Physician
user_ct = ContentType.objects.get_for_model(User)
physician_ct = ContentType.objects.get_for_model(Physician)
# Badge criteria mapping (using codes from seed command)
badge_criteria = {
'first_appreciation': {'min_count': 1, 'categories': None},
'appreciated_5': {'min_count': 5, 'categories': None},
'appreciated_10': {'min_count': 10, 'categories': None},
'appreciated_25': {'min_count': 25, 'categories': None},
'appreciated_50': {'min_count': 50, 'categories': None},
# Monthly champion and other badges have complex criteria, award randomly
'monthly_champion': {'min_count': 10, 'categories': None},
'streak_4_weeks': {'min_count': 15, 'categories': None},
'diverse_appreciation': {'min_count': 20, 'categories': None},
}
badge_map = {badge.code: badge for badge in badges}
# Award badges to users (30% get badges)
for user in users:
if random.random() > 0.7: # 30% of users get badges
continue
# Count appreciations received by this user
received_count = Appreciation.objects.filter(
recipient_content_type=user_ct,
recipient_object_id=user.id,
status=AppreciationStatus.ACKNOWLEDGED
).count()
# Award appropriate badges
for badge_code, criteria in badge_criteria.items():
if badge_code not in badge_map:
continue
if received_count >= criteria['min_count']:
# Check if already has this badge
existing = UserBadge.objects.filter(
recipient_content_type=user_ct,
recipient_object_id=user.id,
badge=badge_map[badge_code]
).first()
if not existing:
# Random chance to award (not guaranteed even if criteria met)
if random.random() < 0.6:
user_badges.append(UserBadge(
recipient_content_type=user_ct,
recipient_object_id=user.id,
badge=badge_map[badge_code],
appreciation_count=received_count,
))
# Award badges to physicians (60% get badges)
for physician in physicians:
if random.random() > 0.6: # 60% of physicians get badges
continue
# Count appreciations received by this physician
received_count = Appreciation.objects.filter(
recipient_content_type=physician_ct,
recipient_object_id=physician.id,
status=AppreciationStatus.ACKNOWLEDGED
).count()
# Award appropriate badges
for badge_code, criteria in badge_criteria.items():
if badge_code not in badge_map:
continue
if received_count >= criteria['min_count']:
# Check if already has this badge
existing = UserBadge.objects.filter(
recipient_content_type=physician_ct,
recipient_object_id=physician.id,
badge=badge_map[badge_code]
).first()
if not existing:
# Higher chance for physicians
if random.random() < 0.7:
user_badges.append(UserBadge(
recipient_content_type=physician_ct,
recipient_object_id=physician.id,
badge=badge_map[badge_code],
appreciation_count=received_count,
))
# Bulk create user badges
UserBadge.objects.bulk_create(user_badges)
print(f" Awarded {len(user_badges)} badges to users and physicians")
return user_badges
def generate_appreciation_stats(users, physicians, hospitals):
"""Generate appreciation statistics for users and physicians"""
print("Generating appreciation statistics...")
stats = []
now = timezone.now()
# Get ContentType for User and Physician
user_ct = ContentType.objects.get_for_model(User)
physician_ct = ContentType.objects.get_for_model(Physician)
# Get current year and month
year = now.year
month = now.month
# Generate stats for users (70% have stats)
for user in users:
if random.random() > 0.7:
continue
# Generate realistic stats
received_count = random.randint(0, 30)
sent_count = random.randint(0, 50)
acknowledged_count = int(received_count * random.uniform(0.6, 1.0))
stats.append(AppreciationStats(
recipient_content_type=user_ct,
recipient_object_id=user.id,
year=year,
month=month,
hospital=user.hospital if user.hospital else random.choice(hospitals),
received_count=received_count,
sent_count=sent_count,
acknowledged_count=acknowledged_count,
hospital_rank=random.randint(1, 20) if received_count > 0 else None,
))
# Generate stats for physicians (90% have stats)
for physician in physicians:
if random.random() > 0.1: # 90% have stats
# Physicians typically receive more appreciations
received_count = random.randint(5, 50)
sent_count = random.randint(0, 20) # Physicians send less
acknowledged_count = int(received_count * random.uniform(0.7, 1.0))
stats.append(AppreciationStats(
recipient_content_type=physician_ct,
recipient_object_id=physician.id,
year=year,
month=month,
hospital=physician.hospital,
department=physician.department,
received_count=received_count,
sent_count=sent_count,
acknowledged_count=acknowledged_count,
hospital_rank=random.randint(1, 10) if received_count > 5 else None,
))
# Bulk create stats
AppreciationStats.objects.bulk_create(stats)
print(f" Generated {len(stats)} appreciation statistics")
return stats
def main():
"""Main data generation function"""
print("\n" + "="*60)
@ -960,6 +1355,27 @@ def main():
social_mentions = create_social_mentions(hospitals)
physician_ratings = create_physician_monthly_ratings(physicians)
# Get appreciation categories and badges (should be seeded)
categories = list(AppreciationCategory.objects.all())
badges = list(AppreciationBadge.objects.all())
if not categories or not badges:
print("\n" + "!"*60)
print("WARNING: Appreciation categories or badges not found!")
print("Please run: python manage.py seed_appreciation_data")
print("!"*60 + "\n")
# Create appreciation data
if categories and badges:
appreciations = create_appreciations(users, physicians, hospitals, departments, categories)
user_badges = award_badges(badges, users, physicians, categories)
# Stats are auto-generated by signals, so we don't need to create them manually
appreciation_stats = list(AppreciationStats.objects.all())
else:
appreciations = []
user_badges = []
appreciation_stats = []
print("\n" + "="*60)
print("Data Generation Complete!")
print("="*60)
@ -979,6 +1395,9 @@ def main():
print(f" - {len(social_mentions)} Social Media Mentions")
print(f" - {len(projects)} QI Projects")
print(f" - {len(physician_ratings)} Physician Monthly Ratings")
print(f" - {len(appreciations)} Appreciations (2 years)")
print(f" - {len(user_badges)} Badges Awarded")
print(f" - {len(appreciation_stats)} Appreciation Statistics")
print(f"\nYou can now login with:")
print(f" Username: px_admin")
print(f" Password: admin123")

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,199 @@
{% extends "layouts/base.html" %}
{% load i18n static %}
{% block title %}{% trans "Appreciation Details" %} - {% endblock %}
{% block content %}
<div class="container-fluid py-4">
<!-- Breadcrumb -->
<nav aria-label="breadcrumb" class="mb-4">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{% url 'appreciation:appreciation_list' %}">{% trans "Appreciation" %}</a></li>
<li class="breadcrumb-item active" aria-current="page">{% trans "Details" %}</li>
</ol>
</nav>
<!-- Appreciation Details Card -->
<div class="row">
<div class="col-lg-8">
<div class="card shadow-sm mb-4">
<div class="card-body">
<!-- Category Badge -->
<div class="d-flex justify-content-between align-items-start mb-3">
<div>
{% if appreciation.category %}
<span class="badge bg-primary fs-6 mb-2">
<i class="{{ appreciation.category.icon|default:'fa-heart' }} me-2"></i>
{{ appreciation.category.name_en }}
</span>
{% endif %}
<h2 class="h4 mb-1">{{ appreciation.message_en }}</h2>
{% if appreciation.message_ar %}
<p class="text-muted mb-0" dir="rtl">{{ appreciation.message_ar }}</p>
{% endif %}
</div>
<span class="badge bg-{% if appreciation.status == 'acknowledged' %}success{% else %}primary{% endif %}">
{{ appreciation.get_status_display }}
</span>
</div>
<hr>
<!-- Sender and Recipient Info -->
<div class="row mb-3">
<div class="col-md-6">
<h6 class="text-muted mb-2">{% trans "From" %}</h6>
{% if appreciation.is_anonymous %}
<p class="mb-0">
<i class="fas fa-user-secret text-muted me-2"></i>
{% trans "Anonymous" %}
</p>
{% else %}
<p class="mb-0">
<i class="fas fa-user text-primary me-2"></i>
{{ appreciation.sender.get_full_name }}
</p>
{% endif %}
</div>
<div class="col-md-6">
<h6 class="text-muted mb-2">{% trans "To" %}</h6>
<p class="mb-0">
<i class="fas fa-user text-success me-2"></i>
{{ appreciation.recipient_name }}
</p>
</div>
</div>
<!-- Additional Details -->
<div class="row mb-3">
<div class="col-md-6">
<h6 class="text-muted mb-2">{% trans "Sent At" %}</h6>
<p class="mb-0">
<i class="fas fa-clock text-info me-2"></i>
{{ appreciation.sent_at|date:"F j, Y, g:i A" }}
</p>
</div>
<div class="col-md-6">
<h6 class="text-muted mb-2">{% trans "Visibility" %}</h6>
<p class="mb-0">
<i class="fas fa-eye text-warning me-2"></i>
{{ appreciation.get_visibility_display }}
</p>
</div>
</div>
{% if appreciation.acknowledged_at %}
<div class="alert alert-success">
<i class="fas fa-check-circle me-2"></i>
{% trans "Acknowledged on" %} {{ appreciation.acknowledged_at|date:"F j, Y, g:i A" }}
</div>
{% endif %}
<!-- Actions -->
<div class="d-flex gap-2 mt-4">
{% if is_recipient and appreciation.status == 'sent' %}
<form method="post" action="{% url 'appreciation:appreciation_acknowledge' appreciation.id %}" class="d-inline">
{% csrf_token %}
<button type="submit" class="btn btn-success">
<i class="fas fa-check me-2"></i>
{% trans "Acknowledge" %}
</button>
</form>
{% endif %}
<a href="{% url 'appreciation:appreciation_list' %}" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left me-2"></i>
{% trans "Back to List" %}
</a>
</div>
</div>
</div>
<!-- Related Appreciations -->
{% if related %}
<div class="card shadow-sm">
<div class="card-header bg-light">
<h5 class="card-title mb-0">
<i class="fas fa-link me-2"></i>
{% trans "Related Appreciations" %}
</h5>
</div>
<div class="card-body">
<div class="list-group list-group-flush">
{% for rel in related %}
<a href="{% url 'appreciation:appreciation_detail' rel.id %}" class="list-group-item list-group-item-action">
<div class="d-flex w-100 justify-content-between">
<h6 class="mb-1">{{ rel.message_en|truncatewords:10 }}</h6>
<small class="text-muted">{{ rel.sent_at|date:"M d, Y" }}</small>
</div>
<p class="mb-1 small text-muted">
<i class="fas fa-user me-1"></i>
{{ rel.sender.get_full_name }}
</p>
</a>
{% endfor %}
</div>
</div>
</div>
{% endif %}
</div>
<!-- Sidebar -->
<div class="col-lg-4">
<!-- Quick Stats -->
<div class="card shadow-sm mb-4">
<div class="card-header bg-light">
<h6 class="card-title mb-0">{% trans "Quick Info" %}</h6>
</div>
<div class="card-body">
<ul class="list-unstyled mb-0">
<li class="mb-2">
<i class="fas fa-building text-muted me-2"></i>
<strong>{% trans "Hospital:" %}</strong> {{ appreciation.hospital.name }}
</li>
{% if appreciation.department %}
<li class="mb-2">
<i class="fas fa-sitemap text-muted me-2"></i>
<strong>{% trans "Department:" %}</strong> {{ appreciation.department.name }}
</li>
{% endif %}
<li>
<i class="fas fa-id-badge text-muted me-2"></i>
<strong>{% trans "ID:" %}</strong> #{{ appreciation.id }}
</li>
</ul>
</div>
</div>
<!-- Actions -->
<div class="card shadow-sm">
<div class="card-header bg-light">
<h6 class="card-title mb-0">{% trans "Actions" %}</h6>
</div>
<div class="card-body">
<div class="d-grid gap-2">
<a href="{% url 'appreciation:appreciation_send' %}" class="btn btn-primary">
<i class="fas fa-paper-plane me-2"></i>
{% trans "Send Appreciation" %}
</a>
<a href="{% url 'appreciation:leaderboard_view' %}" class="btn btn-info">
<i class="fas fa-trophy me-2"></i>
{% trans "View Leaderboard" %}
</a>
<a href="{% url 'appreciation:my_badges_view' %}" class="btn btn-warning">
<i class="fas fa-award me-2"></i>
{% trans "My Badges" %}
</a>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
{{ block.super }}
<script>
// Add any appreciation detail-specific JavaScript here
</script>
{% endblock %}

View File

@ -0,0 +1,350 @@
{% extends "layouts/base.html" %}
{% load i18n static %}
{% block title %}{% trans "Appreciation" %} - {% endblock %}
{% block content %}
<div class="container-fluid py-4">
<!-- Page Header -->
<div class="row mb-4">
<div class="col-12">
<div class="card shadow-sm">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<h2 class="h4 mb-0">
<i class="fas fa-heart text-danger me-2"></i>
{% trans "Appreciation" %}
</h2>
<p class="text-muted mb-0 mt-2">
{% trans "Send appreciation to colleagues and celebrate achievements" %}
</p>
</div>
<a href="{% url 'appreciation:appreciation_send' %}" class="btn btn-primary">
<i class="fas fa-paper-plane me-2"></i>
{% trans "Send Appreciation" %}
</a>
</div>
</div>
</div>
</div>
</div>
<!-- Stats Cards -->
<div class="row mb-4">
<div class="col-md-3">
<div class="card shadow-sm border-0">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="bg-primary bg-opacity-10 p-3 rounded-circle me-3">
<i class="fas fa-inbox text-primary fa-lg"></i>
</div>
<div>
<h6 class="text-muted mb-1">{% trans "Received" %}</h6>
<h3 class="mb-0" id="receivedCount">{{ stats.received }}</h3>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card shadow-sm border-0">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="bg-success bg-opacity-10 p-3 rounded-circle me-3">
<i class="fas fa-paper-plane text-success fa-lg"></i>
</div>
<div>
<h6 class="text-muted mb-1">{% trans "Sent" %}</h6>
<h3 class="mb-0" id="sentCount">{{ stats.sent }}</h3>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card shadow-sm border-0">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="bg-warning bg-opacity-10 p-3 rounded-circle me-3">
<i class="fas fa-award text-warning fa-lg"></i>
</div>
<div>
<h6 class="text-muted mb-1">{% trans "Badges Earned" %}</h6>
<h3 class="mb-0" id="badgesCount">{{ stats.badges_earned }}</h3>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card shadow-sm border-0">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="bg-info bg-opacity-10 p-3 rounded-circle me-3">
<i class="fas fa-trophy text-info fa-lg"></i>
</div>
<div>
<h6 class="text-muted mb-1">{% trans "Leaderboard" %}</h6>
<a href="{% url 'appreciation:leaderboard_view' %}" class="btn btn-sm btn-outline-info">
<i class="fas fa-arrow-right me-1"></i>
{% trans "View" %}
</a>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Quick Actions -->
<div class="row mb-4">
<div class="col-md-4">
<a href="{% url 'appreciation:leaderboard_view' %}" class="text-decoration-none">
<div class="card shadow-sm border-warning mb-3">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="bg-warning bg-opacity-10 p-3 rounded-circle me-3">
<i class="fas fa-trophy text-warning fa-lg"></i>
</div>
<div>
<h6 class="mb-0">{% trans "Leaderboard" %}</h6>
<small class="text-muted">{% trans "See top performers" %}</small>
</div>
</div>
</div>
</div>
</a>
</div>
<div class="col-md-4">
<a href="{% url 'appreciation:my_badges_view' %}" class="text-decoration-none">
<div class="card shadow-sm border-primary mb-3">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="bg-primary bg-opacity-10 p-3 rounded-circle me-3">
<i class="fas fa-award text-primary fa-lg"></i>
</div>
<div>
<h6 class="mb-0">{% trans "My Badges" %}</h6>
<small class="text-muted">{% trans "View earned badges" %}</small>
</div>
</div>
</div>
</div>
</a>
</div>
<div class="col-md-4">
<a href="{% url 'appreciation:appreciation_send' %}" class="text-decoration-none">
<div class="card shadow-sm border-success mb-3">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="bg-success bg-opacity-10 p-3 rounded-circle me-3">
<i class="fas fa-paper-plane text-success fa-lg"></i>
</div>
<div>
<h6 class="mb-0">{% trans "Send Appreciation" %}</h6>
<small class="text-muted">{% trans "Share appreciation" %}</small>
</div>
</div>
</div>
</div>
</a>
</div>
</div>
<!-- Filter Bar -->
<div class="card shadow-sm mb-4">
<div class="card-body">
<form method="get" class="row g-3">
<div class="col-md-3">
<label for="tab" class="form-label">{% trans "View" %}</label>
<select class="form-select" id="tab" name="tab" onchange="this.form.submit()">
<option value="received" {% if current_tab == 'received' %}selected{% endif %}>
{% trans "My Appreciations" %}
</option>
<option value="sent" {% if current_tab == 'sent' %}selected{% endif %}>
{% trans "Sent by Me" %}
</option>
</select>
</div>
<div class="col-md-3">
<label for="status" class="form-label">{% trans "Status" %}</label>
<select class="form-select" id="status" name="status" onchange="this.form.submit()">
<option value="">{% trans "All Status" %}</option>
{% for choice in status_choices %}
<option value="{{ choice.0 }}" {% if filters.status == choice.0 %}selected{% endif %}>
{{ choice.1 }}
</option>
{% endfor %}
</select>
</div>
<div class="col-md-3">
<label for="category" class="form-label">{% trans "Category" %}</label>
<select class="form-select" id="category" name="category" onchange="this.form.submit()">
<option value="">{% trans "All Categories" %}</option>
{% for cat in categories %}
<option value="{{ cat.id }}" {% if filters.category == cat.id|stringformat:"s" %}selected{% endif %}>
{{ cat.name_en }}
</option>
{% endfor %}
</select>
</div>
<div class="col-md-3">
<label for="search" class="form-label">{% trans "Search" %}</label>
<div class="input-group">
<input type="text" class="form-control" id="search" name="search"
value="{{ filters.search|default:'' }}" placeholder="{% trans 'Search messages...' %}">
<button type="submit" class="btn btn-primary">
<i class="fas fa-search"></i>
</button>
</div>
</div>
<div class="col-12">
<a href="{% url 'appreciation:appreciation_list' %}" class="btn btn-outline-secondary">
<i class="fas fa-redo me-2"></i>
{% trans "Clear Filters" %}
</a>
</div>
</form>
</div>
</div>
<!-- Appreciations List -->
<div class="card shadow-sm">
<div class="card-body">
{% if page_obj %}
<div class="list-group">
{% for appreciation in page_obj %}
<a href="{% url 'appreciation:appreciation_detail' appreciation.id %}"
class="list-group-item list-group-item-action">
<div class="d-flex w-100 justify-content-between align-items-start">
<div class="flex-grow-1">
<div class="d-flex align-items-center mb-2">
{% if appreciation.category %}
<span class="badge bg-primary me-2">
<i class="{{ appreciation.category.icon|default:'fa-heart' }} me-1"></i>
{{ appreciation.category.name_en }}
</span>
{% endif %}
<h6 class="mb-0">
{% if current_tab == 'received' %}
{{ appreciation.sender.get_full_name }}
{% else %}
{{ appreciation.recipient_name }}
{% endif %}
</h6>
</div>
<p class="mb-2 text-muted">{{ appreciation.message_en|truncatewords:15 }}</p>
<small class="text-muted">
<i class="fas fa-clock me-1"></i>
{{ appreciation.sent_at|date:"F j, Y, g:i A" }}
{% if appreciation.department %}
<span class="mx-2"></span>
<i class="fas fa-building me-1"></i>
{{ appreciation.department.name }}
{% endif %}
</small>
</div>
<div class="text-end">
<span class="badge bg-{% if appreciation.status == 'acknowledged' %}success{% else %}primary{% endif %}">
{{ appreciation.get_status_display }}
</span>
<div class="small text-muted mt-1">
{{ appreciation.get_visibility_display }}
</div>
</div>
</div>
</a>
{% endfor %}
</div>
<!-- Pagination -->
{% if page_obj.has_other_pages %}
<nav aria-label="Page navigation" class="mt-4">
<ul class="pagination justify-content-center">
{% if page_obj.has_previous %}
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.previous_page_number }}&tab={{ current_tab }}{% if filters.urlencode %}&{{ filters.urlencode }}{% endif %}">
{% trans "Previous" %}
</a>
</li>
{% else %}
<li class="page-item disabled">
<span class="page-link">{% trans "Previous" %}</span>
</li>
{% endif %}
{% for num in page_obj.paginator.page_range %}
{% if page_obj.number == num %}
<li class="page-item active">
<span class="page-link">{{ num }}</span>
</li>
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
<li class="page-item">
<a class="page-link" href="?page={{ num }}&tab={{ current_tab }}{% if filters.urlencode %}&{{ filters.urlencode }}{% endif %}">{{ num }}</a>
</li>
{% endif %}
{% endfor %}
{% if page_obj.has_next %}
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.next_page_number }}&tab={{ current_tab }}{% if filters.urlencode %}&{{ filters.urlencode }}{% endif %}">
{% trans "Next" %}
</a>
</li>
{% else %}
<li class="page-item disabled">
<span class="page-link">{% trans "Next" %}</span>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
{% else %}
<div class="text-center py-5">
<i class="fas fa-heart text-muted fa-4x mb-3"></i>
<h4 class="text-muted">
{% if current_tab == 'received' %}
{% trans "No appreciations received yet" %}
{% else %}
{% trans "No appreciations sent yet" %}
{% endif %}
</h4>
<p class="text-muted mb-3">
{% trans "Start sharing appreciation with your colleagues!" %}
</p>
<a href="{% url 'appreciation:appreciation_send' %}" class="btn btn-primary">
<i class="fas fa-paper-plane me-2"></i>
{% trans "Send Your First Appreciation" %}
</a>
</div>
{% endif %}
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
{{ block.super }}
<script>
// Refresh summary statistics periodically (every 30 seconds)
function refreshSummary() {
fetch("{% url 'appreciation:appreciation_summary_ajax' %}")
.then(response => response.json())
.then(data => {
document.getElementById('receivedCount').textContent = data.total_received || 0;
document.getElementById('sentCount').textContent = data.total_sent || 0;
document.getElementById('badgesCount').textContent = data.badges_earned || 0;
})
.catch(error => {
console.error('Error refreshing summary:', error);
});
}
// Refresh summary every 30 seconds
setInterval(refreshSummary, 30000);
// Initial summary refresh after page load
setTimeout(refreshSummary, 1000);
</script>
{% endblock %}

View File

@ -0,0 +1,326 @@
{% extends "layouts/base.html" %}
{% load i18n static %}
{% block title %}{% trans "Send Appreciation" %} - {% endblock %}
{% block content %}
<div class="container-fluid py-4">
<!-- Breadcrumb -->
<nav aria-label="breadcrumb" class="mb-4">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{% url 'appreciation:appreciation_list' %}">{% trans "Appreciation" %}</a></li>
<li class="breadcrumb-item active" aria-current="page">{% trans "Send Appreciation" %}</li>
</ol>
</nav>
<!-- Send Appreciation Form -->
<div class="row">
<div class="col-lg-8">
<div class="card shadow-sm">
<div class="card-header bg-primary text-white">
<h4 class="mb-0">
<i class="fas fa-paper-plane me-2"></i>
{% trans "Send Appreciation" %}
</h4>
</div>
<div class="card-body">
<form method="post" id="appreciationForm">
{% csrf_token %}
<!-- Recipient Type -->
<div class="row mb-3">
<div class="col-md-6">
<label for="recipient_type" class="form-label">
{% trans "Recipient Type" %} <span class="text-danger">*</span>
</label>
<select class="form-select" id="recipient_type" name="recipient_type" required>
<option value="user">{% trans "User" %}</option>
<option value="physician">{% trans "Physician" %}</option>
</select>
</div>
<div class="col-md-6">
<label for="hospital_id" class="form-label">
{% trans "Hospital" %} <span class="text-danger">*</span>
</label>
<select class="form-select" id="hospital_id" name="hospital_id" required>
<option value="">-- {% trans "Select Hospital" %} --</option>
{% for hospital in hospitals %}
<option value="{{ hospital.id }}">{{ hospital.name }}</option>
{% endfor %}
</select>
</div>
</div>
<!-- Recipient -->
<div class="mb-3">
<label for="recipient_id" class="form-label">
{% trans "Recipient" %} <span class="text-danger">*</span>
</label>
<select class="form-select" id="recipient_id" name="recipient_id" required disabled>
<option value="">-- {% trans "Select Recipient" %} --</option>
</select>
<div class="form-text" id="recipientHelp">{% trans "Select a hospital first" %}</div>
</div>
<!-- Department (Optional) -->
<div class="mb-3">
<label for="department_id" class="form-label">{% trans "Department" %}</label>
<select class="form-select" id="department_id" name="department_id">
<option value="">-- {% trans "Select Department" %} --</option>
</select>
<div class="form-text">{% trans "Optional: Select if related to a specific department" %}</div>
</div>
<!-- Category -->
<div class="mb-3">
<label for="category_id" class="form-label">{% trans "Category" %}</label>
<select class="form-select" id="category_id" name="category_id">
<option value="">-- {% trans "Select Category" %} --</option>
{% for category in categories %}
<option value="{{ category.id }}">
<i class="{{ category.icon }}"></i> {{ category.name_en }}
</option>
{% endfor %}
</select>
</div>
<!-- Message (English) -->
<div class="mb-3">
<label for="message_en" class="form-label">
{% trans "Message (English)" %} <span class="text-danger">*</span>
</label>
<textarea
class="form-control"
id="message_en"
name="message_en"
rows="4"
required
placeholder="{% trans 'Write your appreciation message here...' %}"
></textarea>
<div class="form-text">{% trans "Required: Appreciation message in English" %}</div>
</div>
<!-- Message (Arabic) -->
<div class="mb-3">
<label for="message_ar" class="form-label">{% trans "Message (Arabic)" %}</label>
<textarea
class="form-control"
id="message_ar"
name="message_ar"
rows="4"
dir="rtl"
placeholder="{% trans 'اكتب رسالة التقدير هنا...' %}"
></textarea>
<div class="form-text">{% trans "Optional: Appreciation message in Arabic" %}</div>
</div>
<!-- Visibility -->
<div class="mb-3">
<label for="visibility" class="form-label">{% trans "Visibility" %}</label>
<select class="form-select" id="visibility" name="visibility">
{% for choice in visibility_choices %}
<option value="{{ choice.0 }}" {% if choice.0 == 'private' %}selected{% endif %}>
{{ choice.1 }}
</option>
{% endfor %}
</select>
</div>
<!-- Anonymous -->
<div class="mb-4">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="is_anonymous"
name="is_anonymous"
>
<label class="form-check-label" for="is_anonymous">
{% trans "Send anonymously" %}
</label>
<div class="form-text">{% trans "Your name will not be shown to the recipient" %}</div>
</div>
</div>
<!-- Submit Button -->
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
<a href="{% url 'appreciation:appreciation_list' %}" class="btn btn-outline-secondary">
<i class="fas fa-times me-2"></i>
{% trans "Cancel" %}
</a>
<button type="submit" class="btn btn-success">
<i class="fas fa-paper-plane me-2"></i>
{% trans "Send Appreciation" %}
</button>
</div>
</form>
</div>
</div>
</div>
<!-- Sidebar -->
<div class="col-lg-4">
<!-- Tips -->
<div class="card shadow-sm mb-4">
<div class="card-header bg-light">
<h6 class="card-title mb-0">
<i class="fas fa-lightbulb me-2"></i>
{% trans "Tips for Writing Appreciation" %}
</h6>
</div>
<div class="card-body">
<ul class="list-unstyled mb-0">
<li class="mb-2">
<i class="fas fa-check text-success me-2"></i>
{% trans "Be specific about what you appreciate" %}
</li>
<li class="mb-2">
<i class="fas fa-check text-success me-2"></i>
{% trans "Use the person's name when addressing them" %}
</li>
<li class="mb-2">
<i class="fas fa-check text-success me-2"></i>
{% trans "Mention the impact of their actions" %}
</li>
<li class="mb-2">
<i class="fas fa-check text-success me-2"></i>
{% trans "Be sincere and authentic" %}
</li>
<li>
<i class="fas fa-check text-success me-2"></i>
{% trans "Keep it positive and uplifting" %}
</li>
</ul>
</div>
</div>
<!-- Visibility Guide -->
<div class="card shadow-sm">
<div class="card-header bg-light">
<h6 class="card-title mb-0">
<i class="fas fa-info-circle me-2"></i>
{% trans "Visibility Levels" %}
</h6>
</div>
<div class="card-body">
<ul class="list-unstyled mb-0">
<li class="mb-2">
<strong>{% trans "Private:" %}</strong>
<p class="small text-muted mb-0">
{% trans "Only you and the recipient can see this appreciation" %}
</p>
</li>
<li class="mb-2">
<strong>{% trans "Department:" %}</strong>
<p class="small text-muted mb-0">
{% trans "Visible to everyone in the selected department" %}
</p>
</li>
<li class="mb-2">
<strong>{% trans "Hospital:" %}</strong>
<p class="small text-muted mb-0">
{% trans "Visible to everyone in the selected hospital" %}
</p>
</li>
<li>
<strong>{% trans "Public:" %}</strong>
<p class="small text-muted mb-0">
{% trans "Visible to all PX360 users" %}
</p>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
{{ block.super }}
<script>
document.addEventListener('DOMContentLoaded', function() {
const hospitalSelect = document.getElementById('hospital_id');
const recipientTypeSelect = document.getElementById('recipient_type');
const recipientSelect = document.getElementById('recipient_id');
const departmentSelect = document.getElementById('department_id');
const recipientHelp = document.getElementById('recipientHelp');
let recipientData = [];
// Load recipients when hospital changes
hospitalSelect.addEventListener('change', function() {
const hospitalId = this.value;
const recipientType = recipientTypeSelect.value;
recipientSelect.disabled = true;
recipientSelect.innerHTML = '<option value="">Loading...</option>';
recipientHelp.textContent = 'Loading recipients...';
if (!hospitalId) {
recipientSelect.innerHTML = '<option value="">-- Select Recipient --</option>';
recipientSelect.disabled = true;
recipientHelp.textContent = 'Select a hospital first';
return;
}
// Fetch recipients
const url = recipientType === 'user'
? "{% url 'appreciation:get_users_by_hospital' %}?hospital_id=" + hospitalId
: "{% url 'appreciation:get_physicians_by_hospital' %}?hospital_id=" + hospitalId;
fetch(url)
.then(response => response.json())
.then(data => {
recipientData = recipientType === 'user' ? data.users : data.physicians;
recipientSelect.innerHTML = '<option value="">-- Select Recipient --</option>';
recipientData.forEach(item => {
const option = document.createElement('option');
option.value = item.id;
option.textContent = item.name;
recipientSelect.appendChild(option);
});
recipientSelect.disabled = false;
recipientHelp.textContent = `${recipientData.length} recipients found`;
})
.catch(error => {
console.error('Error:', error);
recipientSelect.innerHTML = '<option value="">Error loading recipients</option>';
recipientHelp.textContent = 'Error loading recipients';
});
});
// Load departments when hospital changes
hospitalSelect.addEventListener('change', function() {
const hospitalId = this.value;
departmentSelect.innerHTML = '<option value="">-- Select Department --</option>';
if (!hospitalId) return;
fetch("{% url 'appreciation:get_departments_by_hospital' %}?hospital_id=" + hospitalId)
.then(response => response.json())
.then(data => {
data.departments.forEach(item => {
const option = document.createElement('option');
option.value = item.id;
option.textContent = item.name;
departmentSelect.appendChild(option);
});
})
.catch(error => {
console.error('Error:', error);
});
});
// Refresh recipients when recipient type changes
recipientTypeSelect.addEventListener('change', function() {
if (hospitalSelect.value) {
hospitalSelect.dispatchEvent(new Event('change'));
}
});
});
</script>
{% endblock %}

View File

@ -0,0 +1,245 @@
{% extends "layouts/base.html" %}
{% load i18n static %}
{% block title %}{% if form.instance.pk %}{% trans "Edit Badge" %}{% else %}{% trans "Add Badge" %}{% endif %} - {% endblock %}
{% block content %}
<div class="container-fluid py-4">
<!-- Breadcrumb -->
<nav aria-label="breadcrumb" class="mb-4">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{% url 'appreciation:appreciation_list' %}">{% trans "Appreciation" %}</a></li>
<li class="breadcrumb-item"><a href="{% url 'appreciation:badge_list' %}">{% trans "Badges" %}</a></li>
<li class="breadcrumb-item active" aria-current="page">{% if form.instance.pk %}{% trans "Edit" %}{% else %}{% trans "Add" %}{% endif %}</li>
</ol>
</nav>
<!-- Form -->
<div class="row">
<div class="col-lg-8">
<div class="card shadow-sm">
<div class="card-header bg-warning text-dark">
<h4 class="mb-0">
<i class="fas fa-award me-2"></i>
{% if form.instance.pk %}{% trans "Edit Badge" %}{% else %}{% trans "Add Badge" %}{% endif %}
</h4>
</div>
<div class="card-body">
<form method="post">
{% csrf_token %}
<!-- Name (English) -->
<div class="mb-3">
<label for="id_name_en" class="form-label">
{% trans "Name (English)" %} <span class="text-danger">*</span>
</label>
<input
type="text"
class="form-control"
id="id_name_en"
name="name_en"
value="{{ form.name_en.value|default:'' }}"
required
>
</div>
<!-- Name (Arabic) -->
<div class="mb-3">
<label for="id_name_ar" class="form-label">
{% trans "Name (Arabic)" %} <span class="text-danger">*</span>
</label>
<input
type="text"
class="form-control"
id="id_name_ar"
name="name_ar"
value="{{ form.name_ar.value|default:'' }}"
dir="rtl"
required
>
</div>
<!-- Description (English) -->
<div class="mb-3">
<label for="id_description_en" class="form-label">{% trans "Description (English)" %}</label>
<textarea
class="form-control"
id="id_description_en"
name="description_en"
rows="3"
>{{ form.description_en.value|default:'' }}</textarea>
</div>
<!-- Description (Arabic) -->
<div class="mb-3">
<label for="id_description_ar" class="form-label">{% trans "Description (Arabic)" %}</label>
<textarea
class="form-control"
id="id_description_ar"
name="description_ar"
rows="3"
dir="rtl"
>{{ form.description_ar.value|default:'' }}</textarea>
</div>
<!-- Icon -->
<div class="mb-3">
<label for="id_icon" class="form-label">{% trans "Icon" %}</label>
<input
type="text"
class="form-control"
id="id_icon"
name="icon"
value="{{ form.icon.value|default:'fa-trophy' }}"
placeholder="fa-trophy"
>
<div class="form-text">
{% trans "FontAwesome icon class (e.g., fa-trophy, fa-star, fa-medal)" %}
</div>
</div>
<!-- Criteria Type -->
<div class="mb-3">
<label for="id_criteria_type" class="form-label">{% trans "Criteria Type" %}</label>
<select class="form-select" id="id_criteria_type" name="criteria_type">
{% for choice in form.fields.criteria_type.choices %}
<option value="{{ choice.0 }}" {% if choice.0 == form.criteria_type.value|stringformat:'s' or (not form.criteria_type.value and choice.0 == 'count') %}selected{% endif %}>
{{ choice.1 }}
</option>
{% endfor %}
</select>
</div>
<!-- Criteria Value -->
<div class="mb-3">
<label for="id_criteria_value" class="form-label">{% trans "Criteria Value" %}</label>
<input
type="number"
class="form-control"
id="id_criteria_value"
name="criteria_value"
value="{{ form.criteria_value.value|default:'' }}"
min="1"
required
>
<div class="form-text">
{% trans "Number of appreciations required to earn this badge" %}
</div>
</div>
<!-- Is Active -->
<div class="mb-4">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="id_is_active"
name="is_active"
{% if form.is_active.value %}checked{% endif %}
>
<label class="form-check-label" for="id_is_active">
{% trans "Active" %}
</label>
</div>
</div>
<!-- Buttons -->
<div class="d-flex gap-2">
<a href="{% url 'appreciation:badge_list' %}" class="btn btn-outline-secondary">
<i class="fas fa-times me-2"></i>
{% trans "Cancel" %}
</a>
<button type="submit" class="btn btn-warning text-dark">
<i class="fas fa-save me-2"></i>
{% if form.instance.pk %}{% trans "Update" %}{% else %}{% trans "Create" %}{% endif %}
</button>
</div>
</form>
</div>
</div>
</div>
<!-- Sidebar -->
<div class="col-lg-4">
<!-- Icon Preview -->
<div class="card shadow-sm mb-4">
<div class="card-header bg-light">
<h6 class="card-title mb-0">{% trans "Badge Preview" %}</h6>
</div>
<div class="card-body text-center">
<div class="badge-icon-wrapper mb-3">
<i id="icon-preview" class="{{ form.icon.value|default:'fa-trophy' }} fa-4x text-warning"></i>
</div>
<p id="name-preview" class="h5 mb-2">{{ form.name_en.value|default:'Badge Name' }}</p>
<p class="small text-muted mb-0">
<strong>{% trans "Requires" %}: </strong>
<span id="criteria-preview">{{ form.criteria_value.value|default:0 }}</span>
{% trans "appreciations" %}
</p>
</div>
</div>
<!-- Criteria Information -->
<div class="card shadow-sm">
<div class="card-header bg-light">
<h6 class="card-title mb-0">
<i class="fas fa-info-circle me-2"></i>
{% trans "About Badge Criteria" %}
</h6>
</div>
<div class="card-body">
<ul class="list-unstyled mb-0 small">
<li class="mb-2">
<strong>{% trans "Count:" %}</strong>
<p class="mb-0 text-muted">
{% trans "Badge is earned after receiving the specified number of appreciations" %}
</p>
</li>
<li>
<strong>{% trans "Tips:" %}</strong>
<ul class="mb-0 ps-3 text-muted">
<li>{% trans "Set achievable criteria to encourage participation" %}</li>
<li>{% trans "Use descriptive names and icons" %}</li>
<li>{% trans "Create badges for different achievement levels" %}</li>
<li>{% trans "Deactivate badges instead of deleting to preserve history" %}</li>
</ul>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
{{ block.super }}
<script>
document.addEventListener('DOMContentLoaded', function() {
const iconInput = document.getElementById('id_icon');
const iconPreview = document.getElementById('icon-preview');
const nameEnInput = document.getElementById('id_name_en');
const namePreview = document.getElementById('name-preview');
const criteriaValueInput = document.getElementById('id_criteria_value');
const criteriaPreview = document.getElementById('criteria-preview');
// Update icon preview
iconInput.addEventListener('input', function() {
const iconClass = this.value.trim() || 'fa-trophy';
iconPreview.className = iconClass + ' fa-4x text-warning';
});
// Update name preview
nameEnInput.addEventListener('input', function() {
const name = this.value.trim() || 'Badge Name';
namePreview.textContent = name;
});
// Update criteria preview
criteriaValueInput.addEventListener('input', function() {
const value = this.value.trim() || '0';
criteriaPreview.textContent = value;
});
});
</script>
{% endblock %}

View File

@ -0,0 +1,137 @@
{% extends "layouts/base.html" %}
{% load i18n static %}
{% block title %}{% trans "Appreciation Badges" %} - {% endblock %}
{% block content %}
<div class="container-fluid py-4">
<!-- Breadcrumb -->
<nav aria-label="breadcrumb" class="mb-4">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{% url 'appreciation:appreciation_list' %}">{% trans "Appreciation" %}</a></li>
<li class="breadcrumb-item active" aria-current="page">{% trans "Badges" %}</li>
</ol>
</nav>
<!-- Page Header -->
<div class="d-flex justify-content-between align-items-center mb-4">
<h2>
<i class="fas fa-award text-warning me-2"></i>
{% trans "Appreciation Badges" %}
</h2>
<a href="{% url 'appreciation:badge_create' %}" class="btn btn-primary">
<i class="fas fa-plus me-2"></i>
{% trans "Add Badge" %}
</a>
</div>
<!-- Badges List -->
<div class="card shadow-sm">
<div class="card-body">
{% if badges %}
<div class="row">
{% for badge in badges %}
<div class="col-md-6 col-lg-4 mb-4">
<div class="card h-100 border-2 border-warning">
<div class="card-body">
<div class="text-center mb-3">
<i class="{{ badge.icon }} fa-4x text-warning"></i>
</div>
<h5 class="card-title text-center">{{ badge.name_en }}</h5>
<p class="card-text small text-muted text-center">
{{ badge.description_en|truncatewords:10 }}
</p>
<hr>
<ul class="list-unstyled small mb-3">
<li class="mb-2">
<strong>{% trans "Type:" %}</strong>
{{ badge.get_criteria_type_display }}
</li>
<li class="mb-2">
<strong>{% trans "Value:" %}</strong>
{{ badge.criteria_value }}
</li>
<li class="mb-2">
<strong>{% trans "Earned:" %}</strong>
{{ badge.earned_count }} {% trans "times" %}
</li>
<li>
<strong>{% trans "Status:" %}</strong>
{% if badge.is_active %}
<span class="badge bg-success">{% trans "Active" %}</span>
{% else %}
<span class="badge bg-secondary">{% trans "Inactive" %}</span>
{% endif %}
</li>
</ul>
<div class="d-grid gap-2">
<a href="{% url 'appreciation:badge_edit' badge.id %}" class="btn btn-sm btn-outline-primary">
<i class="fas fa-edit me-2"></i>{% trans "Edit" %}
</a>
<a href="{% url 'appreciation:badge_delete' badge.id %}" class="btn btn-sm btn-outline-danger">
<i class="fas fa-trash me-2"></i>{% trans "Delete" %}
</a>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
<!-- Pagination -->
{% if page_obj.has_other_pages %}
<nav aria-label="Page navigation" class="mt-4">
<ul class="pagination justify-content-center">
{% if page_obj.has_previous %}
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.previous_page_number }}">
{% trans "Previous" %}
</a>
</li>
{% else %}
<li class="page-item disabled">
<span class="page-link">{% trans "Previous" %}</span>
</li>
{% endif %}
{% for num in page_obj.paginator.page_range %}
{% if page_obj.number == num %}
<li class="page-item active">
<span class="page-link">{{ num }}</span>
</li>
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
<li class="page-item">
<a class="page-link" href="?page={{ num }}">{{ num }}</a>
</li>
{% endif %}
{% endfor %}
{% if page_obj.has_next %}
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.next_page_number }}">
{% trans "Next" %}
</a>
</li>
{% else %}
<li class="page-item disabled">
<span class="page-link">{% trans "Next" %}</span>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
{% else %}
<div class="text-center py-5">
<i class="fas fa-award fa-4x text-muted mb-3"></i>
<h4 class="text-muted">{% trans "No badges found" %}</h4>
<p class="text-muted mb-3">{% trans "Create badges to motivate and recognize achievements" %}</p>
<a href="{% url 'appreciation:badge_create' %}" class="btn btn-primary">
<i class="fas fa-plus me-2"></i>
{% trans "Add Badge" %}
</a>
</div>
{% endif %}
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,214 @@
{% extends "layouts/base.html" %}
{% load i18n static %}
{% block title %}{% if form.instance.pk %}{% trans "Edit Category" %}{% else %}{% trans "Add Category" %}{% endif %} - {% endblock %}
{% block content %}
<div class="container-fluid py-4">
<!-- Breadcrumb -->
<nav aria-label="breadcrumb" class="mb-4">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{% url 'appreciation:appreciation_list' %}">{% trans "Appreciation" %}</a></li>
<li class="breadcrumb-item"><a href="{% url 'appreciation:category_list' %}">{% trans "Categories" %}</a></li>
<li class="breadcrumb-item active" aria-current="page">{% if form.instance.pk %}{% trans "Edit" %}{% else %}{% trans "Add" %}{% endif %}</li>
</ol>
</nav>
<!-- Form -->
<div class="row">
<div class="col-lg-8">
<div class="card shadow-sm">
<div class="card-header bg-primary text-white">
<h4 class="mb-0">
<i class="fas fa-tag me-2"></i>
{% if form.instance.pk %}{% trans "Edit Category" %}{% else %}{% trans "Add Category" %}{% endif %}
</h4>
</div>
<div class="card-body">
<form method="post">
{% csrf_token %}
<!-- Name (English) -->
<div class="mb-3">
<label for="id_name_en" class="form-label">
{% trans "Name (English)" %} <span class="text-danger">*</span>
</label>
<input
type="text"
class="form-control"
id="id_name_en"
name="name_en"
value="{{ form.name_en.value|default:'' }}"
required
>
</div>
<!-- Name (Arabic) -->
<div class="mb-3">
<label for="id_name_ar" class="form-label">
{% trans "Name (Arabic)" %} <span class="text-danger">*</span>
</label>
<input
type="text"
class="form-control"
id="id_name_ar"
name="name_ar"
value="{{ form.name_ar.value|default:'' }}"
dir="rtl"
required
>
</div>
<!-- Description (English) -->
<div class="mb-3">
<label for="id_description_en" class="form-label">{% trans "Description (English)" %}</label>
<textarea
class="form-control"
id="id_description_en"
name="description_en"
rows="3"
>{{ form.description_en.value|default:'' }}</textarea>
</div>
<!-- Description (Arabic) -->
<div class="mb-3">
<label for="id_description_ar" class="form-label">{% trans "Description (Arabic)" %}</label>
<textarea
class="form-control"
id="id_description_ar"
name="description_ar"
rows="3"
dir="rtl"
>{{ form.description_ar.value|default:'' }}</textarea>
</div>
<!-- Icon -->
<div class="mb-3">
<label for="id_icon" class="form-label">{% trans "Icon" %}</label>
<input
type="text"
class="form-control"
id="id_icon"
name="icon"
value="{{ form.icon.value|default:'fa-heart' }}"
placeholder="fa-heart"
>
<div class="form-text">
{% trans "FontAwesome icon class (e.g., fa-heart, fa-star, fa-thumbs-up)" %}
</div>
</div>
<!-- Color -->
<div class="mb-3">
<label for="id_color" class="form-label">{% trans "Color" %}</label>
<select class="form-select" id="id_color" name="color">
{% for choice in form.fields.color.choices %}
<option value="{{ choice.0 }}" {% if choice.0 == form.color.value|stringformat:'s' or (not form.color.value and choice.0 == 'primary') %}selected{% endif %}>
{{ choice.1 }}
</option>
{% endfor %}
</select>
</div>
<!-- Is Active -->
<div class="mb-4">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="id_is_active"
name="is_active"
{% if form.is_active.value %}checked{% endif %}
>
<label class="form-check-label" for="id_is_active">
{% trans "Active" %}
</label>
</div>
</div>
<!-- Buttons -->
<div class="d-flex gap-2">
<a href="{% url 'appreciation:category_list' %}" class="btn btn-outline-secondary">
<i class="fas fa-times me-2"></i>
{% trans "Cancel" %}
</a>
<button type="submit" class="btn btn-primary">
<i class="fas fa-save me-2"></i>
{% if form.instance.pk %}{% trans "Update" %}{% else %}{% trans "Create" %}{% endif %}
</button>
</div>
</form>
</div>
</div>
</div>
<!-- Sidebar -->
<div class="col-lg-4">
<!-- Icon Preview -->
<div class="card shadow-sm mb-4">
<div class="card-header bg-light">
<h6 class="card-title mb-0">{% trans "Icon Preview" %}</h6>
</div>
<div class="card-body text-center">
<i id="icon-preview" class="{{ form.icon.value|default:'fa-heart' }} fa-4x text-primary mb-3"></i>
<p id="name-preview" class="h5 mb-0">{{ form.name_en.value|default:'Category Name' }}</p>
</div>
</div>
<!-- Tips -->
<div class="card shadow-sm">
<div class="card-header bg-light">
<h6 class="card-title mb-0">
<i class="fas fa-lightbulb me-2"></i>
{% trans "Tips" %}
</h6>
</div>
<div class="card-body">
<ul class="list-unstyled mb-0 small">
<li class="mb-2">
<i class="fas fa-check text-success me-2"></i>
{% trans "Use descriptive names for categories" %}
</li>
<li class="mb-2">
<i class="fas fa-check text-success me-2"></i>
{% trans "Choose appropriate icons for each category" %}
</li>
<li class="mb-2">
<i class="fas fa-check text-success me-2"></i>
{% trans "Colors help users quickly identify categories" %}
</li>
<li>
<i class="fas fa-check text-success me-2"></i>
{% trans "Deactivate unused categories instead of deleting" %}
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
{{ block.super }}
<script>
document.addEventListener('DOMContentLoaded', function() {
const iconInput = document.getElementById('id_icon');
const iconPreview = document.getElementById('icon-preview');
const nameEnInput = document.getElementById('id_name_en');
const namePreview = document.getElementById('name-preview');
// Update icon preview
iconInput.addEventListener('input', function() {
const iconClass = this.value.trim() || 'fa-heart';
iconPreview.className = iconClass + ' fa-4x text-primary';
});
// Update name preview
nameEnInput.addEventListener('input', function() {
const name = this.value.trim() || 'Category Name';
namePreview.textContent = name;
});
});
</script>
{% endblock %}

View File

@ -0,0 +1,85 @@
{% extends "layouts/base.html" %}
{% load i18n static %}
{% block title %}{% trans "Appreciation Categories" %} - {% endblock %}
{% block content %}
<div class="container-fluid py-4">
<!-- Breadcrumb -->
<nav aria-label="breadcrumb" class="mb-4">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{% url 'appreciation:appreciation_list' %}">{% trans "Appreciation" %}</a></li>
<li class="breadcrumb-item active" aria-current="page">{% trans "Categories" %}</li>
</ol>
</nav>
<!-- Page Header -->
<div class="d-flex justify-content-between align-items-center mb-4">
<h2>
<i class="fas fa-tags text-primary me-2"></i>
{% trans "Appreciation Categories" %}
</h2>
<a href="{% url 'appreciation:category_create' %}" class="btn btn-primary">
<i class="fas fa-plus me-2"></i>
{% trans "Add Category" %}
</a>
</div>
<!-- Categories List -->
<div class="card shadow-sm">
<div class="card-body">
{% if categories %}
<div class="table-responsive">
<table class="table table-hover">
<thead class="table-light">
<tr>
<th>{% trans "Icon" %}</th>
<th>{% trans "Name (English)" %}</th>
<th>{% trans "Name (Arabic)" %}</th>
<th>{% trans "Color" %}</th>
<th>{% trans "Count" %}</th>
<th class="text-center">{% trans "Actions" %}</th>
</tr>
</thead>
<tbody>
{% for category in categories %}
<tr>
<td>
<i class="{{ category.icon }} fa-2x text-primary"></i>
</td>
<td>{{ category.name_en }}</td>
<td dir="rtl">{{ category.name_ar }}</td>
<td>
<span class="badge bg-{{ category.color }}">
{{ category.get_color_display }}
</span>
</td>
<td>{{ category.appreciation_count }}</td>
<td class="text-center">
<a href="{% url 'appreciation:category_edit' category.id %}" class="btn btn-sm btn-outline-primary">
<i class="fas fa-edit"></i>
</a>
<a href="{% url 'appreciation:category_delete' category.id %}" class="btn btn-sm btn-outline-danger">
<i class="fas fa-trash"></i>
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="text-center py-5">
<i class="fas fa-tags fa-4x text-muted mb-3"></i>
<h4 class="text-muted">{% trans "No categories found" %}</h4>
<p class="text-muted mb-3">{% trans "Create categories to organize appreciations" %}</p>
<a href="{% url 'appreciation:category_create' %}" class="btn btn-primary">
<i class="fas fa-plus me-2"></i>
{% trans "Add Category" %}
</a>
</div>
{% endif %}
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,248 @@
{% extends "layouts/base.html" %}
{% load i18n static %}
{% block title %}{% trans "Appreciation Leaderboard" %} - {% endblock %}
{% block content %}
<div class="container-fluid py-4">
<!-- Breadcrumb -->
<nav aria-label="breadcrumb" class="mb-4">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{% url 'appreciation:appreciation_list' %}">{% trans "Appreciation" %}</a></li>
<li class="breadcrumb-item active" aria-current="page">{% trans "Leaderboard" %}</li>
</ol>
</nav>
<!-- Page Header -->
<div class="d-flex justify-content-between align-items-center mb-4">
<h2>
<i class="fas fa-trophy text-warning me-2"></i>
{% trans "Appreciation Leaderboard" %}
</h2>
<div>
<a href="{% url 'appreciation:appreciation_send' %}" class="btn btn-primary">
<i class="fas fa-paper-plane me-2"></i>
{% trans "Send Appreciation" %}
</a>
</div>
</div>
<!-- Filters -->
<div class="card shadow-sm mb-4">
<div class="card-body">
<form method="get" class="row g-3">
<div class="col-md-3">
<label for="year" class="form-label">{% trans "Year" %}</label>
<select class="form-select" id="year" name="year">
{% for y in years %}
<option value="{{ y }}" {% if y == selected_year %}selected{% endif %}>{{ y }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-3">
<label for="month" class="form-label">{% trans "Month" %}</label>
<select class="form-select" id="month" name="month">
{% for m in months %}
<option value="{{ m.0 }}" {% if m.0 == selected_month %}selected{% endif %}>{{ m.1 }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-3">
<label for="hospital" class="form-label">{% trans "Hospital" %}</label>
<select class="form-select" id="hospital" name="hospital">
<option value="">All Hospitals</option>
{% for hospital in hospitals %}
<option value="{{ hospital.id }}" {% if filters.hospital == hospital.id|stringformat:"s" %}selected{% endif %}>
{{ hospital.name }}
</option>
{% endfor %}
</select>
</div>
<div class="col-md-3">
<label for="department" class="form-label">{% trans "Department" %}</label>
<select class="form-select" id="department" name="department">
<option value="">All Departments</option>
{% for department in departments %}
<option value="{{ department.id }}" {% if filters.department == department.id|stringformat:"s" %}selected{% endif %}>
{{ department.name }}
</option>
{% endfor %}
</select>
</div>
<div class="col-12">
<button type="submit" class="btn btn-primary">
<i class="fas fa-filter me-2"></i>
{% trans "Apply Filters" %}
</button>
<a href="{% url 'appreciation:leaderboard_view' %}" class="btn btn-outline-secondary">
<i class="fas fa-redo me-2"></i>
{% trans "Reset" %}
</a>
</div>
</form>
</div>
</div>
<!-- Leaderboard Table -->
<div class="card shadow-sm">
<div class="card-body">
{% if page_obj %}
<div class="table-responsive">
<table class="table table-hover">
<thead class="table-light">
<tr>
<th>Rank</th>
<th>{% trans "Recipient" %}</th>
<th>{% trans "Hospital" %}</th>
<th>{% trans "Department" %}</th>
<th class="text-center">{% trans "Received" %}</th>
<th class="text-center">{% trans "Sent" %}</th>
<th class="text-center">{% trans "Hospital Rank" %}</th>
</tr>
</thead>
<tbody>
{% for item in page_obj %}
<tr class="{% if item.get_recipient_name == request.user.get_full_name %}table-primary{% endif %}">
<td>
{% if forloop.counter <= 3 %}
<span class="badge bg-{% if forloop.counter == 1 %}warning{% elif forloop.counter == 2 %}secondary{% else %}info{% endif %} fs-6">
{% if forloop.counter == 1 %}<i class="fas fa-crown me-1"></i>{% endif %}
#{{ forloop.counter }}
</span>
{% else %}
<span class="text-muted">#{{ forloop.counter }}</span>
{% endif %}
</td>
<td>
<i class="fas fa-user text-primary me-2"></i>
{{ item.get_recipient_name }}
</td>
<td>{{ item.hospital.name }}</td>
<td>{% if item.department %}{{ item.department.name }}{% else %}-{% endif %}</td>
<td class="text-center">
<span class="badge bg-success fs-6">{{ item.received_count }}</span>
</td>
<td class="text-center">
<span class="badge bg-info fs-6">{{ item.sent_count }}</span>
</td>
<td class="text-center">
{% if item.hospital_rank %}
{% if item.hospital_rank <= 3 %}
<span class="badge bg-warning">
#{{ item.hospital_rank }}
</span>
{% else %}
<span class="badge bg-secondary">
#{{ item.hospital_rank }}
</span>
{% endif %}
{% else %}
-
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- Pagination -->
{% if page_obj.has_other_pages %}
<nav aria-label="Page navigation" class="mt-4">
<ul class="pagination justify-content-center">
{% if page_obj.has_previous %}
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.previous_page_number }}{% if filters.urlencode %}&{{ filters.urlencode }}{% endif %}">
{% trans "Previous" %}
</a>
</li>
{% else %}
<li class="page-item disabled">
<span class="page-link">{% trans "Previous" %}</span>
</li>
{% endif %}
{% for num in page_obj.paginator.page_range %}
{% if page_obj.number == num %}
<li class="page-item active">
<span class="page-link">{{ num }}</span>
</li>
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
<li class="page-item">
<a class="page-link" href="?page={{ num }}{% if filters.urlencode %}&{{ filters.urlencode }}{% endif %}">{{ num }}</a>
</li>
{% endif %}
{% endfor %}
{% if page_obj.has_next %}
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.next_page_number }}{% if filters.urlencode %}&{{ filters.urlencode }}{% endif %}">
{% trans "Next" %}
</a>
</li>
{% else %}
<li class="page-item disabled">
<span class="page-link">{% trans "Next" %}</span>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
{% else %}
<div class="text-center py-5">
<i class="fas fa-trophy fa-4x text-muted mb-3"></i>
<h4 class="text-muted">{% trans "No appreciations found for this period" %}</h4>
<p class="text-muted">{% trans "Try changing the filters or select a different time period" %}</p>
</div>
{% endif %}
</div>
</div>
<!-- Quick Actions -->
<div class="row mt-4">
<div class="col-md-4">
<div class="card shadow-sm h-100">
<div class="card-body text-center">
<i class="fas fa-paper-plane fa-3x text-primary mb-3"></i>
<h5>{% trans "Send Appreciation" %}</h5>
<p class="text-muted mb-3">{% trans "Share your appreciation with colleagues" %}</p>
<a href="{% url 'appreciation:appreciation_send' %}" class="btn btn-primary">
{% trans "Send Now" %}
</a>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card shadow-sm h-100">
<div class="card-body text-center">
<i class="fas fa-award fa-3x text-warning mb-3"></i>
<h5>{% trans "View Badges" %}</h5>
<p class="text-muted mb-3">{% trans "See your earned badges" %}</p>
<a href="{% url 'appreciation:my_badges_view' %}" class="btn btn-warning">
{% trans "My Badges" %}
</a>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card shadow-sm h-100">
<div class="card-body text-center">
<i class="fas fa-list fa-3x text-info mb-3"></i>
<h5>{% trans "All Appreciations" %}</h5>
<p class="text-muted mb-3">{% trans "View your appreciations" %}</p>
<a href="{% url 'appreciation:appreciation_list' %}" class="btn btn-info">
{% trans "View List" %}
</a>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
{{ block.super }}
<script>
// Add any leaderboard-specific JavaScript here
</script>
{% endblock %}

View File

@ -0,0 +1,256 @@
{% extends "layouts/base.html" %}
{% load i18n static %}
{% block title %}{% trans "My Badges" %} - {% endblock %}
{% block content %}
<div class="container-fluid py-4">
<!-- Breadcrumb -->
<nav aria-label="breadcrumb" class="mb-4">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{% url 'appreciation:appreciation_list' %}">{% trans "Appreciation" %}</a></li>
<li class="breadcrumb-item active" aria-current="page">{% trans "My Badges" %}</li>
</ol>
</nav>
<!-- Page Header -->
<div class="d-flex justify-content-between align-items-center mb-4">
<h2>
<i class="fas fa-award text-warning me-2"></i>
{% trans "My Badges" %}
</h2>
<div>
<a href="{% url 'appreciation:appreciation_send' %}" class="btn btn-primary">
<i class="fas fa-paper-plane me-2"></i>
{% trans "Send Appreciation" %}
</a>
</div>
</div>
<!-- Stats Summary -->
<div class="row mb-4">
<div class="col-md-4">
<div class="card shadow-sm border-primary">
<div class="card-body text-center">
<i class="fas fa-heart fa-2x text-danger mb-2"></i>
<h3 class="mb-0">{{ total_received }}</h3>
<p class="text-muted mb-0">{% trans "Total Appreciations Received" %}</p>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card shadow-sm border-warning">
<div class="card-body text-center">
<i class="fas fa-award fa-2x text-warning mb-2"></i>
<h3 class="mb-0">{{ badges|length }}</h3>
<p class="text-muted mb-0">{% trans "Badges Earned" %}</p>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card shadow-sm border-info">
<div class="card-body text-center">
<i class="fas fa-trophy fa-2x text-info mb-2"></i>
<h3 class="mb-0">{{ badge_progress|length }}</h3>
<p class="text-muted mb-0">{% trans "Available Badges" %}</p>
</div>
</div>
</div>
</div>
<div class="row">
<!-- Earned Badges -->
<div class="col-lg-8">
<div class="card shadow-sm mb-4">
<div class="card-header bg-warning text-white">
<h5 class="card-title mb-0">
<i class="fas fa-medal me-2"></i>
{% trans "Earned Badges" %}
</h5>
</div>
<div class="card-body">
{% if badges %}
<div class="row">
{% for user_badge in badges %}
<div class="col-md-6 col-lg-4 mb-3">
<div class="card h-100 border-2 border-warning">
<div class="card-body text-center">
<div class="badge-icon-wrapper mb-3">
<i class="{{ user_badge.badge.icon }} fa-4x text-warning"></i>
</div>
<h5 class="card-title">{{ user_badge.badge.name_en }}</h5>
<p class="card-text small text-muted">
{{ user_badge.badge.description_en|truncatewords:10 }}
</p>
<small class="text-muted">
<i class="fas fa-calendar-alt me-1"></i>
{% trans "Earned on" %} {{ user_badge.earned_at|date:"F j, Y" }}
</small>
</div>
</div>
</div>
{% endfor %}
</div>
<!-- Pagination -->
{% if page_obj.has_other_pages %}
<nav aria-label="Page navigation" class="mt-4">
<ul class="pagination justify-content-center">
{% if page_obj.has_previous %}
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.previous_page_number }}">
{% trans "Previous" %}
</a>
</li>
{% else %}
<li class="page-item disabled">
<span class="page-link">{% trans "Previous" %}</span>
</li>
{% endif %}
{% for num in page_obj.paginator.page_range %}
{% if page_obj.number == num %}
<li class="page-item active">
<span class="page-link">{{ num }}</span>
</li>
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
<li class="page-item">
<a class="page-link" href="?page={{ num }}">{{ num }}</a>
</li>
{% endif %}
{% endfor %}
{% if page_obj.has_next %}
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.next_page_number }}">
{% trans "Next" %}
</a>
</li>
{% else %}
<li class="page-item disabled">
<span class="page-link">{% trans "Next" %}</span>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
{% else %}
<div class="text-center py-5">
<i class="fas fa-award fa-4x text-muted mb-3"></i>
<h4 class="text-muted">{% trans "No badges earned yet" %}</h4>
<p class="text-muted">
{% trans "Start receiving appreciations to earn badges!" %}
</p>
</div>
{% endif %}
</div>
</div>
</div>
<!-- Badge Progress -->
<div class="col-lg-4">
<div class="card shadow-sm">
<div class="card-header bg-light">
<h6 class="card-title mb-0">
<i class="fas fa-chart-line me-2"></i>
{% trans "Badge Progress" %}
</h6>
</div>
<div class="card-body">
{% for item in badge_progress %}
<div class="mb-3 {% if not item.earned %}opacity-75{% endif %}">
<div class="d-flex align-items-center mb-1">
<i class="{{ item.badge.icon }} me-2 {% if item.earned %}text-warning{% else %}text-muted{% endif %}"></i>
<div>
<strong class="{% if item.earned %}text-warning{% else %}text-muted{% endif %}">
{{ item.badge.name_en }}
</strong>
{% if item.earned %}
<span class="badge bg-success ms-2"></span>
{% endif %}
</div>
</div>
<div class="progress" style="height: 10px;">
<div
class="progress-bar {% if item.earned %}bg-warning{% else %}bg-info{% endif %}"
role="progressbar"
style="width: {{ item.progress }}%"
aria-valuenow="{{ item.progress }}"
aria-valuemin="0"
aria-valuemax="100"
></div>
</div>
<small class="text-muted">
{% if item.badge.criteria_type == 'count' %}
{% trans "Requires" %} {{ item.badge.criteria_value }} {% trans "appreciations" %}
{% endif %}
({% trans "Progress" %}: {{ item.progress }}%)
</small>
</div>
{% empty %}
<div class="text-center py-3">
<i class="fas fa-trophy fa-2x text-muted mb-2"></i>
<p class="text-muted mb-0">{% trans "No badges available" %}</p>
</div>
{% endfor %}
</div>
</div>
<!-- Tips -->
<div class="card shadow-sm mt-3">
<div class="card-header bg-light">
<h6 class="card-title mb-0">
<i class="fas fa-lightbulb me-2"></i>
{% trans "How to Earn Badges" %}
</h6>
</div>
<div class="card-body">
<ul class="list-unstyled mb-0 small">
<li class="mb-2">
<i class="fas fa-check text-success me-2"></i>
{% trans "Receive appreciations from colleagues" %}
</li>
<li class="mb-2">
<i class="fas fa-check text-success me-2"></i>
{% trans "Consistently provide excellent service" %}
</li>
<li class="mb-2">
<i class="fas fa-check text-success me-2"></i>
{% trans "Be a team player" %}
</li>
<li class="mb-2">
<i class="fas fa-check text-success me-2"></i>
{% trans "Show leadership qualities" %}
</li>
<li>
<i class="fas fa-check text-success me-2"></i>
{% trans "Demonstrate innovation" %}
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_css %}
{{ block.super }}
<style>
.badge-icon-wrapper {
padding: 20px;
border-radius: 50%;
background-color: #fff9c4;
width: 120px;
height: 120px;
margin: 0 auto;
display: flex;
align-items: center;
justify-content: center;
}
.opacity-75 {
opacity: 0.75;
}
</style>
{% endblock %}