feat: add i18n support for Vietnamese and English
Some checks failed
Tests / PHP 8.3 (push) Has been cancelled
Tests / PHP 8.4 (push) Has been cancelled
Tests / PHP 8.5 (push) Has been cancelled

This commit is contained in:
2026-05-04 10:40:39 +00:00
parent 3a8db5cae6
commit 8c6b71cb8a
64 changed files with 4652 additions and 254 deletions

107
AGENTS.md
View File

@@ -68,11 +68,19 @@ minicrm/
│ │ │ ├── Customers/ │ │ │ ├── Customers/
│ │ │ │ └── RelationManagers/FeedbacksRelationManager.php │ │ │ │ └── RelationManagers/FeedbacksRelationManager.php
│ │ │ ├── FeedbackChannels/ │ │ │ ├── FeedbackChannels/
│ │ │ ── FeedbackTags/ │ │ │ ── FeedbackTags/
│ │ │ ├── Contracts/
│ │ │ ├── Users/
│ │ │ └── Roles/
│ │ └── Widgets/ │ │ └── Widgets/
│ │ ├── ResolvedTicketsWidget.php # Dashboard: resolved tickets table │ │ ├── ResolvedTicketsWidget.php # Dashboard: resolved tickets table
│ │ ├── AvgProcessingTimeWidget.php # Dashboard: stats overview │ │ ├── AvgProcessingTimeWidget.php # Dashboard: stats overview
│ │ └── HandlerPerformanceWidget.php # Dashboard: bar chart per handler │ │ └── HandlerPerformanceWidget.php # Dashboard: bar chart per handler
│ ├── Enums/
│ │ ├── ContractType.php
│ │ ├── ContractStatus.php
│ │ ├── HandoverStatus.php
│ │ └── ServiceType.php
│ ├── Models/ │ ├── Models/
│ │ ├── User.php # HasRoles (Spatie), isAdmin(), isManager() │ │ ├── User.php # HasRoles (Spatie), isAdmin(), isManager()
│ │ ├── Department.php # NEW: name, manager_id → User │ │ ├── Department.php # NEW: name, manager_id → User
@@ -101,6 +109,15 @@ minicrm/
│ │ └── TicketClosed.php # DB + Mail for ticket closure │ │ └── TicketClosed.php # DB + Mail for ticket closure
│ └── Providers/Filament/ │ └── Providers/Filament/
│ └── AdminPanelProvider.php │ └── AdminPanelProvider.php
├── lang/ # i18n translation files
│ ├── en/
│ │ ├── app.php # General UI labels, navigation, status, widgets
│ │ ├── feedback.php # Feedback domain: services, notifications, interactions
│ │ └── enums.php # Enum labels
│ └── vi/
│ ├── app.php
│ ├── feedback.php
│ └── enums.php
├── resources/views/filament/resources/feedback/ ├── resources/views/filament/resources/feedback/
│ ├── similar-cases.blade.php │ ├── similar-cases.blade.php
│ └── attachment-links.blade.php # Modal listing attachments with signed download URLs │ └── attachment-links.blade.php # Modal listing attachments with signed download URLs
@@ -217,19 +234,84 @@ feedback_interactions, feedback_tags, feedback_feedback_tag, products, customers
- AvgProcessingTimeWidget: 4 stats (resolved count, avg time, pending count, escalated count) - AvgProcessingTimeWidget: 4 stats (resolved count, avg time, pending count, escalated count)
- HandlerPerformanceWidget: bar chart of avg processing time per handler - HandlerPerformanceWidget: bar chart of avg processing time per handler
## Internationalization (i18n)
### Configuration
- Default locale: `vi` (Vietnamese), fallback: `en` (English)
- Set in `.env`: `APP_LOCALE=vi`, `APP_FALLBACK_LOCALE=en`
- Filament uses `app()->getLocale()` — no extra panel config needed
### Translation Files
```
lang/
├── en/
│ ├── app.php # General UI: navigation, labels, status, widgets, blade strings
│ ├── feedback.php # Feedback domain: services, notifications, interactions
│ └── enums.php # Enum labels (HandoverStatus, ServiceType, ContractStatus, ContractType)
└── vi/
├── app.php
├── feedback.php
└── enums.php
```
### Translation Patterns
```php
// Resource labels — MUST override getPluralLabel() (not auto-translated)
public static function getPluralLabel(): ?string
{
return __('app.resource_feedbacks');
}
// Navigation group — MUST override getNavigationGroup() (not auto-translated)
public static function getNavigationGroup(): ?string
{
return __('app.nav_customer_care');
}
// Form fields — MUST add explicit ->label()
TextInput::make('name')->label(__('app.name'))
// Status options
'options' => [
'pending' => __('app.status_pending'),
'processing' => __('app.status_processing'),
]
// Enum labels
public function label(): string
{
return match ($this) {
self::ACTIVE => __('enums.contract_status.active'),
};
}
// Widget heading/description — override getHeading()/getDescription()
public function getHeading(): string | Htmlable | null
{
return __('app.widget_avg_time_by_handler');
}
```
### Key Gotchas
- `$pluralLabel` property does NOT auto-translate — must override `getPluralLabel()`
- `getNavigationGroup()` does NOT auto-translate — must call `__()` explicitly
- `$heading`/`$description` in ChartWidget are raw properties — must override `getHeading()`/`getDescription()`
- Form fields without `->label()` use Filament's `filament-panels::` namespace (may not resolve correctly)
- Always add `->label(__('key'))` to TextInput, Select, Textarea, etc.
## Navigation Structure ## Navigation Structure
``` ```
Dashboard Dashboard
├── Customer Care (group) ├── Customer Care / Chăm sóc khách hàng (group)
│ └── Feedbacks (chat-bubble-left-right, sort=1) │ └── Feedbacks / Phản ánh (chat-bubble-left-right, sort=1)
└── Management (group) └── Management / Quản lý (group)
├── Products (building-storefront, sort=1) ├── Products / Sản phẩm (building-storefront, sort=1)
├── Customers (user-group, sort=2) ├── Customers / Khách hàng (user-group, sort=2)
├── Channels (inbox-stack, sort=3) ├── Channels / Kênh phản ánh (inbox-stack, sort=3)
├── Tags (tag, sort=default) ├── Tags / Thẻ (tag, sort=default)
├── Users (users, sort=5) — admin only ├── Users / Người dùng (users, sort=5) — admin only
└── Roles (shield-check, sort=6) — admin only └── Roles / Vai trò (shield-check, sort=6) — admin only
``` ```
Dashboard widgets: ResolvedTicketsWidget, AvgProcessingTimeWidget, HandlerPerformanceWidget (admin/manager only) Dashboard widgets: ResolvedTicketsWidget, AvgProcessingTimeWidget, HandlerPerformanceWidget (admin/manager only)
@@ -337,3 +419,8 @@ $user->assignRole('manager');
8. **ChartWidget properties**: `$heading` and `$columnSpan` are non-static in Filament's ChartWidget. 8. **ChartWidget properties**: `$heading` and `$columnSpan` are non-static in Filament's ChartWidget.
9. **Manager closing permission**: Manager must be department head (`Department.manager_id`) to close tickets in that department. 9. **Manager closing permission**: Manager must be department head (`Department.manager_id`) to close tickets in that department.
10. **Docker**: Entrypoint auto-generates APP_KEY and runs migrations. SQLite persists via volume mount. 10. **Docker**: Entrypoint auto-generates APP_KEY and runs migrations. SQLite persists via volume mount.
11. **i18n - pluralLabel**: `$pluralLabel` property does NOT auto-translate — must override `getPluralLabel()` and call `__()`.
12. **i18n - navigationGroup**: `getNavigationGroup()` does NOT auto-translate — must return `__('key')` explicitly.
13. **i18n - ChartWidget heading**: `$heading`/`$description` are raw properties — must override `getHeading()`/`getDescription()`.
14. **i18n - form labels**: Form fields without `->label()` use Filament's `filament-panels::` namespace — always add explicit `->label(__('key'))`.
15. **i18n - Filament vendor**: Filament ships 57 Vietnamese translation files — auto-activated when `APP_LOCALE=vi`.

View File

@@ -137,6 +137,21 @@ Tham chiếu: `SOP_des.md` — Bản đặc tả hệ thống After-Sale CRM
- [x] UserPolicy + RolePolicy (admin-only) - [x] UserPolicy + RolePolicy (admin-only)
- [x] Tests updated: 8 tests, 21 assertions pass - [x] Tests updated: 8 tests, 21 assertions pass
### Internationalization (i18n) — Vietnamese + English
- [x] Config: `APP_LOCALE=vi`, `APP_FALLBACK_LOCALE=en` in `.env`
- [x] Created `lang/en/` and `lang/vi/` with 3 files each: `app.php`, `feedback.php`, `enums.php`
- [x] ~120 translation keys covering: navigation, labels, status, widgets, services, notifications, enums
- [x] All Resources: override `getPluralLabel()` + `getNavigationGroup()` with `__()` calls
- [x] All Form fields: explicit `->label(__('key'))` on TextInput, Select, Textarea, etc.
- [x] All Table columns: explicit `->label(__('key'))` on TextColumn, IconColumn
- [x] All RelationManagers: `$title` property uses translation key
- [x] All Widgets: `getHeading()`/`getDescription()` methods return translated strings
- [x] Enums: `ContractType`, `ContractStatus`, `HandoverStatus`, `ServiceType` use `__()` in `label()`
- [x] Services/Notifications: all user-facing strings use translation keys
- [x] Blade views: all hardcoded strings replaced with `__()` calls
- [x] Filament vendor translations (57 Vietnamese files) auto-activated for UI chrome
- [x] Tests: 8/8 pass (21 assertions)
--- ---
## Database Schema (final — 23 migrations) ## Database Schema (final — 23 migrations)

View File

@@ -0,0 +1,56 @@
# AfterSales CRM - System Assessment & Proposals (Draft)
> **Ngày ghi nhận:** Monday, May 4, 2026
> **Người thực hiện:** Gemini CLI Agent
> **Mục đích:** Ghi lại các nhận định về hệ thống, rủi ro bảo mật và đề xuất nâng cấp để thảo luận sau.
---
## 1. Nhận định Hệ thống (Current Understanding)
- **Kiến trúc:** Laravel 13.6 + Filament 5.6, PHP 8.3. Sử dụng Spatie Permissions cho RBAC.
- **Dữ liệu:** Đã import ~3600 records từ Excel thành công. Sử dụng SQLite làm database.
- **Workflow:** Quy trình Feedback -> Transfer -> Resolve -> Close đã được thiết lập chặt chẽ với các Service chuyên biệt (`ClosingService`, `TransferService`).
- **Tài liệu:** Đã có `AGENTS.md`, `PROGRESS.md`, `TASKS_ROADMAP.md``CODEBASE_SNAPSHOT.md` giúp nắm bắt nhanh trạng thái dự án.
## 2. Rủi ro Bảo mật & An toàn (Security & Safety Risks)
### 2.1. Phạm vi truy cập dữ liệu (Data Visibility)
- **Vấn đề:** `FeedbackPolicy` hiện tại cho phép mọi User có quyền `view-feedback` xem được toàn bộ các phản hồi của tất cả phòng ban.
- **Rủi ro:** Lộ thông tin nhạy cảm giữa các phòng ban.
- **Đề xuất:** Áp dụng Global Scope hoặc Scoping trong Resource để nhân viên chỉ thấy phiếu thuộc phòng ban mình hoặc được giao cho mình.
### 2.2. Bảo mật Backup
- **Vấn đề:** Command `app:export-backup` lưu file JSON (bao gồm hash mật khẩu và thông tin khách hàng) trực tiếp trong `storage/app/backups`.
- **Rủi ro:** Dễ bị đánh cắp nếu folder này bị lộ hoặc server bị chiếm quyền user.
- **Đề xuất:** Mã hóa file backup hoặc đẩy thẳng lên S3 với chính sách bảo mật nghiêm ngặt.
### 2.3. Signed URL trên Local Disk
- **Vấn đề:** `FileService` dùng `temporaryUrl` trên driver `local`.
- **Rủi ro:** Laravel mặc định không hỗ trợ URL tạm thời cho local driver trừ khi được cấu hình thêm. Có thể gây lỗi không xem được ảnh/file trong thực tế.
### 2.4. Tính nhất quán của Logic Đóng phiếu
- **Vấn đề:** Logic kiểm tra `proof_of_resolution` đang nằm rải rác ở Service và một số chỗ trong Filament UI.
- **Rủi ro:** Khó bảo trì và dễ xảy ra sai sót khi cập nhật quy trình.
## 3. Đề xuất Nâng cấp (Proposed Upgrades)
### 3.1. Audit Trail (Lịch sử chi tiết)
- **Mô tả:** Ghi lại chi tiết ai đã sửa cái gì, từ giá trị cũ sang giá trị mới cho tất cả các Model quan trọng (Contract, Feedback, Customer).
- **Công cụ gợi ý:** `spatie/laravel-activitylog`.
### 3.2. Mã hóa dữ liệu (PII Encryption)
- **Mô tả:** Mã hóa các trường thông tin cá nhân (Số điện thoại, Email) trong Database để đảm bảo an toàn nếu database bị leak.
### 3.3. Theo dõi SLA (SLA Tracking)
- **Mô tả:** Thiết lập thời hạn xử lý cho từng loại phản hồi. Cảnh báo (Email/Notification) khi sắp đến hạn hoặc quá hạn.
### 3.4. Gợi ý chuyển phòng thông minh (Smart Suggestion)
- **Mô tả:** Tự động phân tích loại Hợp đồng hoặc Tag để gợi ý "Nên chuyển cho phòng Pháp lý/Kế toán".
### 3.5. Cải tiến Bulk Actions
- **Mô tả:** Cho phép xử lý hàng loạt (Chuyển phòng, Phân công nhân viên) để tối ưu năng suất cho Manager.
## 4. Các câu hỏi thảo luận sau
1. Quy tắc phân quyền xem chéo giữa các phòng ban cụ thể là gì?
2. Hệ thống sẽ chạy Production trên SQLite hay chuyển sang MySQL/PostgreSQL?
3. Có cần tích hợp Chat trực tuyến (Zalo/Messenger) vào hệ thống không?

View File

@@ -11,9 +11,9 @@ enum ContractStatus: string
public function label(): string public function label(): string
{ {
return match ($this) { return match ($this) {
self::ACTIVE => 'Đang hiệu lực', self::ACTIVE => __('enums.contract_status.active'),
self::EXPIRED => 'Hết hạn', self::EXPIRED => __('enums.contract_status.expired'),
self::LIQUIDATED => 'Đã thanh lý', self::LIQUIDATED => __('enums.contract_status.liquidated'),
}; };
} }

View File

@@ -11,9 +11,9 @@ enum ContractType: string
public function label(): string public function label(): string
{ {
return match ($this) { return match ($this) {
self::SALE => 'HĐ Mua bán', self::SALE => __('enums.contract_type.sale'),
self::LEASE => 'HĐ Thuê', self::LEASE => __('enums.contract_type.lease'),
self::BCC => 'HĐ Hợp tác kinh doanh (BCC)', self::BCC => __('enums.contract_type.bcc'),
}; };
} }

View File

@@ -12,10 +12,10 @@ enum HandoverStatus: string
public function label(): string public function label(): string
{ {
return match ($this) { return match ($this) {
self::NOT_READY => 'Chưa đủ điều kiện', self::NOT_READY => __('enums.handover.not_ready'),
self::READY => 'Đủ điều kiện', self::READY => __('enums.handover.ready'),
self::HANDED_OVER => 'Đã bàn giao', self::HANDED_OVER => __('enums.handover.handed_over'),
self::SCHEDULED => 'Đã lên lịch', self::SCHEDULED => __('enums.handover.scheduled'),
}; };
} }

View File

@@ -11,9 +11,9 @@ enum ServiceType: string
public function label(): string public function label(): string
{ {
return match ($this) { return match ($this) {
self::MANAGEMENT_FEE => 'Phí quản lý', self::MANAGEMENT_FEE => __('enums.service.management_fee'),
self::GRATITUDE => 'Tri ân', self::GRATITUDE => __('enums.service.gratitude'),
self::SELF_BUSINESS => 'Tự doanh', self::SELF_BUSINESS => __('enums.service.self_business'),
}; };
} }

View File

@@ -20,13 +20,18 @@ class ContractResource extends Resource
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedDocumentText; protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedDocumentText;
protected static ?string $pluralLabel = 'Contracts'; protected static ?string $pluralLabel = 'app.resource_contracts';
protected static ?int $navigationSort = 4; protected static ?int $navigationSort = 4;
public static function getPluralLabel(): ?string
{
return __('app.resource_contracts');
}
public static function getNavigationGroup(): ?string public static function getNavigationGroup(): ?string
{ {
return 'Management'; return __('app.nav_management');
} }
public static function form(Schema $schema): Schema public static function form(Schema $schema): Schema

View File

@@ -16,33 +16,37 @@ class ContractForm
{ {
return $schema return $schema
->components([ ->components([
Section::make('Thông tin hợp đồng') Section::make(__('app.contract_info'))
->schema([ ->schema([
Select::make('product_id') Select::make('product_id')
->label(__('app.product'))
->relationship('product', 'name') ->relationship('product', 'name')
->searchable() ->searchable()
->required(), ->required(),
Select::make('customer_id') Select::make('customer_id')
->label(__('app.widget_customer'))
->relationship('customer', 'name') ->relationship('customer', 'name')
->searchable() ->searchable()
->required(), ->required(),
Select::make('type') Select::make('type')
->label(__('app.contract'))
->options(ContractType::options()) ->options(ContractType::options())
->required() ->required()
->native(false), ->native(false),
Select::make('status') Select::make('status')
->label(__('app.status'))
->options(ContractStatus::ACTIVE->options()) ->options(ContractStatus::ACTIVE->options())
->default('active') ->default('active')
->required() ->required()
->native(false), ->native(false),
DatePicker::make('start_date') DatePicker::make('start_date')
->label('Ngày bắt đầu'), ->label(__('app.start_date')),
DatePicker::make('end_date') DatePicker::make('end_date')
->label('Ngày kết thúc'), ->label(__('app.end_date')),
DatePicker::make('printed_at') DatePicker::make('printed_at')
->label('Ngày in HĐ'), ->label(__('app.printed_at')),
TextInput::make('signed_status') TextInput::make('signed_status')
->label('Tình trạng ký') ->label(__('app.signed_status'))
->maxLength(255), ->maxLength(255),
]) ])
->columns(2), ->columns(2),

View File

@@ -46,7 +46,7 @@ class ContractsTable
->toggleable(), ->toggleable(),
TextColumn::make('feedbacks_count') TextColumn::make('feedbacks_count')
->counts('feedbacks') ->counts('feedbacks')
->label('Tickets'), ->label(__('app.tickets')),
TextColumn::make('created_at') TextColumn::make('created_at')
->dateTime() ->dateTime()
->sortable() ->sortable()

View File

@@ -21,13 +21,18 @@ class CustomerResource extends Resource
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedUserGroup; protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedUserGroup;
protected static ?string $pluralLabel = 'Customers'; protected static ?string $pluralLabel = 'app.resource_customers';
protected static ?int $navigationSort = 2; protected static ?int $navigationSort = 2;
public static function getPluralLabel(): ?string
{
return __('app.resource_customers');
}
public static function getNavigationGroup(): ?string public static function getNavigationGroup(): ?string
{ {
return 'Management'; return __('app.nav_management');
} }
public static function form(Schema $schema): Schema public static function form(Schema $schema): Schema

View File

@@ -12,7 +12,7 @@ class FeedbacksRelationManager extends RelationManager
{ {
protected static string $relationship = 'feedbacks'; protected static string $relationship = 'feedbacks';
protected static ?string $title = 'Feedback History'; protected static ?string $title = 'app.feedback_history';
public function table(Table $table): Table public function table(Table $table): Table
{ {
@@ -23,8 +23,8 @@ class FeedbacksRelationManager extends RelationManager
->searchable() ->searchable()
->url(fn ($record) => FeedbackResource::getUrl('edit', ['record' => $record])), ->url(fn ($record) => FeedbackResource::getUrl('edit', ['record' => $record])),
TextColumn::make('customerProduct.product.name') TextColumn::make('customerProduct.product.name')
->label('Product') ->label(__('app.product'))
->placeholder('General'), ->placeholder(__('app.general')),
TextColumn::make('status') TextColumn::make('status')
->badge() ->badge()
->color(fn (string $state): string => match ($state) { ->color(fn (string $state): string => match ($state) {
@@ -37,10 +37,10 @@ class FeedbacksRelationManager extends RelationManager
TextColumn::make('tags.name') TextColumn::make('tags.name')
->badge(), ->badge(),
TextColumn::make('feedbackChannel.name') TextColumn::make('feedbackChannel.name')
->label('Channel'), ->label(__('app.channel')),
TextColumn::make('interactions_count') TextColumn::make('interactions_count')
->counts('interactions') ->counts('interactions')
->label('Interactions'), ->label(__('app.interactions')),
TextColumn::make('created_at') TextColumn::make('created_at')
->dateTime('d/m/Y') ->dateTime('d/m/Y')
->sortable(), ->sortable(),
@@ -48,10 +48,10 @@ class FeedbacksRelationManager extends RelationManager
->filters([ ->filters([
SelectFilter::make('status') SelectFilter::make('status')
->options([ ->options([
'pending' => 'Pending', 'pending' => __('app.status_pending'),
'processing' => 'Processing', 'processing' => __('app.status_processing'),
'resolved' => 'Resolved', 'resolved' => __('app.status_resolved'),
'closed' => 'Closed', 'closed' => __('app.status_closed'),
]), ]),
SelectFilter::make('tags') SelectFilter::make('tags')
->relationship('tags', 'name'), ->relationship('tags', 'name'),

View File

@@ -14,32 +14,37 @@ class CustomerForm
{ {
return $schema return $schema
->components([ ->components([
Section::make('Customer Information') Section::make(__('app.customer_information'))
->schema([ ->schema([
TextInput::make('name') TextInput::make('name')
->label(__('app.name'))
->required() ->required()
->maxLength(255), ->maxLength(255),
TextInput::make('email') TextInput::make('email')
->label(__('app.email'))
->email() ->email()
->maxLength(255), ->maxLength(255),
TextInput::make('phone') TextInput::make('phone')
->label(__('app.phone'))
->tel() ->tel()
->maxLength(255), ->maxLength(255),
Textarea::make('address') Textarea::make('address')
->label(__('app.address'))
->columnSpanFull(), ->columnSpanFull(),
Select::make('status') Select::make('status')
->options([ ->options([
'active' => 'Active', 'active' => __('app.status_active'),
'inactive' => 'Inactive', 'inactive' => __('app.status_inactive'),
]) ])
->default('active') ->default('active')
->required(), ->required(),
]) ])
->columns(2), ->columns(2),
Section::make('Owned Products') Section::make(__('app.owned_products'))
->schema([ ->schema([
Select::make('products') Select::make('products')
->label(__('app.resource_products'))
->relationship('products', 'name') ->relationship('products', 'name')
->multiple() ->multiple()
->preload() ->preload()

View File

@@ -33,10 +33,10 @@ class CustomersTable
}), }),
TextColumn::make('products_count') TextColumn::make('products_count')
->counts('products') ->counts('products')
->label('Products'), ->label(__('app.resource_products')),
TextColumn::make('feedbacks_count') TextColumn::make('feedbacks_count')
->counts('feedbacks') ->counts('feedbacks')
->label('Feedbacks'), ->label(__('app.feedbacks')),
TextColumn::make('created_at') TextColumn::make('created_at')
->dateTime() ->dateTime()
->sortable() ->sortable()
@@ -45,8 +45,8 @@ class CustomersTable
->filters([ ->filters([
SelectFilter::make('status') SelectFilter::make('status')
->options([ ->options([
'active' => 'Active', 'active' => __('app.status_active'),
'inactive' => 'Inactive', 'inactive' => __('app.status_inactive'),
]), ]),
]) ])
->recordActions([ ->recordActions([

View File

@@ -21,13 +21,18 @@ class FeedbackResource extends Resource
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedChatBubbleLeftRight; protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedChatBubbleLeftRight;
protected static ?string $pluralLabel = 'Feedbacks'; protected static ?string $pluralLabel = 'app.resource_feedbacks';
protected static ?int $navigationSort = 1; protected static ?int $navigationSort = 1;
public static function getPluralLabel(): ?string
{
return __('app.resource_feedbacks');
}
public static function getNavigationGroup(): ?string public static function getNavigationGroup(): ?string
{ {
return 'Customer Care'; return __('app.nav_customer_care');
} }
public static function form(Schema $schema): Schema public static function form(Schema $schema): Schema

View File

@@ -29,7 +29,7 @@ class CreateFeedback extends CreateRecord
$feedback->interactions()->create([ $feedback->interactions()->create([
'user_id' => auth()->id(), 'user_id' => auth()->id(),
'type' => 'note', 'type' => 'note',
'content' => 'Feedback created via ' . ($feedback->feedbackChannel?->name ?? 'unknown channel'), 'content' => __('feedback.created_via', ['channel' => $feedback->feedbackChannel?->name ?? __('feedback.unknown_channel')]),
'changes' => ['status' => ['old' => null, 'new' => $feedback->status]], 'changes' => ['status' => ['old' => null, 'new' => $feedback->status]],
]); ]);

View File

@@ -33,18 +33,18 @@ class EditFeedback extends EditRecord
protected function transferDepartmentAction(): Action protected function transferDepartmentAction(): Action
{ {
return Action::make('transferDepartment') return Action::make('transferDepartment')
->label('Transfer Department') ->label(__('feedback.transfer_department'))
->icon('heroicon-o-arrows-right-left') ->icon('heroicon-o-arrows-right-left')
->color('warning') ->color('warning')
->visible(fn (): bool => auth()->user()->hasPermissionTo('transfer-department')) ->visible(fn (): bool => auth()->user()->hasPermissionTo('transfer-department'))
->form([ ->form([
Select::make('to_department_id') Select::make('to_department_id')
->label('Target Department') ->label(__('feedback.target_department'))
->options(Department::pluck('name', 'id')->toArray()) ->options(Department::pluck('name', 'id')->toArray())
->required() ->required()
->searchable(), ->searchable(),
Select::make('new_assignee_id') Select::make('new_assignee_id')
->label('Assign To (optional)') ->label(__('feedback.assign_to_optional'))
->options(function (\Filament\Forms\Get $get) { ->options(function (\Filament\Forms\Get $get) {
$deptId = $get('to_department_id'); $deptId = $get('to_department_id');
if (! $deptId) { if (! $deptId) {
@@ -57,11 +57,11 @@ class EditFeedback extends EditRecord
->searchable() ->searchable()
->nullable(), ->nullable(),
Textarea::make('reason') Textarea::make('reason')
->label('Reason') ->label(__('feedback.reason'))
->required() ->required()
->rows(3), ->rows(3),
FileUpload::make('transfer_attachments') FileUpload::make('transfer_attachments')
->label('Attachments') ->label(__('app.attachments'))
->multiple() ->multiple()
->disk('local') ->disk('local')
->directory('feedback-attachments') ->directory('feedback-attachments')
@@ -92,7 +92,7 @@ class EditFeedback extends EditRecord
} }
Notification::make() Notification::make()
->title('Transferred successfully') ->title(__('feedback.transferred_successfully'))
->success() ->success()
->send(); ->send();
@@ -103,14 +103,14 @@ class EditFeedback extends EditRecord
protected function closeTicketAction(): Action protected function closeTicketAction(): Action
{ {
return Action::make('closeTicket') return Action::make('closeTicket')
->label('Close Ticket') ->label(__('feedback.close_ticket'))
->icon('heroicon-o-check-circle') ->icon('heroicon-o-check-circle')
->color('success') ->color('success')
->visible(fn (): bool => $this->getRecord()->status !== 'closed' && auth()->user()->hasPermissionTo('close-ticket')) ->visible(fn (): bool => $this->getRecord()->status !== 'closed' && auth()->user()->hasPermissionTo('close-ticket'))
->requiresConfirmation() ->requiresConfirmation()
->modalHeading('Close Ticket') ->modalHeading(__('feedback.close_ticket'))
->modalDescription('Bạn có chắc muốn đóng phiếu này? Hành động này KHÔNG thể hoàn tác.') ->modalDescription(__('feedback.close_confirm'))
->modalSubmitActionLabel('Close') ->modalSubmitActionLabel(__('feedback.close_submit'))
->action(function (): void { ->action(function (): void {
$feedback = $this->getRecord(); $feedback = $this->getRecord();
$actor = auth()->user(); $actor = auth()->user();
@@ -121,7 +121,7 @@ class EditFeedback extends EditRecord
$closingService->close($feedback, $actor); $closingService->close($feedback, $actor);
Notification::make() Notification::make()
->title('Ticket closed successfully') ->title(__('feedback.closed_successfully'))
->success() ->success()
->send(); ->send();

View File

@@ -27,7 +27,7 @@ class InteractionsRelationManager extends RelationManager
{ {
protected static string $relationship = 'interactions'; protected static string $relationship = 'interactions';
protected static ?string $title = 'Interaction History'; protected static ?string $title = 'feedback.interaction_history';
public function form(Schema $schema): Schema public function form(Schema $schema): Schema
{ {
@@ -35,9 +35,9 @@ class InteractionsRelationManager extends RelationManager
->components([ ->components([
Select::make('type') Select::make('type')
->options([ ->options([
'note' => 'Note', 'note' => __('feedback.interaction_type_note'),
'status_change' => 'Status Change', 'status_change' => __('feedback.interaction_type_status_change'),
'assignment' => 'Assignment / Forward', 'assignment' => __('feedback.interaction_type_assignment'),
]) ])
->default('note') ->default('note')
->required() ->required()
@@ -52,28 +52,28 @@ class InteractionsRelationManager extends RelationManager
->columnSpanFull(), ->columnSpanFull(),
Select::make('new_status') Select::make('new_status')
->label('New Status') ->label(__('app.new_status'))
->options(fn (): array => auth()->user()->hasPermissionTo('close-ticket') ->options(fn (): array => auth()->user()->hasPermissionTo('close-ticket')
? ['pending' => 'Pending', 'processing' => 'Processing', 'resolved' => 'Resolved', 'closed' => 'Closed'] ? ['pending' => __('app.status_pending'), 'processing' => __('app.status_processing'), 'resolved' => __('app.status_resolved'), 'closed' => __('app.status_closed')]
: ['pending' => 'Pending', 'processing' => 'Processing', 'resolved' => 'Resolved'], : ['pending' => __('app.status_pending'), 'processing' => __('app.status_processing'), 'resolved' => __('app.status_resolved')],
) )
->visible(fn ($get): bool => $get('type') === 'status_change'), ->visible(fn ($get): bool => $get('type') === 'status_change'),
Select::make('new_assignee') Select::make('new_assignee')
->label('Assign To') ->label(__('app.assign_to'))
->options(User::pluck('name', 'id')->toArray()) ->options(User::pluck('name', 'id')->toArray())
->searchable() ->searchable()
->visible(fn ($get): bool => $get('type') === 'assignment'), ->visible(fn ($get): bool => $get('type') === 'assignment'),
Select::make('department_id') Select::make('department_id')
->label('Target Department') ->label(__('app.target_department'))
->options(Department::pluck('name', 'id')->toArray()) ->options(Department::pluck('name', 'id')->toArray())
->searchable() ->searchable()
->nullable() ->nullable()
->visible(fn ($get): bool => $get('type') === 'assignment'), ->visible(fn ($get): bool => $get('type') === 'assignment'),
FileUpload::make('attachments') FileUpload::make('attachments')
->label('Attachments') ->label(__('app.attachments'))
->multiple() ->multiple()
->disk('local') ->disk('local')
->directory('feedback-attachments') ->directory('feedback-attachments')
@@ -90,13 +90,13 @@ class InteractionsRelationManager extends RelationManager
->dateTime('d/m/Y H:i') ->dateTime('d/m/Y H:i')
->sortable(), ->sortable(),
TextColumn::make('user.name') TextColumn::make('user.name')
->label('By'), ->label(__('app.by')),
TextColumn::make('type') TextColumn::make('type')
->badge() ->badge()
->formatStateUsing(fn (string $state): string => match ($state) { ->formatStateUsing(fn (string $state): string => match ($state) {
'note' => 'Note', 'note' => __('feedback.interaction_type_note'),
'status_change' => 'Status Change', 'status_change' => __('feedback.interaction_type_status_change'),
'assignment' => 'Assignment', 'assignment' => __('feedback.interaction_type_assignment'),
default => $state, default => $state,
}) })
->color(fn (string $state): string => match ($state) { ->color(fn (string $state): string => match ($state) {
@@ -110,7 +110,7 @@ class InteractionsRelationManager extends RelationManager
->limit(80) ->limit(80)
->wrap(), ->wrap(),
TextColumn::make('changes') TextColumn::make('changes')
->label('Details') ->label(__('app.details'))
->formatStateUsing(function (?array $state): string { ->formatStateUsing(function (?array $state): string {
if (! $state) return ''; if (! $state) return '';
$lines = []; $lines = [];
@@ -123,7 +123,7 @@ class InteractionsRelationManager extends RelationManager
}) })
->html(), ->html(),
TextColumn::make('attachments_count') TextColumn::make('attachments_count')
->label('Files') ->label(__('app.files'))
->counts('attachments') ->counts('attachments')
->badge() ->badge()
->color(fn (int $state): string => $state > 0 ? 'primary' : 'gray'), ->color(fn (int $state): string => $state > 0 ? 'primary' : 'gray'),
@@ -131,9 +131,9 @@ class InteractionsRelationManager extends RelationManager
->defaultSort('created_at', 'desc') ->defaultSort('created_at', 'desc')
->recordActions([ ->recordActions([
Action::make('viewAttachments') Action::make('viewAttachments')
->label('View Attachments') ->label(__('feedback.view_attachments'))
->icon('heroicon-o-paper-clip') ->icon('heroicon-o-paper-clip')
->modalHeading('Attachments') ->modalHeading(__('app.attachments'))
->modalContent(function (FeedbackInteraction $record): \Illuminate\Contracts\View\View { ->modalContent(function (FeedbackInteraction $record): \Illuminate\Contracts\View\View {
$fileService = App::make(FileService::class); $fileService = App::make(FileService::class);
$attachments = $record->attachments; $attachments = $record->attachments;
@@ -155,7 +155,7 @@ class InteractionsRelationManager extends RelationManager
]) ])
->headerActions([ ->headerActions([
CreateAction::make() CreateAction::make()
->label('Add Interaction') ->label(__('feedback.add_interaction'))
->icon('heroicon-o-plus') ->icon('heroicon-o-plus')
->mutateFormDataUsing(function (array $data, $livewire): array { ->mutateFormDataUsing(function (array $data, $livewire): array {
$data['user_id'] = auth()->id(); $data['user_id'] = auth()->id();
@@ -177,8 +177,8 @@ class InteractionsRelationManager extends RelationManager
if ($data['type'] === 'assignment' && isset($data['new_assignee'])) { if ($data['type'] === 'assignment' && isset($data['new_assignee'])) {
$feedback = $livewire->getOwnerRecord(); $feedback = $livewire->getOwnerRecord();
$oldAssignee = $feedback->assignedTo?->name ?? 'Unassigned'; $oldAssignee = $feedback->assignedTo?->name ?? __('app.unassigned');
$newAssignee = User::find($data['new_assignee'])?->name ?? 'Unknown'; $newAssignee = User::find($data['new_assignee'])?->name ?? __('app.widget_unknown');
$data['changes'] = [ $data['changes'] = [
'assignment' => ['old' => $oldAssignee, 'new' => $newAssignee], 'assignment' => ['old' => $oldAssignee, 'new' => $newAssignee],
]; ];
@@ -221,13 +221,13 @@ class InteractionsRelationManager extends RelationManager
if ($record->type === 'assignment') { if ($record->type === 'assignment') {
Notification::make() Notification::make()
->title('Feedback assigned successfully') ->title(__('feedback.assigned_successfully'))
->success() ->success()
->send(); ->send();
} }
if ($record->type === 'status_change') { if ($record->type === 'status_change') {
Notification::make() Notification::make()
->title('Status updated successfully') ->title(__('feedback.status_updated'))
->success() ->success()
->send(); ->send();
} }

View File

@@ -20,8 +20,8 @@ class FeedbackForm
{ {
return $schema return $schema
->components([ ->components([
Section::make('Customer & Product') Section::make(__('app.customer_product'))
->description('Select the customer and their associated product') ->description(__('app.select_customer_product'))
->icon('heroicon-o-user-group') ->icon('heroicon-o-user-group')
->schema([ ->schema([
Select::make('customer_id') Select::make('customer_id')
@@ -32,13 +32,13 @@ class FeedbackForm
->columnSpan(1), ->columnSpan(1),
Toggle::make('is_general') Toggle::make('is_general')
->label('General feedback (not related to a product)') ->label(__('app.general_feedback'))
->default(false) ->default(false)
->live() ->live()
->columnSpan(1), ->columnSpan(1),
Select::make('customer_product_id') Select::make('customer_product_id')
->label('Product') ->label(__('app.product'))
->searchable() ->searchable()
->getSearchResultsUsing(function (string $search, $get): array { ->getSearchResultsUsing(function (string $search, $get): array {
$customerId = $get('customer_id'); $customerId = $get('customer_id');
@@ -62,7 +62,7 @@ class FeedbackForm
->live(), ->live(),
Select::make('contract_id') Select::make('contract_id')
->label('Contract') ->label(__('app.contract'))
->visible(fn ($get): bool => ! $get('is_general') && filled($get('customer_product_id'))) ->visible(fn ($get): bool => ! $get('is_general') && filled($get('customer_product_id')))
->options(function ($get): array { ->options(function ($get): array {
$customerProductId = $get('customer_product_id'); $customerProductId = $get('customer_product_id');
@@ -110,8 +110,8 @@ class FeedbackForm
]) ])
->columns(2), ->columns(2),
Section::make('Feedback Details') Section::make(__('app.feedback_details'))
->description('Describe the customer feedback') ->description(__('app.describe_feedback'))
->icon('heroicon-o-chat-bubble-left-right') ->icon('heroicon-o-chat-bubble-left-right')
->schema([ ->schema([
Select::make('feedback_channel_id') Select::make('feedback_channel_id')
@@ -143,16 +143,16 @@ class FeedbackForm
]) ])
->columns(2), ->columns(2),
Section::make('Assignment & Status') Section::make(__('app.assignment_status'))
->description('Assign handler and set initial status') ->description(__('app.assign_handler'))
->icon('heroicon-o-arrow-path-rounded-square') ->icon('heroicon-o-arrow-path-rounded-square')
->schema([ ->schema([
Select::make('status') Select::make('status')
->options([ ->options([
'pending' => 'Pending', 'pending' => __('app.status_pending'),
'processing' => 'Processing', 'processing' => __('app.status_processing'),
'resolved' => 'Resolved', 'resolved' => __('app.status_resolved'),
'closed' => 'Closed', 'closed' => __('app.status_closed'),
]) ])
->default('pending') ->default('pending')
->required() ->required()
@@ -165,35 +165,35 @@ class FeedbackForm
->columnSpan(1), ->columnSpan(1),
Select::make('current_department_id') Select::make('current_department_id')
->label('Department') ->label(__('app.department'))
->options(Department::pluck('name', 'id')->toArray()) ->options(Department::pluck('name', 'id')->toArray())
->searchable() ->searchable()
->nullable() ->nullable()
->columnSpan(1), ->columnSpan(1),
Toggle::make('is_escalated') Toggle::make('is_escalated')
->label('Escalated') ->label(__('app.escalated'))
->visible(fn (): bool => auth()->user()?->hasPermissionTo('close-ticket') ?? false) ->visible(fn (): bool => auth()->user()?->hasPermissionTo('close-ticket') ?? false)
->columnSpan(1), ->columnSpan(1),
]) ])
->columns(2), ->columns(2),
Section::make('Attachments') Section::make(__('app.attachments'))
->description('Upload files related to this feedback') ->description(__('app.upload_files'))
->icon('heroicon-o-paper-clip') ->icon('heroicon-o-paper-clip')
->schema([ ->schema([
Select::make('attachment_collection') Select::make('attachment_collection')
->label('Collection') ->label(__('app.collection'))
->options([ ->options([
'general' => 'General', 'general' => __('app.collection_general'),
'proof_of_resolution' => 'Proof of Resolution', 'proof_of_resolution' => __('app.collection_proof_of_resolution'),
'customer_evidence' => 'Customer Evidence', 'customer_evidence' => __('app.collection_customer_evidence'),
]) ])
->default('general') ->default('general')
->columnSpan(1), ->columnSpan(1),
FileUpload::make('attachments') FileUpload::make('attachments')
->label('Files') ->label(__('app.files'))
->multiple() ->multiple()
->disk('local') ->disk('local')
->directory('feedback-attachments') ->directory('feedback-attachments')

View File

@@ -33,13 +33,13 @@ class FeedbackTable
->sortable(), ->sortable(),
TextColumn::make('customerProduct.product.name') TextColumn::make('customerProduct.product.name')
->label('Product') ->label(__('app.product'))
->placeholder('General') ->placeholder(__('app.general'))
->sortable() ->sortable()
->toggleable(), ->toggleable(),
TextColumn::make('contract.type') TextColumn::make('contract.type')
->label('Contract') ->label(__('app.contract'))
->badge() ->badge()
->formatStateUsing(fn (?string $state): string => $state ? \App\Enums\ContractType::from($state)->label() : '-') ->formatStateUsing(fn (?string $state): string => $state ? \App\Enums\ContractType::from($state)->label() : '-')
->color(fn (?string $state): string => match ($state) { ->color(fn (?string $state): string => match ($state) {
@@ -52,7 +52,7 @@ class FeedbackTable
->toggleable(), ->toggleable(),
TextColumn::make('feedbackChannel.name') TextColumn::make('feedbackChannel.name')
->label('Channel') ->label(__('app.channel'))
->badge() ->badge()
->color('gray'), ->color('gray'),
@@ -72,22 +72,22 @@ class FeedbackTable
}), }),
TextColumn::make('currentDepartment.name') TextColumn::make('currentDepartment.name')
->label('Department') ->label(__('app.department'))
->badge() ->badge()
->color('info') ->color('info')
->toggleable(), ->toggleable(),
TextColumn::make('assignedTo.name') TextColumn::make('assignedTo.name')
->label('Assigned To') ->label(__('app.assigned_to'))
->toggleable(), ->toggleable(),
IconColumn::make('is_escalated') IconColumn::make('is_escalated')
->label('Escalated') ->label(__('app.escalated'))
->boolean() ->boolean()
->toggleable(), ->toggleable(),
TextColumn::make('created_at') TextColumn::make('created_at')
->label('Created') ->label(__('app.created'))
->since() ->since()
->sortable() ->sortable()
->toggleable() ->toggleable()
@@ -96,20 +96,20 @@ class FeedbackTable
->filters([ ->filters([
SelectFilter::make('status') SelectFilter::make('status')
->options([ ->options([
'pending' => 'Pending', 'pending' => __('app.status_pending'),
'processing' => 'Processing', 'processing' => __('app.status_processing'),
'resolved' => 'Resolved', 'resolved' => __('app.status_resolved'),
'closed' => 'Closed', 'closed' => __('app.status_closed'),
]), ]),
SelectFilter::make('feedback_channel_id') SelectFilter::make('feedback_channel_id')
->relationship('feedbackChannel', 'name') ->relationship('feedbackChannel', 'name')
->label('Channel'), ->label(__('app.channel')),
SelectFilter::make('current_department_id') SelectFilter::make('current_department_id')
->relationship('currentDepartment', 'name') ->relationship('currentDepartment', 'name')
->label('Department'), ->label(__('app.department')),
SelectFilter::make('assigned_to') SelectFilter::make('assigned_to')
->relationship('assignedTo', 'name') ->relationship('assignedTo', 'name')
->label('Assigned To'), ->label(__('app.assigned_to')),
]) ])
->recordActions([ ->recordActions([
EditAction::make(), EditAction::make(),

View File

@@ -20,13 +20,18 @@ class FeedbackChannelResource extends Resource
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedInboxStack; protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedInboxStack;
protected static ?string $pluralLabel = 'Channels'; protected static ?string $pluralLabel = 'app.resource_channels';
protected static ?int $navigationSort = 3; protected static ?int $navigationSort = 3;
public static function getPluralLabel(): ?string
{
return __('app.resource_channels');
}
public static function getNavigationGroup(): ?string public static function getNavigationGroup(): ?string
{ {
return 'Management'; return __('app.nav_management');
} }
public static function form(Schema $schema): Schema public static function form(Schema $schema): Schema

View File

@@ -17,18 +17,22 @@ class FeedbackChannelForm
Section::make() Section::make()
->schema([ ->schema([
TextInput::make('name') TextInput::make('name')
->label(__('app.name'))
->required() ->required()
->maxLength(255), ->maxLength(255),
TextInput::make('slug') TextInput::make('slug')
->label(__('app.slug'))
->required() ->required()
->maxLength(255) ->maxLength(255)
->unique(ignoreRecord: true), ->unique(ignoreRecord: true),
TextInput::make('icon') TextInput::make('icon')
->label(__('app.icon'))
->maxLength(255) ->maxLength(255)
->hint('Heroicon name or emoji'), ->hint(__('app.icon_hint')),
ColorPicker::make('color') ColorPicker::make('color')
->label('Color'), ->label(__('app.color')),
Toggle::make('is_active') Toggle::make('is_active')
->label(__('app.status_active'))
->default(true), ->default(true),
]) ])
->columns(2), ->columns(2),

View File

@@ -27,7 +27,7 @@ class FeedbackChannelsTable
TextColumn::make('slug') TextColumn::make('slug')
->searchable(), ->searchable(),
TextColumn::make('color') TextColumn::make('color')
->label('Color') ->label(__('app.color'))
->formatStateUsing(fn ($state) => sprintf( ->formatStateUsing(fn ($state) => sprintf(
'<span style="display:inline-block;width:24px;height:24px;background:%s;border-radius:6px;border:1px solid #d1d5db;" title="%s"></span>', '<span style="display:inline-block;width:24px;height:24px;background:%s;border-radius:6px;border:1px solid #d1d5db;" title="%s"></span>',
$state ?: '#6b7280', $state ?: '#6b7280',
@@ -36,10 +36,10 @@ class FeedbackChannelsTable
->html(), ->html(),
IconColumn::make('is_active') IconColumn::make('is_active')
->boolean() ->boolean()
->label('Active'), ->label(__('app.status_active')),
TextColumn::make('feedbacks_count') TextColumn::make('feedbacks_count')
->counts('feedbacks') ->counts('feedbacks')
->label('Feedbacks'), ->label(__('app.feedbacks')),
TextColumn::make('created_at') TextColumn::make('created_at')
->dateTime() ->dateTime()
->sortable() ->sortable()

View File

@@ -20,11 +20,14 @@ class FeedbackTagResource extends Resource
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedTag; protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedTag;
protected static ?string $pluralLabel = 'Tags'; public static function getPluralLabel(): ?string
{
return __('app.resource_tags');
}
public static function getNavigationGroup(): ?string public static function getNavigationGroup(): ?string
{ {
return 'Management'; return __('app.nav_management');
} }
public static function form(Schema $schema): Schema public static function form(Schema $schema): Schema

View File

@@ -16,13 +16,15 @@ class FeedbackTagForm
Section::make() Section::make()
->schema([ ->schema([
TextInput::make('name') TextInput::make('name')
->label(__('app.name'))
->required() ->required()
->maxLength(255), ->maxLength(255),
TextInput::make('slug') TextInput::make('slug')
->label(__('app.slug'))
->required() ->required()
->maxLength(255), ->maxLength(255),
ColorPicker::make('color') ColorPicker::make('color')
->label('Color') ->label(__('app.color'))
->default('#3b82f6'), ->default('#3b82f6'),
]) ])
->columns(2), ->columns(2),

View File

@@ -26,7 +26,7 @@ class FeedbackTagsTable
TextColumn::make('slug') TextColumn::make('slug')
->searchable(), ->searchable(),
TextColumn::make('color') TextColumn::make('color')
->label('Color') ->label(__('app.color'))
->formatStateUsing(fn ($state) => sprintf( ->formatStateUsing(fn ($state) => sprintf(
'<span style="display:inline-block;width:24px;height:24px;background:%s;border-radius:6px;border:1px solid #d1d5db;" title="%s"></span>', '<span style="display:inline-block;width:24px;height:24px;background:%s;border-radius:6px;border:1px solid #d1d5db;" title="%s"></span>',
$state ?: '#6b7280', $state ?: '#6b7280',
@@ -35,7 +35,7 @@ class FeedbackTagsTable
->html(), ->html(),
TextColumn::make('feedbacks_count') TextColumn::make('feedbacks_count')
->counts('feedbacks') ->counts('feedbacks')
->label('Used'), ->label(__('app.used')),
TextColumn::make('created_at') TextColumn::make('created_at')
->dateTime() ->dateTime()
->sortable() ->sortable()

View File

@@ -23,13 +23,18 @@ class ProductResource extends Resource
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedBuildingStorefront; protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedBuildingStorefront;
protected static ?string $pluralLabel = 'Products'; protected static ?string $pluralLabel = 'app.resource_products';
protected static ?int $navigationSort = 1; protected static ?int $navigationSort = 1;
public static function getPluralLabel(): ?string
{
return __('app.resource_products');
}
public static function getNavigationGroup(): ?string public static function getNavigationGroup(): ?string
{ {
return 'Management'; return __('app.nav_management');
} }
public static function form(Schema $schema): Schema public static function form(Schema $schema): Schema

View File

@@ -15,7 +15,7 @@ class ContractsRelationManager extends RelationManager
{ {
protected static string $relationship = 'contracts'; protected static string $relationship = 'contracts';
protected static ?string $title = 'Contracts'; protected static ?string $title = 'app.contracts';
public function table(Table $table): Table public function table(Table $table): Table
{ {
@@ -26,7 +26,7 @@ class ContractsRelationManager extends RelationManager
->badge() ->badge()
->formatStateUsing(fn (string $state): string => ContractType::from($state)->label()), ->formatStateUsing(fn (string $state): string => ContractType::from($state)->label()),
TextColumn::make('customer.name') TextColumn::make('customer.name')
->label('Customer') ->label(__('app.widget_customer'))
->searchable(), ->searchable(),
TextColumn::make('status') TextColumn::make('status')
->badge() ->badge()
@@ -44,10 +44,10 @@ class ContractsRelationManager extends RelationManager
->date('d/m/Y') ->date('d/m/Y')
->sortable(), ->sortable(),
TextColumn::make('signed_status') TextColumn::make('signed_status')
->label('Signed'), ->label(__('app.signed')),
TextColumn::make('feedbacks_count') TextColumn::make('feedbacks_count')
->counts('feedbacks') ->counts('feedbacks')
->label('Tickets'), ->label(__('app.tickets')),
]) ])
->filters([ ->filters([
SelectFilter::make('type') SelectFilter::make('type')

View File

@@ -12,7 +12,7 @@ class HandoversRelationManager extends RelationManager
{ {
protected static string $relationship = 'handovers'; protected static string $relationship = 'handovers';
protected static ?string $title = 'Handovers'; protected static ?string $title = 'app.handovers';
public function table(Table $table): Table public function table(Table $table): Table
{ {
@@ -20,11 +20,11 @@ class HandoversRelationManager extends RelationManager
->recordTitleAttribute('status') ->recordTitleAttribute('status')
->columns([ ->columns([
TextColumn::make('customer.name') TextColumn::make('customer.name')
->label('Customer') ->label(__('app.widget_customer'))
->searchable() ->searchable()
->sortable(), ->sortable(),
TextColumn::make('handover_date') TextColumn::make('handover_date')
->label('Ngày BG') ->label(__('app.handover_date'))
->date('d/m/Y') ->date('d/m/Y')
->sortable(), ->sortable(),
TextColumn::make('status') TextColumn::make('status')
@@ -38,7 +38,7 @@ class HandoversRelationManager extends RelationManager
}) })
->formatStateUsing(fn (string $state): string => HandoverStatus::from($state)->label()), ->formatStateUsing(fn (string $state): string => HandoverStatus::from($state)->label()),
TextColumn::make('remark') TextColumn::make('remark')
->label('Ghi chú') ->label(__('app.remark'))
->limit(50) ->limit(50)
->toggleable(), ->toggleable(),
TextColumn::make('created_at') TextColumn::make('created_at')

View File

@@ -12,7 +12,7 @@ class ProductServicesRelationManager extends RelationManager
{ {
protected static string $relationship = 'productServices'; protected static string $relationship = 'productServices';
protected static ?string $title = 'Services'; protected static ?string $title = 'app.services';
public function table(Table $table): Table public function table(Table $table): Table
{ {
@@ -38,11 +38,11 @@ class ProductServicesRelationManager extends RelationManager
default => 'gray', default => 'gray',
}), }),
TextColumn::make('actual_collection_date') TextColumn::make('actual_collection_date')
->label('Ngày thu') ->label(__('app.collection_date'))
->date('d/m/Y') ->date('d/m/Y')
->sortable(), ->sortable(),
TextColumn::make('remark') TextColumn::make('remark')
->label('Ghi chú') ->label(__('app.remark'))
->limit(50) ->limit(50)
->toggleable(), ->toggleable(),
TextColumn::make('created_at') TextColumn::make('created_at')
@@ -55,10 +55,10 @@ class ProductServicesRelationManager extends RelationManager
->options(ServiceType::MANAGEMENT_FEE->options()), ->options(ServiceType::MANAGEMENT_FEE->options()),
SelectFilter::make('status') SelectFilter::make('status')
->options([ ->options([
'active' => 'Active', 'active' => __('app.status_active'),
'pending' => 'Pending', 'pending' => __('app.status_pending'),
'completed' => 'Completed', 'completed' => __('app.status_completed'),
'cancelled' => 'Cancelled', 'cancelled' => __('app.status_cancelled'),
]), ]),
]) ])
->defaultSort('created_at', 'desc') ->defaultSort('created_at', 'desc')

View File

@@ -17,15 +17,17 @@ class ProductForm
Section::make() Section::make()
->schema([ ->schema([
TextInput::make('name') TextInput::make('name')
->label(__('app.name'))
->required() ->required()
->maxLength(255), ->maxLength(255),
Textarea::make('description') Textarea::make('description')
->label(__('app.description'))
->columnSpanFull(), ->columnSpanFull(),
Select::make('status') Select::make('status')
->options([ ->options([
'active' => 'Active', 'active' => __('app.status_active'),
'inactive' => 'Inactive', 'inactive' => __('app.status_inactive'),
'sold_out' => 'Sold Out', 'sold_out' => __('app.status_sold_out'),
]) ])
->default('active') ->default('active')
->required(), ->required(),

View File

@@ -31,7 +31,7 @@ class ProductsTable
->toggleable(isToggledHiddenByDefault: true), ->toggleable(isToggledHiddenByDefault: true),
TextColumn::make('customers_count') TextColumn::make('customers_count')
->counts('customers') ->counts('customers')
->label('Owners'), ->label(__('app.owners')),
TextColumn::make('created_at') TextColumn::make('created_at')
->dateTime() ->dateTime()
->sortable() ->sortable()
@@ -40,9 +40,9 @@ class ProductsTable
->filters([ ->filters([
SelectFilter::make('status') SelectFilter::make('status')
->options([ ->options([
'active' => 'Active', 'active' => __('app.status_active'),
'inactive' => 'Inactive', 'inactive' => __('app.status_inactive'),
'sold_out' => 'Sold Out', 'sold_out' => __('app.status_sold_out'),
]), ]),
]) ])
->recordActions([ ->recordActions([

View File

@@ -20,15 +20,20 @@ class RoleResource extends Resource
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedShieldCheck; protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedShieldCheck;
protected static ?string $pluralLabel = 'Roles'; protected static ?string $pluralLabel = 'app.resource_roles';
protected static ?int $navigationSort = 6; protected static ?int $navigationSort = 6;
protected static ?string $recordTitleAttribute = 'name'; protected static ?string $recordTitleAttribute = 'name';
public static function getPluralLabel(): ?string
{
return __('app.resource_roles');
}
public static function getNavigationGroup(): ?string public static function getNavigationGroup(): ?string
{ {
return 'Management'; return __('app.nav_management');
} }
public static function form(Schema $schema): Schema public static function form(Schema $schema): Schema

View File

@@ -17,14 +17,14 @@ class RoleForm
Section::make() Section::make()
->schema([ ->schema([
TextInput::make('name') TextInput::make('name')
->label('Role Name') ->label(__('app.role_name'))
->required() ->required()
->maxLength(255) ->maxLength(255)
->unique(ignoreRecord: true), ->unique(ignoreRecord: true),
]), ]),
Section::make('Permissions') Section::make(__('app.permissions'))
->description('Chọn quyền cho role này') ->description(__('app.permissions'))
->schema([ ->schema([
CheckboxList::make('permissions') CheckboxList::make('permissions')
->label('') ->label('')

View File

@@ -19,7 +19,7 @@ class RolesTable
->sortable(), ->sortable(),
TextColumn::make('name') TextColumn::make('name')
->label('Role Name') ->label(__('app.role_name'))
->searchable() ->searchable()
->sortable() ->sortable()
->badge() ->badge()
@@ -31,13 +31,13 @@ class RolesTable
}), }),
TextColumn::make('permissions_count') TextColumn::make('permissions_count')
->label('Permissions') ->label(__('app.permissions'))
->counts('permissions') ->counts('permissions')
->badge() ->badge()
->color('primary'), ->color('primary'),
TextColumn::make('users_count') TextColumn::make('users_count')
->label('Users') ->label(__('app.resource_users'))
->counts('users') ->counts('users')
->badge() ->badge()
->color('success'), ->color('success'),

View File

@@ -22,14 +22,18 @@ class ListUsers extends ListRecords
public function getTabs(): array public function getTabs(): array
{ {
return [ return [
'all' => Tab::make(), 'all' => Tab::make()
->label(__('app.tab_all')),
'admin' => Tab::make() 'admin' => Tab::make()
->label('Admin')
->modifyQueryUsing(fn (Builder $query) => $query->where('role', 'admin')) ->modifyQueryUsing(fn (Builder $query) => $query->where('role', 'admin'))
->badge(fn () => static::getResource()::getModel()::where('role', 'admin')->count()), ->badge(fn () => static::getResource()::getModel()::where('role', 'admin')->count()),
'manager' => Tab::make() 'manager' => Tab::make()
->label('Manager')
->modifyQueryUsing(fn (Builder $query) => $query->where('role', 'manager')) ->modifyQueryUsing(fn (Builder $query) => $query->where('role', 'manager'))
->badge(fn () => static::getResource()::getModel()::where('role', 'manager')->count()), ->badge(fn () => static::getResource()::getModel()::where('role', 'manager')->count()),
'staff' => Tab::make() 'staff' => Tab::make()
->label('Staff')
->modifyQueryUsing(fn (Builder $query) => $query->where('role', 'staff')) ->modifyQueryUsing(fn (Builder $query) => $query->where('role', 'staff'))
->badge(fn () => static::getResource()::getModel()::where('role', 'staff')->count()), ->badge(fn () => static::getResource()::getModel()::where('role', 'staff')->count()),
]; ];

View File

@@ -14,16 +14,19 @@ class UserForm
return $schema return $schema
->components([ ->components([
TextInput::make('name') TextInput::make('name')
->label(__('app.name'))
->required() ->required()
->maxLength(255), ->maxLength(255),
TextInput::make('email') TextInput::make('email')
->label(__('app.email'))
->email() ->email()
->required() ->required()
->maxLength(255) ->maxLength(255)
->unique(ignoreRecord: true), ->unique(ignoreRecord: true),
TextInput::make('password') TextInput::make('password')
->label(__('app.password'))
->password() ->password()
->revealable() ->revealable()
->required(fn (string $operation): bool => $operation === 'create') ->required(fn (string $operation): bool => $operation === 'create')
@@ -33,7 +36,7 @@ class UserForm
->columnSpanFull(), ->columnSpanFull(),
Select::make('role') Select::make('role')
->label('Role') ->label(__('app.role'))
->options(Role::pluck('name', 'name')->toArray()) ->options(Role::pluck('name', 'name')->toArray())
->required() ->required()
->searchable(), ->searchable(),

View File

@@ -20,15 +20,20 @@ class UserResource extends Resource
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedUsers; protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedUsers;
protected static ?string $pluralLabel = 'Users'; protected static ?string $pluralLabel = 'app.resource_users';
protected static ?int $navigationSort = 5; protected static ?int $navigationSort = 5;
protected static ?string $recordTitleAttribute = 'name'; protected static ?string $recordTitleAttribute = 'name';
public static function getPluralLabel(): ?string
{
return __('app.resource_users');
}
public static function getNavigationGroup(): ?string public static function getNavigationGroup(): ?string
{ {
return 'Management'; return __('app.nav_management');
} }
public static function form(Schema $schema): Schema public static function form(Schema $schema): Schema

View File

@@ -35,29 +35,29 @@ class AvgProcessingTimeWidget extends BaseWidget
$avgDisplay = $avgMinutes ? round($avgMinutes) . ' min' : 'N/A'; $avgDisplay = $avgMinutes ? round($avgMinutes) . ' min' : 'N/A';
$avgDescription = $avgMinutes $avgDescription = $avgMinutes
? 'Avg time from creation to resolution' ? __('app.widget_avg_time_desc')
: 'No resolved tickets yet'; : __('app.widget_no_resolved_tickets');
return [ return [
Stat::make('Resolved Tickets', (string) $totalResolved) Stat::make(__('app.widget_resolved_tickets'), (string) $totalResolved)
->description('Total resolved/closed tickets') ->description(__('app.widget_total_resolved_closed'))
->descriptionIcon('heroicon-m-check-circle') ->descriptionIcon('heroicon-m-check-circle')
->color('success') ->color('success')
->chart([7, 3, 4, 5, 6, 8, $totalResolved]), ->chart([7, 3, 4, 5, 6, 8, $totalResolved]),
Stat::make('Avg Processing Time', $avgDisplay) Stat::make(__('app.widget_avg_processing_time'), $avgDisplay)
->description($avgDescription) ->description($avgDescription)
->descriptionIcon('heroicon-m-clock') ->descriptionIcon('heroicon-m-clock')
->color('info'), ->color('info'),
Stat::make('Pending Tickets', (string) $totalPending) Stat::make(__('app.widget_pending_tickets'), (string) $totalPending)
->description('Still in progress') ->description(__('app.widget_still_in_progress'))
->descriptionIcon('heroicon-m-arrow-path') ->descriptionIcon('heroicon-m-arrow-path')
->color('warning') ->color('warning')
->chart([3, 5, 4, 6, 5, 4, $totalPending]), ->chart([3, 5, 4, 6, 5, 4, $totalPending]),
Stat::make('Escalated', (string) $escalatedCount) Stat::make(__('app.widget_escalated'), (string) $escalatedCount)
->description('Tickets marked as escalated') ->description(__('app.widget_escalated_desc'))
->descriptionIcon('heroicon-m-exclamation-triangle') ->descriptionIcon('heroicon-m-exclamation-triangle')
->color('danger'), ->color('danger'),
]; ];

View File

@@ -5,15 +5,22 @@ namespace App\Filament\Widgets;
use App\Models\Department; use App\Models\Department;
use App\Models\Feedback; use App\Models\Feedback;
use Filament\Widgets\ChartWidget; use Filament\Widgets\ChartWidget;
use Illuminate\Contracts\Support\Htmlable;
class HandlerPerformanceWidget extends ChartWidget class HandlerPerformanceWidget extends ChartWidget
{ {
protected ?string $heading = 'Avg Processing Time by Handler';
protected ?string $description = 'Average time from ticket creation to resolution, broken down by handler.';
protected int|string|array $columnSpan = 'full'; protected int|string|array $columnSpan = 'full';
public function getHeading(): string | Htmlable | null
{
return __('app.widget_avg_time_by_handler');
}
public function getDescription(): string | Htmlable | null
{
return __('app.widget_avg_time_by_handler_desc');
}
protected function getType(): string protected function getType(): string
{ {
return 'bar'; return 'bar';
@@ -39,19 +46,18 @@ class HandlerPerformanceWidget extends ChartWidget
->with('assignedTo') ->with('assignedTo')
->get(); ->get();
$labels = $handlers->map(fn ($h) => $h->assignedTo?->name ?? 'Unknown')->toArray(); $labels = $handlers->map(fn ($h) => $h->assignedTo?->name ?? __('app.widget_unknown'))->toArray();
$data = $handlers->map(fn ($h) => round($h->avg_minutes, 1))->toArray(); $data = $handlers->map(fn ($h) => round($h->avg_minutes, 1))->toArray();
// Generate gradient colors based on performance
$maxMinutes = max($data) ?: 1; $maxMinutes = max($data) ?: 1;
$colors = collect($data)->map(function ($minutes) use ($maxMinutes) { $colors = collect($data)->map(function ($minutes) use ($maxMinutes) {
$ratio = $minutes / $maxMinutes; $ratio = $minutes / $maxMinutes;
if ($ratio > 0.8) { if ($ratio > 0.8) {
return 'rgba(220, 38, 38, 0.8)'; // Red for slow return 'rgba(220, 38, 38, 0.8)';
} elseif ($ratio > 0.5) { } elseif ($ratio > 0.5) {
return 'rgba(217, 119, 6, 0.8)'; // Amber for medium return 'rgba(217, 119, 6, 0.8)';
} }
return 'rgba(26, 86, 219, 0.8)'; // Blue for fast return 'rgba(26, 86, 219, 0.8)';
})->toArray(); })->toArray();
$borderColors = collect($data)->map(function ($minutes) use ($maxMinutes) { $borderColors = collect($data)->map(function ($minutes) use ($maxMinutes) {
@@ -67,7 +73,7 @@ class HandlerPerformanceWidget extends ChartWidget
return [ return [
'datasets' => [ 'datasets' => [
[ [
'label' => 'Avg Time (minutes)', 'label' => __('app.widget_avg_time_minutes'),
'data' => $data, 'data' => $data,
'backgroundColor' => $colors, 'backgroundColor' => $colors,
'borderColor' => $borderColors, 'borderColor' => $borderColors,

View File

@@ -35,37 +35,37 @@ class ResolvedTicketsWidget extends BaseWidget
->color('primary'), ->color('primary'),
TextColumn::make('title') TextColumn::make('title')
->label('Ticket') ->label(__('app.widget_ticket'))
->searchable() ->searchable()
->weight('semibold') ->weight('semibold')
->url(fn (Feedback $record): string => FeedbackResource::getUrl('edit', ['record' => $record])) ->url(fn (Feedback $record): string => FeedbackResource::getUrl('edit', ['record' => $record]))
->color('primary'), ->color('primary'),
TextColumn::make('customer.name') TextColumn::make('customer.name')
->label('Customer') ->label(__('app.widget_customer'))
->searchable(), ->searchable(),
TextColumn::make('customerProduct.product.name') TextColumn::make('customerProduct.product.name')
->label('Product') ->label(__('app.product'))
->placeholder('General'), ->placeholder(__('app.general')),
TextColumn::make('assignedTo.name') TextColumn::make('assignedTo.name')
->label('Handler'), ->label(__('app.widget_handler')),
TextColumn::make('currentDepartment.name') TextColumn::make('currentDepartment.name')
->label('Department') ->label(__('app.department'))
->badge() ->badge()
->color('info'), ->color('info'),
TextColumn::make('updated_at') TextColumn::make('updated_at')
->label('Resolved') ->label(__('app.widget_resolved'))
->since() ->since()
->sortable() ->sortable()
->color('secondary'), ->color('secondary'),
]) ])
->defaultSort('updated_at', 'desc') ->defaultSort('updated_at', 'desc')
->heading('Tickets Awaiting Closure') ->heading(__('app.widget_tickets_awaiting_closure'))
->description('Tickets that have been resolved and are pending manager review for closure.') ->description(__('app.widget_tickets_awaiting_desc'))
->paginated([5]); ->paginated([5]);
} }

View File

@@ -25,16 +25,16 @@ class TicketClosed extends Notification
public function toMail(object $notifiable): MailMessage public function toMail(object $notifiable): MailMessage
{ {
return (new MailMessage) return (new MailMessage)
->subject(__('Ticket #{id} đã được đóng', ['id' => $this->feedback->id])) ->subject(__('feedback.notif_ticket_closed_subject', ['id' => $this->feedback->id]))
->line(__('Ticket: :title', ['title' => $this->feedback->title])) ->line(__('feedback.notif_ticket_closed_ticket', ['title' => $this->feedback->title]))
->line(__('Đóng bởi: :name', ['name' => $this->actor->name])) ->line(__('feedback.notif_ticket_closed_by', ['name' => $this->actor->name]))
->action(__('Xem Ticket'), url('/admin/feedbacks/' . $this->feedback->id . '/edit')); ->action(__('feedback.notif_view_ticket'), url('/admin/feedbacks/' . $this->feedback->id . '/edit'));
} }
public function toDatabase(object $notifiable): array public function toDatabase(object $notifiable): array
{ {
return [ return [
'message' => __('Ticket #{id} ":title" đã được đóng bởi :actor.', [ 'message' => __('feedback.notif_ticket_closed_message', [
'id' => $this->feedback->id, 'id' => $this->feedback->id,
'title' => $this->feedback->title, 'title' => $this->feedback->title,
'actor' => $this->actor->name, 'actor' => $this->actor->name,

View File

@@ -27,17 +27,17 @@ class TicketTransferred extends Notification
public function toMail(object $notifiable): MailMessage public function toMail(object $notifiable): MailMessage
{ {
return (new MailMessage) return (new MailMessage)
->subject(__('Ticket #{id} đã được chuyển đến phòng của bạn', ['id' => $this->feedback->id])) ->subject(__('feedback.notif_ticket_transferred_subject', ['id' => $this->feedback->id]))
->line(__('Ticket: :title', ['title' => $this->feedback->title])) ->line(__('feedback.notif_ticket_closed_ticket', ['title' => $this->feedback->title]))
->line(__('Người chuyển: :name', ['name' => $this->sender->name])) ->line(__('feedback.notif_ticket_transferred_by', ['name' => $this->sender->name]))
->line(__('Lý do: :reason', ['reason' => $this->transferLog->reason])) ->line(__('feedback.notif_ticket_transferred_reason', ['reason' => $this->transferLog->reason]))
->action(__('Xem Ticket'), url('/admin/feedbacks/' . $this->feedback->id . '/edit')); ->action(__('feedback.notif_view_ticket'), url('/admin/feedbacks/' . $this->feedback->id . '/edit'));
} }
public function toDatabase(object $notifiable): array public function toDatabase(object $notifiable): array
{ {
return [ return [
'message' => __('Ticket #{id} ":title" đã được chuyển đến phòng của bạn bởi :sender.', [ 'message' => __('feedback.notif_ticket_transferred_message', [
'id' => $this->feedback->id, 'id' => $this->feedback->id,
'title' => $this->feedback->title, 'title' => $this->feedback->title,
'sender' => $this->sender->name, 'sender' => $this->sender->name,

View File

@@ -56,10 +56,10 @@ class AdminPanelProvider extends PanelProvider
]) ])
->navigationGroups([ ->navigationGroups([
NavigationGroup::make() NavigationGroup::make()
->label('Customer Care') ->label(__('app.nav_customer_care'))
->icon('heroicon-o-chat-bubble-left-right'), ->icon('heroicon-o-chat-bubble-left-right'),
NavigationGroup::make() NavigationGroup::make()
->label('Management') ->label(__('app.nav_management'))
->icon('heroicon-o-cog-6-tooth'), ->icon('heroicon-o-cog-6-tooth'),
]) ])
->middleware([ ->middleware([

View File

@@ -26,7 +26,7 @@ class RequireProofOfResolution implements ValidationRule
$fileService = App::make(FileService::class); $fileService = App::make(FileService::class);
if (! $fileService->hasCollection($this->feedback, 'proof_of_resolution')) { if (! $fileService->hasCollection($this->feedback, 'proof_of_resolution')) {
$fail(__('Yêu cầu cung cấp tài liệu bằng chứng để hoàn tất quy trình.')); $fail(__('feedback.proof_required'));
} }
} }
} }

View File

@@ -11,25 +11,22 @@ use Symfony\Component\HttpKernel\Exception\HttpException;
class ClosingService class ClosingService
{ {
/**
* Close a feedback. Throws if staff tries to close or no proof exists.
*/
public function close(Feedback $feedback, User $actor): void public function close(Feedback $feedback, User $actor): void
{ {
if ($feedback->status === 'closed') { if ($feedback->status === 'closed') {
throw ValidationException::withMessages([ throw ValidationException::withMessages([
'status' => __('Phiếu này đã được đóng.'), 'status' => __('feedback.already_closed'),
]); ]);
} }
if (! $actor->hasPermissionTo('close-ticket')) { if (! $actor->hasPermissionTo('close-ticket')) {
throw new HttpException(403, __('Chỉ lãnh đạo cấp phòng mới có quyền đóng phiếu.')); throw new HttpException(403, __('feedback.close_permission_denied'));
} }
if ($actor->hasRole('manager') && ! $actor->hasRole('admin')) { if ($actor->hasRole('manager') && ! $actor->hasRole('admin')) {
$managedDeptIds = Department::where('manager_id', $actor->id)->pluck('id'); $managedDeptIds = Department::where('manager_id', $actor->id)->pluck('id');
if (! $managedDeptIds->contains($feedback->current_department_id)) { if (! $managedDeptIds->contains($feedback->current_department_id)) {
throw new HttpException(403, __('Bạn chỉ có thể đóng phiếu thuộc phòng ban mình quản lý.')); throw new HttpException(403, __('feedback.close_department_only'));
} }
} }
@@ -37,7 +34,7 @@ class ClosingService
if (! $fileService->hasCollection($feedback, 'proof_of_resolution')) { if (! $fileService->hasCollection($feedback, 'proof_of_resolution')) {
throw ValidationException::withMessages([ throw ValidationException::withMessages([
'status' => __('Yêu cầu cung cấp tài liệu bằng chứng để hoàn tất quy trình.'), 'status' => __('feedback.proof_required'),
]); ]);
} }
@@ -46,14 +43,11 @@ class ClosingService
$this->notifyStakeholders($feedback, $actor); $this->notifyStakeholders($feedback, $actor);
} }
/**
* Resolve a feedback. Any role can resolve.
*/
public function resolve(Feedback $feedback, User $actor): void public function resolve(Feedback $feedback, User $actor): void
{ {
if (in_array($feedback->status, ['closed', 'resolved'])) { if (in_array($feedback->status, ['closed', 'resolved'])) {
throw ValidationException::withMessages([ throw ValidationException::withMessages([
'status' => __('Phiếu này đã được xử lý hoặc đóng.'), 'status' => __('feedback.already_resolved'),
]); ]);
} }

View File

@@ -126,11 +126,11 @@ class FileService
$errors = []; $errors = [];
if (! in_array($file->getMimeType(), $this->allowedMimes)) { if (! in_array($file->getMimeType(), $this->allowedMimes)) {
$errors['file'] = __('File type :type is not allowed. Allowed: jpg, png, pdf, mp4, mov.', ['type' => $file->getMimeType()]); $errors['file'] = __('feedback.file_type_not_allowed', ['type' => $file->getMimeType()]);
} }
if ($file->getSize() > $this->maxSize * 1024) { if ($file->getSize() > $this->maxSize * 1024) {
$errors['file'] = __('File size exceeds :max KB.', ['max' => $this->maxSize]); $errors['file'] = __('feedback.file_size_exceeds', ['max' => $this->maxSize]);
} }
if (! empty($errors)) { if (! empty($errors)) {

View File

@@ -10,9 +10,6 @@ use Illuminate\Validation\ValidationException;
class TransferService class TransferService
{ {
/**
* Transfer a feedback to another department.
*/
public function transfer( public function transfer(
Feedback $feedback, Feedback $feedback,
Department $toDepartment, Department $toDepartment,
@@ -22,7 +19,7 @@ class TransferService
): TicketTransferLog { ): TicketTransferLog {
if (empty(trim($reason))) { if (empty(trim($reason))) {
throw ValidationException::withMessages([ throw ValidationException::withMessages([
'reason' => __('Vui lòng nhập lý do chuyển tiếp.'), 'reason' => __('feedback.transfer_reason_required'),
]); ]);
} }

133
lang/en/app.php Normal file
View File

@@ -0,0 +1,133 @@
<?php
return [
// Navigation
'nav_customer_care' => 'Customer Care',
'nav_management' => 'Management',
// Resource labels
'resource_feedbacks' => 'Feedbacks',
'resource_products' => 'Products',
'resource_customers' => 'Customers',
'resource_contracts' => 'Contracts',
'resource_channels' => 'Channels',
'resource_tags' => 'Tags',
'resource_roles' => 'Roles',
'resource_users' => 'Users',
// Status
'status_pending' => 'Pending',
'status_processing' => 'Processing',
'status_resolved' => 'Resolved',
'status_closed' => 'Closed',
'status_active' => 'Active',
'status_inactive' => 'Inactive',
'status_sold_out' => 'Sold Out',
'status_completed' => 'Completed',
'status_cancelled' => 'Cancelled',
// General labels
'name' => 'Name',
'email' => 'Email',
'password' => 'Password',
'phone' => 'Phone',
'address' => 'Address',
'slug' => 'Slug',
'icon' => 'Icon',
'description' => 'Description',
'status' => 'Status',
'customer_product' => 'Customer & Product',
'select_customer_product' => 'Select the customer and their associated product',
'general_feedback' => 'General feedback (not related to a product)',
'product' => 'Product',
'contract' => 'Contract',
'feedback_details' => 'Feedback Details',
'describe_feedback' => 'Describe the customer feedback',
'assignment_status' => 'Assignment & Status',
'assign_handler' => 'Assign handler and set initial status',
'department' => 'Department',
'escalated' => 'Escalated',
'attachments' => 'Attachments',
'upload_files' => 'Upload files related to this feedback',
'collection' => 'Collection',
'collection_general' => 'General',
'collection_proof_of_resolution' => 'Proof of Resolution',
'collection_customer_evidence' => 'Customer Evidence',
'files' => 'Files',
'channel' => 'Channel',
'assigned_to' => 'Assigned To',
'created' => 'Created',
'general' => 'General',
'new_status' => 'New Status',
'assign_to' => 'Assign To',
'target_department' => 'Target Department',
'by' => 'By',
'details' => 'Details',
'unassigned' => 'Unassigned',
'role' => 'Role',
'role_name' => 'Role Name',
'permissions' => 'Permissions',
'color' => 'Color',
'customer_information' => 'Customer Information',
'owned_products' => 'Owned Products',
'icon_hint' => 'Heroicon name or emoji',
'feedbacks' => 'Feedbacks',
'used' => 'Used',
'owners' => 'Owners',
'tickets' => 'Tickets',
'signed' => 'Signed',
'interactions' => 'Interactions',
'services' => 'Services',
'handovers' => 'Handovers',
'contracts' => 'Contracts',
'collection_date' => 'Collection Date',
'remark' => 'Remark',
'handover_date' => 'Handover Date',
'start_date' => 'Start Date',
'end_date' => 'End Date',
'printed_at' => 'Print Date',
'signed_status' => 'Signed Status',
'contract_info' => 'Contract Information',
'feedback_history' => 'Feedback History',
'tab_all' => 'All',
'min' => 'min',
'na' => 'N/A',
// Widget labels
'widget_tickets_awaiting_closure' => 'Tickets Awaiting Closure',
'widget_tickets_awaiting_desc' => 'Tickets that have been resolved and are pending manager review for closure.',
'widget_ticket' => 'Ticket',
'widget_customer' => 'Customer',
'widget_handler' => 'Handler',
'widget_resolved' => 'Resolved',
'widget_resolved_tickets' => 'Resolved Tickets',
'widget_total_resolved_closed' => 'Total resolved/closed tickets',
'widget_avg_processing_time' => 'Avg Processing Time',
'widget_avg_time_desc' => 'Avg time from creation to resolution',
'widget_no_resolved_tickets' => 'No resolved tickets yet',
'widget_pending_tickets' => 'Pending Tickets',
'widget_still_in_progress' => 'Still in progress',
'widget_escalated' => 'Escalated',
'widget_escalated_desc' => 'Tickets marked as escalated',
'widget_avg_time_by_handler' => 'Avg Processing Time by Handler',
'widget_avg_time_by_handler_desc' => 'Average time from ticket creation to resolution, broken down by handler.',
'widget_avg_time_minutes' => 'Avg Time (minutes)',
'widget_unknown' => 'Unknown',
// Blade views
'blade_similar_cases' => 'Similar Cases',
'blade_matching_tags' => 'Matching tags:',
'blade_title' => 'Title',
'blade_customer' => 'Customer',
'blade_status' => 'Status',
'blade_handler' => 'Handler',
'blade_date' => 'Date',
'blade_no_attachments' => 'No attachments.',
'blade_proof_of_resolution' => 'Proof of Resolution',
'blade_customer_evidence' => 'Customer Evidence',
'blade_open' => 'Open',
'blade_links_expire' => 'Click to open. Links expire after 60 minutes.',
'blade_file_count' => ':count file(s)',
'blade_proof' => 'Proof',
'blade_evidence' => 'Evidence',
];

28
lang/en/enums.php Normal file
View File

@@ -0,0 +1,28 @@
<?php
return [
'handover' => [
'not_ready' => 'Not ready',
'ready' => 'Ready',
'handed_over' => 'Handed over',
'scheduled' => 'Scheduled',
],
'service' => [
'management_fee' => 'Management fee',
'gratitude' => 'Gratitude',
'self_business' => 'Self business',
],
'contract_status' => [
'active' => 'Active',
'expired' => 'Expired',
'liquidated' => 'Liquidated',
],
'contract_type' => [
'sale' => 'Sale contract',
'lease' => 'Lease contract',
'bcc' => 'Business cooperation contract (BCC)',
],
];

54
lang/en/feedback.php Normal file
View File

@@ -0,0 +1,54 @@
<?php
return [
// ClosingService
'already_closed' => 'This ticket has already been closed.',
'close_permission_denied' => 'Only department managers can close tickets.',
'close_department_only' => 'You can only close tickets in your managed department.',
'proof_required' => 'Proof of resolution is required to complete the process.',
'already_resolved' => 'This ticket has already been resolved or closed.',
// TransferService
'transfer_reason_required' => 'Please enter a transfer reason.',
// FileService
'file_type_not_allowed' => 'File type :type is not allowed. Allowed: jpg, png, pdf, mp4, mov.',
'file_size_exceeds' => 'File size exceeds :max KB.',
// EditFeedback page
'transfer_department' => 'Transfer Department',
'target_department' => 'Target Department',
'assign_to_optional' => 'Assign To (optional)',
'reason' => 'Reason',
'transferred_successfully' => 'Transferred successfully',
'close_ticket' => 'Close Ticket',
'close_confirm' => 'Are you sure you want to close this ticket? This action CANNOT be undone.',
'close_submit' => 'Close',
'closed_successfully' => 'Ticket closed successfully',
// InteractionsRelationManager
'interaction_history' => 'Interaction History',
'interaction_type_note' => 'Note',
'interaction_type_status_change' => 'Status Change',
'interaction_type_assignment' => 'Assignment / Forward',
'view_attachments' => 'View Attachments',
'add_interaction' => 'Add Interaction',
'assigned_successfully' => 'Feedback assigned successfully',
'status_updated' => 'Status updated successfully',
// Notifications
'notif_ticket_closed_subject' => 'Ticket #:id has been closed',
'notif_ticket_closed_ticket' => 'Ticket: :title',
'notif_ticket_closed_by' => 'Closed by: :name',
'notif_view_ticket' => 'View Ticket',
'notif_ticket_closed_message' => 'Ticket #:id ":title" has been closed by :actor.',
'notif_ticket_transferred_subject' => 'Ticket #:id has been transferred to your department',
'notif_ticket_transferred_by' => 'Transferred by: :name',
'notif_ticket_transferred_reason' => 'Reason: :reason',
'notif_ticket_transferred_message' => 'Ticket #:id ":title" has been transferred to your department by :sender.',
// CreateFeedback
'created_via' => 'Feedback created via :channel',
'unknown_channel' => 'unknown channel',
];

133
lang/vi/app.php Normal file
View File

@@ -0,0 +1,133 @@
<?php
return [
// Navigation
'nav_customer_care' => 'Chăm sóc khách hàng',
'nav_management' => 'Quản lý',
// Resource labels
'resource_feedbacks' => 'Phản ánh',
'resource_products' => 'Sản phẩm',
'resource_customers' => 'Khách hàng',
'resource_contracts' => 'Hợp đồng',
'resource_channels' => 'Kênh phản ánh',
'resource_tags' => 'Thẻ',
'resource_roles' => 'Vai trò',
'resource_users' => 'Người dùng',
// Status
'status_pending' => 'Chờ xử lý',
'status_processing' => 'Đang xử lý',
'status_resolved' => 'Đã xử lý',
'status_closed' => 'Đã đóng',
'status_active' => 'Hoạt động',
'status_inactive' => 'Không hoạt động',
'status_sold_out' => 'Hết hàng',
'status_completed' => 'Hoàn thành',
'status_cancelled' => 'Đã hủy',
// General labels
'name' => 'Tên',
'email' => 'Email',
'password' => 'Mật khẩu',
'phone' => 'Điện thoại',
'address' => 'Địa chỉ',
'slug' => 'Slug',
'icon' => 'Biểu tượng',
'description' => 'Mô tả',
'status' => 'Trạng thái',
'customer_product' => 'Khách hàng & Sản phẩm',
'select_customer_product' => 'Chọn khách hàng và sản phẩm liên quan',
'general_feedback' => 'Phản ánh chung (không liên quan đến sản phẩm)',
'product' => 'Sản phẩm',
'contract' => 'Hợp đồng',
'feedback_details' => 'Chi tiết phản ánh',
'describe_feedback' => 'Mô tả phản ánh của khách hàng',
'assignment_status' => 'Phân công & Trạng thái',
'assign_handler' => 'Phân công người xử lý và trạng thái ban đầu',
'department' => 'Phòng ban',
'escalated' => 'Khẩn cấp',
'attachments' => 'Tệp đính kèm',
'upload_files' => 'Tải lên tệp liên quan đến phản ánh này',
'collection' => 'Loại',
'collection_general' => 'Chung',
'collection_proof_of_resolution' => 'Bằng chứng giải quyết',
'collection_customer_evidence' => 'Bằng chứng khách hàng',
'files' => 'Tệp',
'channel' => 'Kênh',
'assigned_to' => 'Người xử lý',
'created' => 'Ngày tạo',
'general' => 'Chung',
'new_status' => 'Trạng thái mới',
'assign_to' => 'Chỉ định cho',
'target_department' => 'Phòng ban đích',
'by' => 'Người thực hiện',
'details' => 'Chi tiết',
'unassigned' => 'Chưa phân công',
'role' => 'Vai trò',
'role_name' => 'Tên vai trò',
'permissions' => 'Quyền',
'color' => 'Màu sắc',
'customer_information' => 'Thông tin khách hàng',
'owned_products' => 'Sản phẩm sở hữu',
'icon_hint' => 'Tên Heroicon hoặc emoji',
'feedbacks' => 'Phản ánh',
'used' => 'Đã dùng',
'owners' => 'Chủ sở hữu',
'tickets' => 'Phiếu',
'signed' => 'Đã ký',
'interactions' => 'Tương tác',
'services' => 'Dịch vụ',
'handovers' => 'Bàn giao',
'contracts' => 'Hợp đồng',
'collection_date' => 'Ngày thu',
'remark' => 'Ghi chú',
'handover_date' => 'Ngày bàn giao',
'start_date' => 'Ngày bắt đầu',
'end_date' => 'Ngày kết thúc',
'printed_at' => 'Ngày in HĐ',
'signed_status' => 'Tình trạng ký',
'contract_info' => 'Thông tin hợp đồng',
'feedback_history' => 'Lịch sử phản ánh',
'tab_all' => 'Tất cả',
'min' => 'phút',
'na' => 'N/A',
// Widget labels
'widget_tickets_awaiting_closure' => 'Phiếu chờ đóng',
'widget_tickets_awaiting_desc' => 'Các phiếu đã xử lý và đang chờ quản lý xem xét để đóng.',
'widget_ticket' => 'Phiếu',
'widget_customer' => 'Khách hàng',
'widget_handler' => 'Người xử lý',
'widget_resolved' => 'Đã xử lý',
'widget_resolved_tickets' => 'Phiếu đã xử lý',
'widget_total_resolved_closed' => 'Tổng phiếu đã xử lý/đã đóng',
'widget_avg_processing_time' => 'Thời gian xử lý TB',
'widget_avg_time_desc' => 'Thời gian TB từ khi tạo đến khi xử lý',
'widget_no_resolved_tickets' => 'Chưa có phiếu đã xử lý',
'widget_pending_tickets' => 'Phiếu đang chờ',
'widget_still_in_progress' => 'Đang xử lý',
'widget_escalated' => 'Khẩn cấp',
'widget_escalated_desc' => 'Phiếu được đánh dấu khẩn cấp',
'widget_avg_time_by_handler' => 'Thời gian xử lý TB theo người xử lý',
'widget_avg_time_by_handler_desc' => 'Thời gian trung bình từ khi tạo phiếu đến khi xử lý, phân theo người xử lý.',
'widget_avg_time_minutes' => 'Thời gian TB (phút)',
'widget_unknown' => 'Không xác định',
// Blade views
'blade_similar_cases' => 'Trường hợp tương tự',
'blade_matching_tags' => 'Thẻ trùng khớp:',
'blade_title' => 'Tiêu đề',
'blade_customer' => 'Khách hàng',
'blade_status' => 'Trạng thái',
'blade_handler' => 'Người xử lý',
'blade_date' => 'Ngày',
'blade_no_attachments' => 'Không có tệp đính kèm.',
'blade_proof_of_resolution' => 'Bằng chứng giải quyết',
'blade_customer_evidence' => 'Bằng chứng khách hàng',
'blade_open' => 'Mở',
'blade_links_expire' => 'Nhấn để mở. Liên kết hết hạn sau 60 phút.',
'blade_file_count' => ':count tệp',
'blade_proof' => 'Bằng chứng',
'blade_evidence' => 'Chứng cứ',
];

28
lang/vi/enums.php Normal file
View File

@@ -0,0 +1,28 @@
<?php
return [
'handover' => [
'not_ready' => 'Chưa đủ điều kiện',
'ready' => 'Đủ điều kiện',
'handed_over' => 'Đã bàn giao',
'scheduled' => 'Đã lên lịch',
],
'service' => [
'management_fee' => 'Phí quản lý',
'gratitude' => 'Tri ân',
'self_business' => 'Tự doanh',
],
'contract_status' => [
'active' => 'Đang hiệu lực',
'expired' => 'Hết hạn',
'liquidated' => 'Đã thanh lý',
],
'contract_type' => [
'sale' => 'HĐ Mua bán',
'lease' => 'HĐ Thuê',
'bcc' => 'HĐ Hợp tác kinh doanh (BCC)',
],
];

54
lang/vi/feedback.php Normal file
View File

@@ -0,0 +1,54 @@
<?php
return [
// ClosingService
'already_closed' => 'Phiếu này đã được đóng.',
'close_permission_denied' => 'Chỉ lãnh đạo cấp phòng mới có quyền đóng phiếu.',
'close_department_only' => 'Bạn chỉ có thể đóng phiếu thuộc phòng ban mình quản lý.',
'proof_required' => 'Yêu cầu cung cấp tài liệu bằng chứng để hoàn tất quy trình.',
'already_resolved' => 'Phiếu này đã được xử lý hoặc đóng.',
// TransferService
'transfer_reason_required' => 'Vui lòng nhập lý do chuyển tiếp.',
// FileService
'file_type_not_allowed' => 'Loại file :type không được phép. Cho phép: jpg, png, pdf, mp4, mov.',
'file_size_exceeds' => 'Kích thước file vượt quá :max KB.',
// EditFeedback page
'transfer_department' => 'Chuyển phòng ban',
'target_department' => 'Phòng ban đích',
'assign_to_optional' => 'Chỉ định người xử lý (tùy chọn)',
'reason' => 'Lý do',
'transferred_successfully' => 'Chuyển tiếp thành công',
'close_ticket' => 'Đóng phiếu',
'close_confirm' => 'Bạn có chắc muốn đóng phiếu này? Hành động này KHÔNG thể hoàn tác.',
'close_submit' => 'Đóng',
'closed_successfully' => 'Đóng phiếu thành công',
// InteractionsRelationManager
'interaction_history' => 'Lịch sử tương tác',
'interaction_type_note' => 'Ghi chú',
'interaction_type_status_change' => 'Thay đổi trạng thái',
'interaction_type_assignment' => 'Phân công / Chuyển tiếp',
'view_attachments' => 'Xem tệp đính kèm',
'add_interaction' => 'Thêm tương tác',
'assigned_successfully' => 'Phân công thành công',
'status_updated' => 'Cập nhật trạng thái thành công',
// Notifications
'notif_ticket_closed_subject' => 'Ticket #:id đã được đóng',
'notif_ticket_closed_ticket' => 'Ticket: :title',
'notif_ticket_closed_by' => 'Đóng bởi: :name',
'notif_view_ticket' => 'Xem Ticket',
'notif_ticket_closed_message' => 'Ticket #:id ":title" đã được đóng bởi :actor.',
'notif_ticket_transferred_subject' => 'Ticket #:id đã được chuyển đến phòng của bạn',
'notif_ticket_transferred_by' => 'Người chuyển: :name',
'notif_ticket_transferred_reason' => 'Lý do: :reason',
'notif_ticket_transferred_message' => 'Ticket #:id ":title" đã được chuyển đến phòng của bạn bởi :sender.',
// CreateFeedback
'created_via' => 'Phản ánh được tạo qua :channel',
'unknown_channel' => 'kênh không xác định',
];

441
previews/dashboard.html Normal file
View File

@@ -0,0 +1,441 @@
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AfterSales CRM — Dashboard Preview</title>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwindcss.config = {
theme: {
extend: {
colors: {
primary: { 50:'#eff6ff',100:'#dbeafe',200:'#bfdbfe',300:'#93c5fd',400:'#60a5fa',500:'#3b82f6',600:'#2563eb',700:'#1d4ed8',800:'#1e40af',900:'#1e3a8a' },
brand: '#2563eb',
},
fontFamily: { sans: ['Inter','system-ui','sans-serif'] },
}
}
}
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<style>
body { font-family: 'Inter', sans-serif; }
.stat-card { transition: all 0.2s; }
.stat-card:hover { transform: translateY(-2px); box-shadow: 0 8px 25px rgba(0,0,0,0.1); }
.trend-up { color: #10b981; }
.trend-down { color: #ef4444; }
.pulse-dot { animation: pulse 2s infinite; }
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:0.5} }
.sidebar-link { transition: all 0.15s; }
.sidebar-link:hover { background: #eff6ff; color: #2563eb; }
.sidebar-link.active { background: #2563eb; color: white; }
</style>
</head>
<body class="bg-gray-50 text-gray-900">
<div class="flex min-h-screen">
<!-- Sidebar -->
<aside class="w-64 bg-white border-r border-gray-200 flex flex-col">
<!-- Logo -->
<div class="h-16 flex items-center px-6 border-b border-gray-100">
<div class="w-8 h-8 bg-brand rounded-lg flex items-center justify-center mr-3">
<svg class="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"/></svg>
</div>
<span class="font-bold text-lg text-gray-900">AfterSales</span>
</div>
<!-- Nav -->
<nav class="flex-1 px-3 py-4 space-y-1">
<a href="#" class="sidebar-link active flex items-center px-3 py-2.5 rounded-lg text-sm font-medium">
<svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zm10 0a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zm10 0a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z"/></svg>
Dashboard
</a>
<div class="pt-4 pb-2 px-3">
<p class="text-xs font-semibold text-gray-400 uppercase tracking-wider">Customer Care</p>
</div>
<a href="feedback-list.html" class="sidebar-link flex items-center px-3 py-2.5 rounded-lg text-sm font-medium text-gray-700">
<svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"/></svg>
Feedbacks
<span class="ml-auto bg-red-100 text-red-700 text-xs font-semibold px-2 py-0.5 rounded-full">5</span>
</a>
<div class="pt-4 pb-2 px-3">
<p class="text-xs font-semibold text-gray-400 uppercase tracking-wider">Management</p>
</div>
<a href="#" class="sidebar-link flex items-center px-3 py-2.5 rounded-lg text-sm font-medium text-gray-700">
<svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"/></svg>
Products
</a>
<a href="#" class="sidebar-link flex items-center px-3 py-2.5 rounded-lg text-sm font-medium text-gray-700">
<svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"/></svg>
Customers
</a>
<a href="#" class="sidebar-link flex items-center px-3 py-2.5 rounded-lg text-sm font-medium text-gray-700">
<svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>
Contracts
</a>
<a href="#" class="sidebar-link flex items-center px-3 py-2.5 rounded-lg text-sm font-medium text-gray-700">
<svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4"/></svg>
Channels
</a>
<a href="#" class="sidebar-link flex items-center px-3 py-2.5 rounded-lg text-sm font-medium text-gray-700">
<svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"/></svg>
Tags
</a>
</nav>
<!-- User -->
<div class="p-4 border-t border-gray-100">
<div class="flex items-center">
<div class="w-9 h-9 bg-primary-100 rounded-full flex items-center justify-center">
<span class="text-primary-700 font-semibold text-sm">AD</span>
</div>
<div class="ml-3">
<p class="text-sm font-medium text-gray-900">Admin User</p>
<p class="text-xs text-gray-500">admin@minicrm.local</p>
</div>
</div>
</div>
</aside>
<!-- Main Content -->
<main class="flex-1 overflow-auto">
<!-- Top Bar -->
<header class="h-16 bg-white border-b border-gray-200 flex items-center justify-between px-8">
<div>
<h1 class="text-xl font-bold text-gray-900">Dashboard</h1>
<p class="text-sm text-gray-500">Welcome back, Admin</p>
</div>
<div class="flex items-center gap-4">
<button class="relative p-2 text-gray-400 hover:text-gray-600 transition">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"/></svg>
<span class="absolute top-1.5 right-1.5 w-2 h-2 bg-red-500 rounded-full pulse-dot"></span>
</button>
<div class="w-8 h-8 bg-primary-600 rounded-full flex items-center justify-center">
<span class="text-white font-semibold text-xs">AD</span>
</div>
</div>
</header>
<!-- Dashboard Content -->
<div class="p-8 space-y-6">
<!-- Row 1: Stats Cards -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<!-- Resolved -->
<div class="stat-card bg-white rounded-xl border border-gray-200 p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-gray-500">Resolved Tickets</p>
<p class="text-3xl font-bold text-gray-900 mt-1">156</p>
</div>
<div class="w-12 h-12 bg-emerald-50 rounded-xl flex items-center justify-center">
<svg class="w-6 h-6 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
</div>
</div>
<div class="mt-4 flex items-center">
<span class="trend-up text-sm font-medium flex items-center">
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 10l7-7m0 0l7 7m-7-7v18"/></svg>
+12%
</span>
<span class="text-sm text-gray-500 ml-2">vs last week</span>
</div>
</div>
<!-- Avg Processing Time -->
<div class="stat-card bg-white rounded-xl border border-gray-200 p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-gray-500">Avg Processing Time</p>
<p class="text-3xl font-bold text-gray-900 mt-1">45 <span class="text-lg font-medium text-gray-400">min</span></p>
</div>
<div class="w-12 h-12 bg-amber-50 rounded-xl flex items-center justify-center">
<svg class="w-6 h-6 text-amber-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
</div>
</div>
<div class="mt-4 flex items-center">
<span class="trend-down text-sm font-medium flex items-center">
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 14l-7 7m0 0l-7-7m7 7V3"/></svg>
-8%
</span>
<span class="text-sm text-gray-500 ml-2">improved</span>
</div>
</div>
<!-- Pending -->
<div class="stat-card bg-white rounded-xl border border-gray-200 p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-gray-500">Pending Tickets</p>
<p class="text-3xl font-bold text-gray-900 mt-1">23</p>
</div>
<div class="w-12 h-12 bg-blue-50 rounded-xl flex items-center justify-center">
<svg class="w-6 h-6 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
</div>
</div>
<div class="mt-4 flex items-center">
<span class="trend-down text-sm font-medium flex items-center">
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 14l-7 7m0 0l-7-7m7 7V3"/></svg>
-3
</span>
<span class="text-sm text-gray-500 ml-2">from yesterday</span>
</div>
</div>
<!-- Escalated -->
<div class="stat-card bg-white rounded-xl border border-gray-200 p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-gray-500">Escalated</p>
<p class="text-3xl font-bold text-red-600 mt-1">5</p>
</div>
<div class="w-12 h-12 bg-red-50 rounded-xl flex items-center justify-center">
<svg class="w-6 h-6 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z"/></svg>
</div>
</div>
<div class="mt-4 flex items-center">
<span class="trend-up text-sm font-medium flex items-center text-red-600">
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 10l7-7m0 0l7 7m-7-7v18"/></svg>
+2
</span>
<span class="text-sm text-gray-500 ml-2">needs attention</span>
</div>
</div>
</div>
<!-- Row 2: Charts -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- Handler Performance Chart -->
<div class="bg-white rounded-xl border border-gray-200 p-6">
<div class="flex items-center justify-between mb-6">
<div>
<h3 class="text-lg font-semibold text-gray-900">Handler Performance</h3>
<p class="text-sm text-gray-500">Average processing time per handler</p>
</div>
<select class="text-sm border border-gray-200 rounded-lg px-3 py-1.5 text-gray-600 focus:outline-none focus:ring-2 focus:ring-primary-500">
<option>This Week</option>
<option>This Month</option>
<option>This Quarter</option>
</select>
</div>
<div class="space-y-4">
<div class="flex items-center">
<div class="w-24 text-sm font-medium text-gray-700">Nguyễn A</div>
<div class="flex-1 mx-4">
<div class="h-8 bg-gray-100 rounded-lg overflow-hidden">
<div class="h-full bg-emerald-500 rounded-lg flex items-center justify-end pr-3" style="width: 48%">
<span class="text-xs font-semibold text-white">32 min</span>
</div>
</div>
</div>
<div class="w-16 text-right">
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-emerald-50 text-emerald-700">Fast</span>
</div>
</div>
<div class="flex items-center">
<div class="w-24 text-sm font-medium text-gray-700">Trần B</div>
<div class="flex-1 mx-4">
<div class="h-8 bg-gray-100 rounded-lg overflow-hidden">
<div class="h-full bg-blue-500 rounded-lg flex items-center justify-end pr-3" style="width: 65%">
<span class="text-xs font-semibold text-white">48 min</span>
</div>
</div>
</div>
<div class="w-16 text-right">
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-blue-50 text-blue-700">Normal</span>
</div>
</div>
<div class="flex items-center">
<div class="w-24 text-sm font-medium text-gray-700">Lê C</div>
<div class="flex-1 mx-4">
<div class="h-8 bg-gray-100 rounded-lg overflow-hidden">
<div class="h-full bg-amber-500 rounded-lg flex items-center justify-end pr-3" style="width: 85%">
<span class="text-xs font-semibold text-white">65 min</span>
</div>
</div>
</div>
<div class="w-16 text-right">
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-amber-50 text-amber-700">Slow</span>
</div>
</div>
<div class="flex items-center">
<div class="w-24 text-sm font-medium text-gray-700">Phạm D</div>
<div class="flex-1 mx-4">
<div class="h-8 bg-gray-100 rounded-lg overflow-hidden">
<div class="h-full bg-emerald-500 rounded-lg flex items-center justify-end pr-3" style="width: 40%">
<span class="text-xs font-semibold text-white">28 min</span>
</div>
</div>
</div>
<div class="w-16 text-right">
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-emerald-50 text-emerald-700">Fast</span>
</div>
</div>
<div class="flex items-center">
<div class="w-24 text-sm font-medium text-gray-700">Hoàng E</div>
<div class="flex-1 mx-4">
<div class="h-8 bg-gray-100 rounded-lg overflow-hidden">
<div class="h-full bg-red-500 rounded-lg flex items-center justify-end pr-3" style="width: 95%">
<span class="text-xs font-semibold text-white">82 min</span>
</div>
</div>
</div>
<div class="w-16 text-right">
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-red-50 text-red-700">Alert</span>
</div>
</div>
</div>
</div>
<!-- Tickets by Status Donut -->
<div class="bg-white rounded-xl border border-gray-200 p-6">
<div class="flex items-center justify-between mb-6">
<div>
<h3 class="text-lg font-semibold text-gray-900">Tickets by Status</h3>
<p class="text-sm text-gray-500">Current distribution</p>
</div>
</div>
<div class="flex items-center justify-center">
<!-- SVG Donut Chart -->
<div class="relative">
<svg width="200" height="200" viewBox="0 0 200 200">
<circle cx="100" cy="100" r="80" fill="none" stroke="#fbbf24" stroke-width="32" stroke-dasharray="100.53 402.12" stroke-dashoffset="0" transform="rotate(-90 100 100)"/>
<circle cx="100" cy="100" r="80" fill="none" stroke="#3b82f6" stroke-width="32" stroke-dasharray="75.4 402.12" stroke-dashoffset="-100.53" transform="rotate(-90 100 100)"/>
<circle cx="100" cy="100" r="80" fill="none" stroke="#10b981" stroke-width="32" stroke-dasharray="150.8 402.12" stroke-dashoffset="-175.93" transform="rotate(-90 100 100)"/>
<circle cx="100" cy="100" r="80" fill="none" stroke="#9ca3af" stroke-width="32" stroke-dasharray="75.4 402.12" stroke-dashoffset="-326.73" transform="rotate(-90 100 100)"/>
<text x="100" y="95" text-anchor="middle" class="text-3xl font-bold" fill="#111827">207</text>
<text x="100" y="115" text-anchor="middle" class="text-sm" fill="#6b7280">Total</text>
</svg>
</div>
</div>
<div class="mt-6 grid grid-cols-2 gap-4">
<div class="flex items-center">
<div class="w-3 h-3 rounded-full bg-amber-400 mr-2"></div>
<span class="text-sm text-gray-600">Pending</span>
<span class="ml-auto text-sm font-semibold text-gray-900">23</span>
</div>
<div class="flex items-center">
<div class="w-3 h-3 rounded-full bg-blue-500 mr-2"></div>
<span class="text-sm text-gray-600">Processing</span>
<span class="ml-auto text-sm font-semibold text-gray-900">18</span>
</div>
<div class="flex items-center">
<div class="w-3 h-3 rounded-full bg-emerald-500 mr-2"></div>
<span class="text-sm text-gray-600">Resolved</span>
<span class="ml-auto text-sm font-semibold text-gray-900">156</span>
</div>
<div class="flex items-center">
<div class="w-3 h-3 rounded-full bg-gray-400 mr-2"></div>
<span class="text-sm text-gray-600">Closed</span>
<span class="ml-auto text-sm font-semibold text-gray-900">10</span>
</div>
</div>
</div>
</div>
<!-- Row 3: Resolved Tickets Table -->
<div class="bg-white rounded-xl border border-gray-200">
<div class="px-6 py-5 border-b border-gray-100">
<div class="flex items-center justify-between">
<div>
<h3 class="text-lg font-semibold text-gray-900">Tickets Awaiting Closure</h3>
<p class="text-sm text-gray-500 mt-0.5">Resolved tickets pending manager review</p>
</div>
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-amber-50 text-amber-700 border border-amber-200">
4 tickets
</span>
</div>
</div>
<div class="overflow-x-auto">
<table class="w-full">
<thead>
<tr class="bg-gray-50">
<th class="px-6 py-3 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider">#</th>
<th class="px-6 py-3 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider">Ticket</th>
<th class="px-6 py-3 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider">Customer</th>
<th class="px-6 py-3 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider">Handler</th>
<th class="px-6 py-3 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider">Department</th>
<th class="px-6 py-3 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider">Contract</th>
<th class="px-6 py-3 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider">Resolved Date</th>
<th class="px-6 py-3 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider">Action</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
<tr class="hover:bg-gray-50 transition">
<td class="px-6 py-4 text-sm font-medium text-gray-900">#001</td>
<td class="px-6 py-4">
<a href="edit-feedback.html" class="text-sm font-medium text-primary-600 hover:text-primary-800 hover:underline">Khách hàng phản ánh nước nóng</a>
</td>
<td class="px-6 py-4 text-sm text-gray-700">Nguyễn Văn A</td>
<td class="px-6 py-4">
<div class="flex items-center">
<div class="w-6 h-6 bg-blue-100 rounded-full flex items-center justify-center mr-2"><span class="text-xs font-medium text-blue-700">S</span></div>
<span class="text-sm text-gray-700">Staff User</span>
</div>
</td>
<td class="px-6 py-4"><span class="text-sm text-gray-700">CSKH</span></td>
<td class="px-6 py-4"><span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-emerald-50 text-emerald-700 border border-emerald-200">SALE</span></td>
<td class="px-6 py-4 text-sm text-gray-500">30/04/2026</td>
<td class="px-6 py-4">
<button class="inline-flex items-center px-3 py-1.5 bg-emerald-600 text-white text-xs font-medium rounded-lg hover:bg-emerald-700 transition">
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>
Close
</button>
</td>
</tr>
<tr class="hover:bg-gray-50 transition">
<td class="px-6 py-4 text-sm font-medium text-gray-900">#003</td>
<td class="px-6 py-4">
<a href="#" class="text-sm font-medium text-primary-600 hover:text-primary-800 hover:underline">Yêu cầu sửa chữa cửa sổ</a>
</td>
<td class="px-6 py-4 text-sm text-gray-700">Trần Thị B</td>
<td class="px-6 py-4">
<div class="flex items-center">
<div class="w-6 h-6 bg-purple-100 rounded-full flex items-center justify-center mr-2"><span class="text-xs font-medium text-purple-700">T</span></div>
<span class="text-sm text-gray-700">Trần Staff</span>
</div>
</td>
<td class="px-6 py-4"><span class="text-sm text-gray-700">Kỹ thuật</span></td>
<td class="px-6 py-4"><span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-blue-50 text-blue-700 border border-blue-200">LEASE</span></td>
<td class="px-6 py-4 text-sm text-gray-500">29/04/2026</td>
<td class="px-6 py-4">
<button class="inline-flex items-center px-3 py-1.5 bg-emerald-600 text-white text-xs font-medium rounded-lg hover:bg-emerald-700 transition">
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>
Close
</button>
</td>
</tr>
<tr class="hover:bg-gray-50 transition">
<td class="px-6 py-4 text-sm font-medium text-gray-900">#005</td>
<td class="px-6 py-4">
<a href="#" class="text-sm font-medium text-primary-600 hover:text-primary-800 hover:underline">Hỗ trợ đăng ký cư trú</a>
</td>
<td class="px-6 py-4 text-sm text-gray-700">Lê Minh C</td>
<td class="px-6 py-4">
<div class="flex items-center">
<div class="w-6 h-6 bg-emerald-100 rounded-full flex items-center justify-center mr-2"><span class="text-xs font-medium text-emerald-700">L</span></div>
<span class="text-sm text-gray-700">Lê Staff</span>
</div>
</td>
<td class="px-6 py-4"><span class="text-sm text-gray-700">Pháp lý</span></td>
<td class="px-6 py-4"><span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-amber-50 text-amber-700 border border-amber-200">BCC</span></td>
<td class="px-6 py-4 text-sm text-gray-500">28/04/2026</td>
<td class="px-6 py-4">
<button class="inline-flex items-center px-3 py-1.5 bg-emerald-600 text-white text-xs font-medium rounded-lg hover:bg-emerald-700 transition">
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>
Close
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</main>
</div>
</body>
</html>

372
previews/feedback-list.html Normal file
View File

@@ -0,0 +1,372 @@
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AfterSales CRM — Feedback List Preview</title>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwindcss.config = {
theme: {
extend: {
colors: {
primary: { 50:'#eff6ff',100:'#dbeafe',200:'#bfdbfe',300:'#93c5fd',400:'#60a5fa',500:'#3b82f6',600:'#2563eb',700:'#1d4ed8',800:'#1e40af',900:'#1e3a8a' },
brand: '#2563eb',
},
fontFamily: { sans: ['Inter','system-ui','sans-serif'] },
}
}
}
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<style>
body { font-family: 'Inter', sans-serif; }
.sidebar-link { transition: all 0.15s; }
.sidebar-link:hover { background: #eff6ff; color: #2563eb; }
.sidebar-link.active { background: #2563eb; color: white; }
.row-pending { border-left: 3px solid #fbbf24; }
.row-processing { border-left: 3px solid #3b82f6; }
.row-resolved { border-left: 3px solid #10b981; }
.row-closed { border-left: 3px solid #9ca3af; }
.escalated-pulse { animation: pulse-red 2s infinite; }
@keyframes pulse-red { 0%,100%{box-shadow:0 0 0 0 rgba(239,68,68,0.4)} 50%{box-shadow:0 0 0 6px rgba(239,68,68,0)} }
.tag { display:inline-flex; align-items:center; padding:2px 8px; border-radius:9999px; font-size:0.7rem; font-weight:600; }
.filter-chip { transition: all 0.15s; cursor:pointer; }
.filter-chip:hover { background: #eff6ff; }
.filter-chip.active { background: #2563eb; color: white; border-color: #2563eb; }
</style>
</head>
<body class="bg-gray-50 text-gray-900">
<div class="flex min-h-screen">
<!-- Sidebar (same as dashboard) -->
<aside class="w-64 bg-white border-r border-gray-200 flex flex-col">
<div class="h-16 flex items-center px-6 border-b border-gray-100">
<div class="w-8 h-8 bg-brand rounded-lg flex items-center justify-center mr-3">
<svg class="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"/></svg>
</div>
<span class="font-bold text-lg text-gray-900">AfterSales</span>
</div>
<nav class="flex-1 px-3 py-4 space-y-1">
<a href="dashboard.html" class="sidebar-link flex items-center px-3 py-2.5 rounded-lg text-sm font-medium text-gray-700">
<svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zm10 0a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zm10 0a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z"/></svg>
Dashboard
</a>
<div class="pt-4 pb-2 px-3"><p class="text-xs font-semibold text-gray-400 uppercase tracking-wider">Customer Care</p></div>
<a href="#" class="sidebar-link active flex items-center px-3 py-2.5 rounded-lg text-sm font-medium">
<svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"/></svg>
Feedbacks
<span class="ml-auto bg-red-100 text-red-700 text-xs font-semibold px-2 py-0.5 rounded-full">5</span>
</a>
<div class="pt-4 pb-2 px-3"><p class="text-xs font-semibold text-gray-400 uppercase tracking-wider">Management</p></div>
<a href="#" class="sidebar-link flex items-center px-3 py-2.5 rounded-lg text-sm font-medium text-gray-700">
<svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"/></svg>
Products
</a>
<a href="#" class="sidebar-link flex items-center px-3 py-2.5 rounded-lg text-sm font-medium text-gray-700">
<svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"/></svg>
Customers
</a>
<a href="#" class="sidebar-link flex items-center px-3 py-2.5 rounded-lg text-sm font-medium text-gray-700">
<svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>
Contracts
</a>
<a href="#" class="sidebar-link flex items-center px-3 py-2.5 rounded-lg text-sm font-medium text-gray-700">
<svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4"/></svg>
Channels
</a>
<a href="#" class="sidebar-link flex items-center px-3 py-2.5 rounded-lg text-sm font-medium text-gray-700">
<svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"/></svg>
Tags
</a>
</nav>
<div class="p-4 border-t border-gray-100">
<div class="flex items-center">
<div class="w-9 h-9 bg-primary-100 rounded-full flex items-center justify-center"><span class="text-primary-700 font-semibold text-sm">AD</span></div>
<div class="ml-3"><p class="text-sm font-medium text-gray-900">Admin User</p><p class="text-xs text-gray-500">admin@minicrm.local</p></div>
</div>
</div>
</aside>
<!-- Main Content -->
<main class="flex-1 overflow-auto">
<!-- Top Bar -->
<header class="h-16 bg-white border-b border-gray-200 flex items-center justify-between px-8">
<div>
<h1 class="text-xl font-bold text-gray-900">Feedbacks</h1>
<p class="text-sm text-gray-500">Manage customer feedback and tickets</p>
</div>
<div class="flex items-center gap-3">
<button class="relative p-2 text-gray-400 hover:text-gray-600 transition">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"/></svg>
<span class="absolute top-1.5 right-1.5 w-2 h-2 bg-red-500 rounded-full"></span>
</button>
<a href="#" class="inline-flex items-center px-4 py-2 bg-brand text-white text-sm font-medium rounded-lg hover:bg-primary-700 transition shadow-sm">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/></svg>
New Feedback
</a>
</div>
</header>
<div class="p-8 space-y-5">
<!-- Quick Stats Bar -->
<div class="grid grid-cols-5 gap-4">
<button class="filter-chip active bg-white border border-gray-200 rounded-xl px-4 py-3 text-center transition">
<p class="text-2xl font-bold text-white">5</p>
<p class="text-xs font-medium text-primary-100">All</p>
</button>
<button class="filter-chip bg-white border border-gray-200 rounded-xl px-4 py-3 text-center transition">
<p class="text-2xl font-bold text-amber-600">2</p>
<p class="text-xs font-medium text-gray-500">Pending</p>
</button>
<button class="filter-chip bg-white border border-gray-200 rounded-xl px-4 py-3 text-center transition">
<p class="text-2xl font-bold text-blue-600">1</p>
<p class="text-xs font-medium text-gray-500">Processing</p>
</button>
<button class="filter-chip bg-white border border-gray-200 rounded-xl px-4 py-3 text-center transition">
<p class="text-2xl font-bold text-emerald-600">1</p>
<p class="text-xs font-medium text-gray-500">Resolved</p>
</button>
<button class="filter-chip bg-white border border-gray-200 rounded-xl px-4 py-3 text-center transition">
<p class="text-2xl font-bold text-gray-400">1</p>
<p class="text-xs font-medium text-gray-500">Closed</p>
</button>
</div>
<!-- Search & Filters -->
<div class="bg-white rounded-xl border border-gray-200 p-4">
<div class="flex items-center gap-4">
<!-- Search -->
<div class="flex-1 relative">
<svg class="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg>
<input type="text" placeholder="Search by title, customer, or ID..." class="w-full pl-10 pr-4 py-2.5 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent" value="">
</div>
<!-- Filter dropdowns -->
<select class="border border-gray-200 rounded-lg px-3 py-2.5 text-sm text-gray-600 focus:outline-none focus:ring-2 focus:ring-primary-500">
<option>All Channels</option>
<option>Email</option>
<option>Zalo</option>
<option>Phone</option>
<option>Document</option>
<option>In Person</option>
</select>
<select class="border border-gray-200 rounded-lg px-3 py-2.5 text-sm text-gray-600 focus:outline-none focus:ring-2 focus:ring-primary-500">
<option>All Departments</option>
<option>CSKH</option>
<option>Kỹ thuật</option>
<option>Kế toán</option>
<option>Pháp lý</option>
</select>
<select class="border border-gray-200 rounded-lg px-3 py-2.5 text-sm text-gray-600 focus:outline-none focus:ring-2 focus:ring-primary-500">
<option>All Handlers</option>
<option>Staff User</option>
<option>Manager User</option>
</select>
</div>
</div>
<!-- Feedback Cards -->
<div class="space-y-3">
<!-- Card 1: Pending + Escalated -->
<div class="bg-white rounded-xl border border-gray-200 row-pending hover:shadow-md transition cursor-pointer overflow-hidden">
<div class="p-5">
<div class="flex items-start justify-between">
<div class="flex-1">
<div class="flex items-center gap-3 mb-2">
<span class="text-sm font-bold text-gray-400">#002</span>
<h3 class="text-base font-semibold text-gray-900">Khách hàng phản ánh về chất lượng nước</h3>
<span class="escalated-pulse inline-flex items-center px-2 py-0.5 rounded-full text-xs font-bold bg-red-100 text-red-700 border border-red-200">
<svg class="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 24 24"><path d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z"/></svg>
ESCALATED
</span>
</div>
<p class="text-sm text-gray-600 mb-3 line-clamp-2">Khách hàng phản ánh nước sinh hoạt có màu vàng, mùi lạ trong 3 ngày liên tiếp tại căn hộ A2-0805...</p>
<div class="flex items-center flex-wrap gap-2">
<span class="tag bg-amber-50 text-amber-700 border border-amber-200">
<svg class="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" stroke-width="2"/><path stroke-linecap="round" stroke-width="2" d="M12 6v6l4 2"/></svg>
Pending
</span>
<span class="tag bg-gray-50 text-gray-600 border border-gray-200">Zalo</span>
<span class="tag bg-blue-50 text-blue-700 border border-blue-200">CSKH</span>
<span class="tag bg-emerald-50 text-emerald-700 border border-emerald-200">SALE</span>
<span class="tag bg-purple-50 text-purple-700 border border-purple-200">
<svg class="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5"/></svg>
Sunrise A2
</span>
</div>
</div>
<div class="ml-6 flex flex-col items-end gap-2">
<div class="flex items-center text-sm text-gray-500">
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/></svg>
Trần Thị B
</div>
<div class="flex items-center text-sm text-gray-400">
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/></svg>
28/04/2026
</div>
<div class="flex items-center">
<div class="w-6 h-6 bg-gray-200 rounded-full flex items-center justify-center mr-1"><span class="text-xs font-medium text-gray-600">?</span></div>
<span class="text-xs text-gray-400">Unassigned</span>
</div>
</div>
</div>
</div>
</div>
<!-- Card 2: Processing -->
<div class="bg-white rounded-xl border border-gray-200 row-processing hover:shadow-md transition cursor-pointer overflow-hidden">
<div class="p-5">
<div class="flex items-start justify-between">
<div class="flex-1">
<div class="flex items-center gap-3 mb-2">
<span class="text-sm font-bold text-gray-400">#001</span>
<h3 class="text-base font-semibold text-gray-900">Khách hàng phản ánh hệ thống nước nóng không hoạt động</h3>
</div>
<p class="text-sm text-gray-600 mb-3 line-clamp-2">Khách hàng phản ánh về việc hệ thống nước nóng không hoạt động trong căn hộ A1-1205. Đã kiểm tra nhưng chưa xác định được nguyên nhân...</p>
<div class="flex items-center flex-wrap gap-2">
<span class="tag bg-blue-50 text-blue-700 border border-blue-200">
<svg class="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg>
Processing
</span>
<span class="tag bg-gray-50 text-gray-600 border border-gray-200">Email</span>
<span class="tag bg-blue-50 text-blue-700 border border-blue-200">Kỹ thuật</span>
<span class="tag bg-emerald-50 text-emerald-700 border border-emerald-200">SALE</span>
<span class="tag bg-purple-50 text-purple-700 border border-purple-200">
<svg class="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5"/></svg>
Sunrise A1
</span>
<span class="tag bg-pink-50 text-pink-700 border border-pink-200">
<svg class="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"/></svg>
Technical
</span>
</div>
</div>
<div class="ml-6 flex flex-col items-end gap-2">
<div class="flex items-center text-sm text-gray-500">
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/></svg>
Nguyễn Văn A
</div>
<div class="flex items-center text-sm text-gray-400">
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/></svg>
30/04/2026
</div>
<div class="flex items-center">
<div class="w-6 h-6 bg-blue-100 rounded-full flex items-center justify-center mr-1"><span class="text-xs font-medium text-blue-700">S</span></div>
<span class="text-xs text-gray-600">Staff User</span>
</div>
</div>
</div>
</div>
</div>
<!-- Card 3: Pending -->
<div class="bg-white rounded-xl border border-gray-200 row-pending hover:shadow-md transition cursor-pointer overflow-hidden">
<div class="p-5">
<div class="flex items-start justify-between">
<div class="flex-1">
<div class="flex items-center gap-3 mb-2">
<span class="text-sm font-bold text-gray-400">#004</span>
<h3 class="text-base font-semibold text-gray-900">Yêu cầu hỗ trợ đăng ký cư trú</h3>
</div>
<p class="text-sm text-gray-600 mb-3 line-clamp-2">Khách hàng cần hỗ trợ về thủ tục đăng ký cư trú cho căn hộ mới bàn giao...</p>
<div class="flex items-center flex-wrap gap-2">
<span class="tag bg-amber-50 text-amber-700 border border-amber-200">Pending</span>
<span class="tag bg-gray-50 text-gray-600 border border-gray-200">In Person</span>
<span class="tag bg-violet-50 text-violet-700 border border-violet-200">Pháp lý</span>
<span class="tag bg-amber-50 text-amber-700 border border-amber-200">BCC</span>
<span class="tag bg-purple-50 text-purple-700 border border-purple-200">Green Valley</span>
</div>
</div>
<div class="ml-6 flex flex-col items-end gap-2">
<div class="flex items-center text-sm text-gray-500">Lê Minh C</div>
<div class="flex items-center text-sm text-gray-400">27/04/2026</div>
<div class="flex items-center">
<div class="w-6 h-6 bg-gray-200 rounded-full flex items-center justify-center mr-1"><span class="text-xs font-medium text-gray-600">?</span></div>
<span class="text-xs text-gray-400">Unassigned</span>
</div>
</div>
</div>
</div>
</div>
<!-- Card 4: Resolved -->
<div class="bg-white rounded-xl border border-gray-200 row-resolved hover:shadow-md transition cursor-pointer overflow-hidden">
<div class="p-5">
<div class="flex items-start justify-between">
<div class="flex-1">
<div class="flex items-center gap-3 mb-2">
<span class="text-sm font-bold text-gray-400">#003</span>
<h3 class="text-base font-semibold text-gray-900">Yêu cầu sửa chữa hệ thống cửa sổ</h3>
</div>
<p class="text-sm text-gray-600 mb-3 line-clamp-2">Cửa sổ phòng ngủ bị kẹt không đóng được. Đã cử kỹ thuật viên sửa chữa xong...</p>
<div class="flex items-center flex-wrap gap-2">
<span class="tag bg-emerald-50 text-emerald-700 border border-emerald-200">
<svg class="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
Resolved
</span>
<span class="tag bg-gray-50 text-gray-600 border border-gray-200">Phone</span>
<span class="tag bg-blue-50 text-blue-700 border border-blue-200">Kỹ thuật</span>
<span class="tag bg-blue-50 text-blue-700 border border-blue-200">LEASE</span>
<span class="tag bg-purple-50 text-purple-700 border border-purple-200">Ocean Tower</span>
</div>
</div>
<div class="ml-6 flex flex-col items-end gap-2">
<div class="flex items-center text-sm text-gray-500">Phạm Thị D</div>
<div class="flex items-center text-sm text-gray-400">29/04/2026</div>
<div class="flex items-center">
<div class="w-6 h-6 bg-purple-100 rounded-full flex items-center justify-center mr-1"><span class="text-xs font-medium text-purple-700">T</span></div>
<span class="text-xs text-gray-600">Trần Staff</span>
</div>
</div>
</div>
</div>
</div>
<!-- Card 5: Closed -->
<div class="bg-white rounded-xl border border-gray-200 row-closed hover:shadow-md transition cursor-pointer overflow-hidden opacity-75">
<div class="p-5">
<div class="flex items-start justify-between">
<div class="flex-1">
<div class="flex items-center gap-3 mb-2">
<span class="text-sm font-bold text-gray-400">#005</span>
<h3 class="text-base font-semibold text-gray-700">Phản ánh về phí quản lý</h3>
</div>
<p class="text-sm text-gray-500 mb-3 line-clamp-2">Khách hàng thắc mắc về phí quản lý tháng 3/2026. Đã giải thích và khách hàng đồng ý...</p>
<div class="flex items-center flex-wrap gap-2">
<span class="tag bg-gray-100 text-gray-500 border border-gray-200">Closed</span>
<span class="tag bg-gray-50 text-gray-500 border border-gray-200">Email</span>
<span class="tag bg-gray-50 text-gray-500 border border-gray-200">Kế toán</span>
<span class="tag bg-gray-50 text-gray-500 border border-gray-200">SALE</span>
</div>
</div>
<div class="ml-6 flex flex-col items-end gap-2">
<div class="flex items-center text-sm text-gray-400">Hoàng Văn E</div>
<div class="flex items-center text-sm text-gray-400">25/04/2026</div>
<div class="flex items-center">
<div class="w-6 h-6 bg-emerald-100 rounded-full flex items-center justify-center mr-1"><span class="text-xs font-medium text-emerald-700">L</span></div>
<span class="text-xs text-gray-500">Lê Staff</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Pagination -->
<div class="flex items-center justify-between">
<p class="text-sm text-gray-500">Showing <span class="font-medium">1-5</span> of <span class="font-medium">5</span> results</p>
<div class="flex items-center gap-1">
<button class="px-3 py-2 text-sm text-gray-400 bg-white border border-gray-200 rounded-lg cursor-not-allowed">Previous</button>
<button class="px-3 py-2 text-sm font-medium text-white bg-brand border border-brand rounded-lg">1</button>
<button class="px-3 py-2 text-sm text-gray-600 bg-white border border-gray-200 rounded-lg hover:bg-gray-50">Next</button>
</div>
</div>
</div>
</main>
</div>
</body>
</html>

View File

@@ -1,6 +1,6 @@
<div class="p-4"> <div class="p-4">
@if(empty($links) || count($links) === 0) @if(empty($links) || count($links) === 0)
<p class="text-sm text-gray-500 py-8 text-center">No attachments.</p> <p class="text-sm text-gray-500 py-8 text-center">{{ __('app.blade_no_attachments') }}</p>
@else @else
<div class="space-y-3"> <div class="space-y-3">
@foreach($links as $link) @foreach($links as $link)
@@ -38,11 +38,11 @@
<div class="flex items-center gap-2 mt-1"> <div class="flex items-center gap-2 mt-1">
<span class="text-xs text-gray-500">{{ $link['size'] }}</span> <span class="text-xs text-gray-500">{{ $link['size'] }}</span>
@if(($link['collection'] ?? '') === 'proof_of_resolution') @if(($link['collection'] ?? '') === 'proof_of_resolution')
<span class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-semibold bg-green-100 text-green-700 border border-green-200">Proof of Resolution</span> <span class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-semibold bg-green-100 text-green-700 border border-green-200">{{ __('app.blade_proof_of_resolution') }}</span>
@elseif(($link['collection'] ?? '') === 'customer_evidence') @elseif(($link['collection'] ?? '') === 'customer_evidence')
<span class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-semibold bg-blue-100 text-blue-700 border border-blue-200">Customer Evidence</span> <span class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-semibold bg-blue-100 text-blue-700 border border-blue-200">{{ __('app.blade_customer_evidence') }}</span>
@else @else
<span class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-semibold bg-gray-100 text-gray-600 border border-gray-200">{{ $link['collection'] ?? 'General' }}</span> <span class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-semibold bg-gray-100 text-gray-600 border border-gray-200">{{ $link['collection'] ?? __('app.collection_general') }}</span>
@endif @endif
</div> </div>
</div> </div>
@@ -51,11 +51,11 @@
<a href="{{ $link['url'] }}" target="_blank" <a href="{{ $link['url'] }}" target="_blank"
onclick="event.stopPropagation();" onclick="event.stopPropagation();"
class="flex-shrink-0 inline-flex items-center px-3 py-1.5 rounded text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 transition-colors shadow-sm no-underline"> class="flex-shrink-0 inline-flex items-center px-3 py-1.5 rounded text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 transition-colors shadow-sm no-underline">
Open {{ __('app.blade_open') }}
</a> </a>
</div> </div>
@endforeach @endforeach
</div> </div>
<p class="mt-4 text-xs text-gray-500">Click to open. Links expire after 60 minutes.</p> <p class="mt-4 text-xs text-gray-500">{{ __('app.blade_links_expire') }}</p>
@endif @endif
</div> </div>

View File

@@ -10,9 +10,9 @@
<div class="px-6 py-4 border-b border-gray-100 bg-gray-50/50 flex justify-between items-center"> <div class="px-6 py-4 border-b border-gray-100 bg-gray-50/50 flex justify-between items-center">
<h3 class="font-semibold text-lg text-gray-900 flex items-center gap-2" style="font-family: 'Manrope', sans-serif;"> <h3 class="font-semibold text-lg text-gray-900 flex items-center gap-2" style="font-family: 'Manrope', sans-serif;">
<svg class="w-5 h-5 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.172 7l-6.586 6.586a2 2 0 102.828 2.828l6.414-6.586a4 4 0 00-5.656-5.656l-6.415 6.585a6 6 0 108.486 8.486L20.5 13"/></svg> <svg class="w-5 h-5 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.172 7l-6.586 6.586a2 2 0 102.828 2.828l6.414-6.586a4 4 0 00-5.656-5.656l-6.415 6.585a6 6 0 108.486 8.486L20.5 13"/></svg>
Attachments {{ __('app.attachments') }}
</h3> </h3>
<span class="text-xs text-gray-500">{{ $attachments->count() }} file(s)</span> <span class="text-xs text-gray-500">{{ __('app.blade_file_count', ['count' => $attachments->count()]) }}</span>
</div> </div>
<div class="p-4"> <div class="p-4">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3"> <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
@@ -45,11 +45,11 @@
<div class="flex items-center gap-2 mt-1"> <div class="flex items-center gap-2 mt-1">
<span class="text-xs text-gray-500">{{ $sizeKb }}</span> <span class="text-xs text-gray-500">{{ $sizeKb }}</span>
@if($attachment->collection === 'proof_of_resolution') @if($attachment->collection === 'proof_of_resolution')
<span class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-semibold bg-green-100 text-green-700 border border-green-200">Proof</span> <span class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-semibold bg-green-100 text-green-700 border border-green-200">{{ __('app.blade_proof') }}</span>
@elseif($attachment->collection === 'customer_evidence') @elseif($attachment->collection === 'customer_evidence')
<span class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-semibold bg-blue-100 text-blue-700 border border-blue-200">Evidence</span> <span class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-semibold bg-blue-100 text-blue-700 border border-blue-200">{{ __('app.blade_evidence') }}</span>
@else @else
<span class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-semibold bg-gray-100 text-gray-600 border border-gray-200">General</span> <span class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-semibold bg-gray-100 text-gray-600 border border-gray-200">{{ __('app.collection_general') }}</span>
@endif @endif
</div> </div>
</div> </div>

View File

@@ -19,19 +19,19 @@
<div class="px-6 py-4 border-b border-gray-100 bg-gray-50/50 flex justify-between items-center"> <div class="px-6 py-4 border-b border-gray-100 bg-gray-50/50 flex justify-between items-center">
<h3 class="font-semibold text-lg text-gray-900 flex items-center gap-2" style="font-family: 'Manrope', sans-serif;"> <h3 class="font-semibold text-lg text-gray-900 flex items-center gap-2" style="font-family: 'Manrope', sans-serif;">
<svg class="w-5 h-5 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg> <svg class="w-5 h-5 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg>
Similar Cases {{ __('app.blade_similar_cases') }}
</h3> </h3>
<span class="text-xs text-gray-500">Matching tags: {{ $record->tags->pluck('name')->implode(', ') }}</span> <span class="text-xs text-gray-500">{{ __('app.blade_matching_tags') }} {{ $record->tags->pluck('name')->implode(', ') }}</span>
</div> </div>
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="w-full text-left border-collapse"> <table class="w-full text-left border-collapse">
<thead> <thead>
<tr class="bg-gray-50 border-b border-gray-200"> <tr class="bg-gray-50 border-b border-gray-200">
<th class="px-6 py-3 text-xs font-semibold text-gray-500 uppercase tracking-wider">Title</th> <th class="px-6 py-3 text-xs font-semibold text-gray-500 uppercase tracking-wider">{{ __('app.blade_title') }}</th>
<th class="px-6 py-3 text-xs font-semibold text-gray-500 uppercase tracking-wider">Customer</th> <th class="px-6 py-3 text-xs font-semibold text-gray-500 uppercase tracking-wider">{{ __('app.blade_customer') }}</th>
<th class="px-6 py-3 text-xs font-semibold text-gray-500 uppercase tracking-wider">Status</th> <th class="px-6 py-3 text-xs font-semibold text-gray-500 uppercase tracking-wider">{{ __('app.blade_status') }}</th>
<th class="px-6 py-3 text-xs font-semibold text-gray-500 uppercase tracking-wider">Handler</th> <th class="px-6 py-3 text-xs font-semibold text-gray-500 uppercase tracking-wider">{{ __('app.blade_handler') }}</th>
<th class="px-6 py-3 text-xs font-semibold text-gray-500 uppercase tracking-wider">Date</th> <th class="px-6 py-3 text-xs font-semibold text-gray-500 uppercase tracking-wider">{{ __('app.blade_date') }}</th>
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-gray-100"> <tbody class="divide-y divide-gray-100">
@@ -47,21 +47,21 @@
<td class="px-6 py-4"> <td class="px-6 py-4">
@switch($case->status) @switch($case->status)
@case('pending') @case('pending')
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-semibold bg-amber-50 text-amber-700 border border-amber-200">Pending</span> <span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-semibold bg-amber-50 text-amber-700 border border-amber-200">{{ __('app.status_pending') }}</span>
@break @break
@case('processing') @case('processing')
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-semibold bg-blue-50 text-blue-700 border border-blue-200">Processing</span> <span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-semibold bg-blue-50 text-blue-700 border border-blue-200">{{ __('app.status_processing') }}</span>
@break @break
@case('resolved') @case('resolved')
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-semibold bg-green-50 text-green-700 border border-green-200">Resolved</span> <span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-semibold bg-green-50 text-green-700 border border-green-200">{{ __('app.status_resolved') }}</span>
@break @break
@case('closed') @case('closed')
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-semibold bg-gray-100 text-gray-600 border border-gray-200">Closed</span> <span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-semibold bg-gray-100 text-gray-600 border border-gray-200">{{ __('app.status_closed') }}</span>
@break @break
@endswitch @endswitch
</td> </td>
<td class="px-6 py-4 text-sm text-gray-600"> <td class="px-6 py-4 text-sm text-gray-600">
{{ $case->assignedTo?->name ?? 'Unassigned' }} {{ $case->assignedTo?->name ?? __('app.unassigned') }}
</td> </td>
<td class="px-6 py-4 text-sm text-gray-500"> <td class="px-6 py-4 text-sm text-gray-500">
{{ $case->created_at->format('d/m/Y') }} {{ $case->created_at->format('d/m/Y') }}

417
webappUI/dashboard.html Normal file
View File

@@ -0,0 +1,417 @@
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dashboard — AfterSales CRM</title>
<link rel="stylesheet" href="shared.css">
</head>
<body>
<div class="app-layout">
<!-- Sidebar -->
<aside class="sidebar">
<div class="sidebar-brand">
<div class="sidebar-brand-icon">
<span class="material-symbols-outlined">support_agent</span>
</div>
<span class="sidebar-brand-text">AfterSales</span>
<span class="sidebar-brand-badge">CRM</span>
</div>
<nav class="sidebar-nav">
<div class="sidebar-group">
<div class="sidebar-group-label">Main</div>
<a class="sidebar-item active" href="dashboard.html">
<span class="sidebar-item-icon material-symbols-outlined">dashboard</span>
Dashboard
</a>
</div>
<div class="sidebar-group">
<div class="sidebar-group-label">Customer Care</div>
<a class="sidebar-item" href="feedback-list.html">
<span class="sidebar-item-icon material-symbols-outlined">chat_bubble_left_right</span>
Feedbacks
<span class="sidebar-item-badge">8</span>
</a>
</div>
<div class="sidebar-group">
<div class="sidebar-group-label">Management</div>
<a class="sidebar-item" href="#">
<span class="sidebar-item-icon material-symbols-outlined">building_storefront</span>
Products
</a>
<a class="sidebar-item" href="#">
<span class="sidebar-item-icon material-symbols-outlined">group</span>
Customers
</a>
<a class="sidebar-item" href="#">
<span class="sidebar-item-icon material-symbols-outlined">inbox_stack</span>
Channels
</a>
<a class="sidebar-item" href="#">
<span class="sidebar-item-icon material-symbols-outlined">description</span>
Contracts
</a>
<a class="sidebar-item" href="#">
<span class="sidebar-item-icon material-symbols-outlined">label</span>
Tags
</a>
</div>
</nav>
<div class="sidebar-footer">
<div class="sidebar-user">
<div class="sidebar-user-avatar">AD</div>
<div class="sidebar-user-info">
<div class="sidebar-user-name">Admin User</div>
<div class="sidebar-user-role">admin</div>
</div>
<span class="material-symbols-outlined" style="font-size:20px;color:var(--crm-text-tertiary)">unfold_more</span>
</div>
</div>
</aside>
<!-- Main Content -->
<main class="main-content">
<!-- Top Bar -->
<header class="topbar">
<div class="topbar-left">
<div class="breadcrumb">
<a href="#">Home</a>
<span class="breadcrumb-sep material-symbols-outlined">chevron_right</span>
<span class="breadcrumb-current">Dashboard</span>
</div>
</div>
<div class="topbar-right">
<button class="topbar-btn">
<span class="material-symbols-outlined">search</span>
</button>
<button class="topbar-btn">
<span class="material-symbols-outlined">notifications</span>
<span class="topbar-btn-badge"></span>
</button>
<button class="topbar-btn">
<span class="material-symbols-outlined">settings</span>
</button>
</div>
</header>
<!-- Page Content -->
<div class="page-content">
<div class="page-header">
<h1 class="page-title">Dashboard</h1>
<p class="page-subtitle">Overview of customer care operations</p>
</div>
<!-- Stats Grid -->
<div class="stats-grid">
<div class="stat-card stat-card--success">
<div class="stat-card-header">
<div class="stat-card-icon">
<span class="material-symbols-outlined">check_circle</span>
</div>
<div class="stat-card-trend stat-card-trend--up">
<span class="material-symbols-outlined" style="font-size:14px">trending_up</span>
+12%
</div>
</div>
<div class="stat-card-value">42</div>
<div class="stat-card-label">Resolved Tickets</div>
</div>
<div class="stat-card stat-card--info">
<div class="stat-card-header">
<div class="stat-card-icon">
<span class="material-symbols-outlined">schedule</span>
</div>
<div class="stat-card-trend stat-card-trend--down">
<span class="material-symbols-outlined" style="font-size:14px">trending_down</span>
-8%
</div>
</div>
<div class="stat-card-value">2.5h</div>
<div class="stat-card-label">Avg Processing Time</div>
</div>
<div class="stat-card stat-card--warning">
<div class="stat-card-header">
<div class="stat-card-icon">
<span class="material-symbols-outlined">pending</span>
</div>
<div class="stat-card-trend stat-card-trend--up">
<span class="material-symbols-outlined" style="font-size:14px">trending_up</span>
+3
</div>
</div>
<div class="stat-card-value">8</div>
<div class="stat-card-label">Pending Tickets</div>
</div>
<div class="stat-card stat-card--danger">
<div class="stat-card-header">
<div class="stat-card-icon">
<span class="material-symbols-outlined">warning</span>
</div>
</div>
<div class="stat-card-value">3</div>
<div class="stat-card-label">Escalated</div>
</div>
</div>
<!-- 2 Column Layout -->
<div style="display:grid; grid-template-columns: 1.2fr 1fr; gap:24px;">
<!-- Chart -->
<div class="card">
<div class="card-header">
<div class="card-title">
<div class="card-title-icon">
<span class="material-symbols-outlined">bar_chart</span>
</div>
Avg Processing Time by Handler
</div>
<div class="btn-group">
<button class="btn btn--ghost btn--sm">Week</button>
<button class="btn btn--secondary btn--sm">Month</button>
</div>
</div>
<div class="card-body">
<div class="chart-placeholder" style="height:240px; padding-bottom:40px;">
<div class="chart-bar" style="height:65%; background: linear-gradient(180deg, #3b82f6, #60a5fa);">
<span class="chart-bar-value">45m</span>
<span class="chart-bar-label">Staff A</span>
</div>
<div class="chart-bar" style="height:85%; background: linear-gradient(180deg, #3b82f6, #60a5fa);">
<span class="chart-bar-value">62m</span>
<span class="chart-bar-label">Staff B</span>
</div>
<div class="chart-bar" style="height:45%; background: linear-gradient(180deg, #3b82f6, #60a5fa);">
<span class="chart-bar-value">31m</span>
<span class="chart-bar-label">Staff C</span>
</div>
<div class="chart-bar" style="height:70%; background: linear-gradient(180deg, #3b82f6, #60a5fa);">
<span class="chart-bar-value">52m</span>
<span class="chart-bar-label">Staff D</span>
</div>
<div class="chart-bar" style="height:55%; background: linear-gradient(180deg, #3b82f6, #60a5fa);">
<span class="chart-bar-value">38m</span>
<span class="chart-bar-label">Staff E</span>
</div>
<div class="chart-bar" style="height:90%; background: linear-gradient(180deg, #f59e0b, #fbbf24);">
<span class="chart-bar-value">68m</span>
<span class="chart-bar-label">Staff F</span>
</div>
</div>
</div>
</div>
<!-- Resolved Awaiting Closure -->
<div class="card">
<div class="card-header">
<div class="card-title">
<div class="card-title-icon" style="background:var(--crm-resolved-bg); color:var(--crm-resolved);">
<span class="material-symbols-outlined">task_alt</span>
</div>
Resolved — Awaiting Closure
</div>
<span class="badge badge--resolved badge--dot">5 tickets</span>
</div>
<div class="card-body card-body--compact">
<table>
<thead>
<tr>
<th>Ticket</th>
<th>Customer</th>
<th>Dept</th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<td><a href="feedback-detail.html" class="table-link">#12 Leak issue</a></td>
<td>Nguyễn A</td>
<td><span class="badge badge--processing" style="font-size:11px;">CSKH</span></td>
<td class="text-right"><span class="material-symbols-outlined" style="font-size:18px;color:var(--crm-text-tertiary)">chevron_right</span></td>
</tr>
<tr>
<td><a href="#" class="table-link">#8 AC broken</a></td>
<td>Trần B</td>
<td><span class="badge badge--processing" style="font-size:11px;">Kỹ thuật</span></td>
<td class="text-right"><span class="material-symbols-outlined" style="font-size:18px;color:var(--crm-text-tertiary)">chevron_right</span></td>
</tr>
<tr>
<td><a href="#" class="table-link">#5 Door fix</a></td>
<td>Lê C</td>
<td><span class="badge badge--processing" style="font-size:11px;">CSKH</span></td>
<td class="text-right"><span class="material-symbols-outlined" style="font-size:18px;color:var(--crm-text-tertiary)">chevron_right</span></td>
</tr>
<tr>
<td><a href="#" class="table-link">#3 Paint crack</a></td>
<td>Phạm D</td>
<td><span class="badge badge--processing" style="font-size:11px;">Kỹ thuật</span></td>
<td class="text-right"><span class="material-symbols-outlined" style="font-size:18px;color:var(--crm-text-tertiary)">chevron_right</span></td>
</tr>
<tr>
<td><a href="#" class="table-link">#1 Window leak</a></td>
<td>Hoàng E</td>
<td><span class="badge badge--processing" style="font-size:11px;">CSKH</span></td>
<td class="text-right"><span class="material-symbols-outlined" style="font-size:18px;color:var(--crm-text-tertiary)">chevron_right</span></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- Recent Feedbacks -->
<div class="card mt-6">
<div class="card-header">
<div class="card-title">
<div class="card-title-icon">
<span class="material-symbols-outlined">history</span>
</div>
Recent Feedbacks
</div>
<a href="feedback-list.html" class="btn btn--secondary btn--sm">
View All
<span class="material-symbols-outlined" style="font-size:16px">arrow_forward</span>
</a>
</div>
<div class="card-body card-body--compact">
<div class="table-container">
<table>
<thead>
<tr>
<th>Title</th>
<th>Customer</th>
<th>Product</th>
<th>Contract</th>
<th>Channel</th>
<th>Status</th>
<th>Department</th>
<th>Assigned To</th>
<th>Created</th>
</tr>
</thead>
<tbody>
<tr>
<td><a href="feedback-detail.html" class="table-link">Leak in bathroom</a></td>
<td>
<div class="table-meta">
<div class="table-avatar table-avatar--blue">NA</div>
Nguyễn A
</div>
</td>
<td>Sunrise A1</td>
<td><span class="badge badge--sale">Sale</span></td>
<td><span style="display:inline-flex;align-items:center;gap:4px;"><span class="material-symbols-outlined" style="font-size:16px;color:#0066ff">chat</span> Zalo</span></td>
<td><span class="badge badge--pending badge--dot">Pending</span></td>
<td>CSKH</td>
<td>
<div class="table-meta">
<div class="table-avatar table-avatar--green">ST</div>
Staff User
</div>
</td>
<td style="color:var(--crm-text-tertiary); font-size:13px;">2 hours ago</td>
</tr>
<tr>
<td><a href="#" class="table-link">AC not working</a></td>
<td>
<div class="table-meta">
<div class="table-avatar table-avatar--purple">TB</div>
Trần B
</div>
</td>
<td>Green Valley</td>
<td><span class="badge badge--lease">Lease</span></td>
<td><span style="display:inline-flex;align-items:center;gap:4px;"><span class="material-symbols-outlined" style="font-size:16px;color:#ea4335">mail</span> Email</span></td>
<td><span class="badge badge--processing badge--dot">Processing</span></td>
<td>Kỹ thuật</td>
<td>
<div class="table-meta">
<div class="table-avatar table-avatar--amber">MG</div>
Manager User
</div>
</td>
<td style="color:var(--crm-text-tertiary); font-size:13px;">5 hours ago</td>
</tr>
<tr>
<td>
<div style="display:flex;align-items:center;gap:8px;">
<a href="#" class="table-link">Door lock broken</a>
<span class="material-symbols-outlined" style="font-size:16px;color:var(--crm-escalated)" title="Escalated">priority_high</span>
</div>
</td>
<td>
<div class="table-meta">
<div class="table-avatar table-avatar--blue">LC</div>
Lê C
</div>
</td>
<td>Ocean Tower</td>
<td><span class="badge badge--bcc">BCC</span></td>
<td><span style="display:inline-flex;align-items:center;gap:4px;"><span class="material-symbols-outlined" style="font-size:16px;color:#34a853">phone</span> Phone</span></td>
<td><span class="badge badge--escalated badge--dot">Escalated</span></td>
<td>CSKH</td>
<td>
<div class="table-meta">
<div class="table-avatar table-avatar--green">ST</div>
Staff User
</div>
</td>
<td style="color:var(--crm-text-tertiary); font-size:13px;">1 day ago</td>
</tr>
<tr>
<td><a href="#" class="table-link">Paint crack in living room</a></td>
<td>
<div class="table-meta">
<div class="table-avatar table-avatar--amber">PD</div>
Phạm D
</div>
</td>
<td>Park Hill</td>
<td><span class="badge badge--sale">Sale</span></td>
<td><span style="display:inline-flex;align-items:center;gap:4px;"><span class="material-symbols-outlined" style="font-size:16px;color:#6b7280">description</span> Document</span></td>
<td><span class="badge badge--resolved badge--dot">Resolved</span></td>
<td>Kỹ thuật</td>
<td>
<div class="table-meta">
<div class="table-avatar table-avatar--purple">AD</div>
Admin User
</div>
</td>
<td style="color:var(--crm-text-tertiary); font-size:13px;">3 days ago</td>
</tr>
<tr>
<td><a href="#" class="table-link">Window seal broken</a></td>
<td>
<div class="table-meta">
<div class="table-avatar table-avatar--blue">HE</div>
Hoàng E
</div>
</td>
<td>Sunrise A2</td>
<td><span class="badge badge--lease">Lease</span></td>
<td><span style="display:inline-flex;align-items:center;gap:4px;"><span class="material-symbols-outlined" style="font-size:16px;color:#9c27b0">person</span> In Person</span></td>
<td><span class="badge badge--closed badge--dot">Closed</span></td>
<td>CSKH</td>
<td>
<div class="table-meta">
<div class="table-avatar table-avatar--green">ST</div>
Staff User
</div>
</td>
<td style="color:var(--crm-text-tertiary); font-size:13px;">1 week ago</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</main>
</div>
</body>
</html>

View File

@@ -0,0 +1,602 @@
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Ticket #12 — AfterSales CRM</title>
<link rel="stylesheet" href="shared.css">
</head>
<body>
<div class="app-layout">
<!-- Sidebar -->
<aside class="sidebar">
<div class="sidebar-brand">
<div class="sidebar-brand-icon">
<span class="material-symbols-outlined">support_agent</span>
</div>
<span class="sidebar-brand-text">AfterSales</span>
<span class="sidebar-brand-badge">CRM</span>
</div>
<nav class="sidebar-nav">
<div class="sidebar-group">
<div class="sidebar-group-label">Main</div>
<a class="sidebar-item" href="dashboard.html">
<span class="sidebar-item-icon material-symbols-outlined">dashboard</span>
Dashboard
</a>
</div>
<div class="sidebar-group">
<div class="sidebar-group-label">Customer Care</div>
<a class="sidebar-item active" href="feedback-list.html">
<span class="sidebar-item-icon material-symbols-outlined">chat_bubble_left_right</span>
Feedbacks
<span class="sidebar-item-badge">8</span>
</a>
</div>
<div class="sidebar-group">
<div class="sidebar-group-label">Management</div>
<a class="sidebar-item" href="#">
<span class="sidebar-item-icon material-symbols-outlined">building_storefront</span>
Products
</a>
<a class="sidebar-item" href="#">
<span class="sidebar-item-icon material-symbols-outlined">group</span>
Customers
</a>
<a class="sidebar-item" href="#">
<span class="sidebar-item-icon material-symbols-outlined">inbox_stack</span>
Channels
</a>
<a class="sidebar-item" href="#">
<span class="sidebar-item-icon material-symbols-outlined">description</span>
Contracts
</a>
<a class="sidebar-item" href="#">
<span class="sidebar-item-icon material-symbols-outlined">label</span>
Tags
</a>
</div>
</nav>
<div class="sidebar-footer">
<div class="sidebar-user">
<div class="sidebar-user-avatar">AD</div>
<div class="sidebar-user-info">
<div class="sidebar-user-name">Admin User</div>
<div class="sidebar-user-role">admin</div>
</div>
<span class="material-symbols-outlined" style="font-size:20px;color:var(--crm-text-tertiary)">unfold_more</span>
</div>
</div>
</aside>
<!-- Main Content -->
<main class="main-content">
<header class="topbar">
<div class="topbar-left">
<div class="breadcrumb">
<a href="dashboard.html">Home</a>
<span class="breadcrumb-sep material-symbols-outlined">chevron_right</span>
<a href="feedback-list.html" style="color:var(--crm-text-tertiary); text-decoration:none;">Feedbacks</a>
<span class="breadcrumb-sep material-symbols-outlined">chevron_right</span>
<span class="breadcrumb-current">#12 Leak in bathroom</span>
</div>
</div>
<div class="topbar-right">
<button class="topbar-btn">
<span class="material-symbols-outlined">search</span>
</button>
<button class="topbar-btn">
<span class="material-symbols-outlined">notifications</span>
<span class="topbar-btn-badge"></span>
</button>
<button class="topbar-btn">
<span class="material-symbols-outlined">settings</span>
</button>
</div>
</header>
<div class="page-content">
<!-- Page Header -->
<div style="display:flex; align-items:flex-start; justify-content:space-between; margin-bottom:28px;">
<div>
<div style="display:flex; align-items:center; gap:12px; margin-bottom:8px;">
<a href="feedback-list.html" class="btn btn--ghost btn--sm" style="padding:6px;">
<span class="material-symbols-outlined">arrow_back</span>
</a>
<h1 class="page-title" style="margin:0;">Leak in bathroom</h1>
<span class="badge badge--pending badge--dot">Pending</span>
<span class="badge badge--escalated" style="font-size:11px;">
<span class="material-symbols-outlined" style="font-size:14px;">warning</span>
Escalated
</span>
</div>
<div style="display:flex; align-items:center; gap:16px; margin-left:52px; font-size:13px; color:var(--crm-text-secondary);">
<span>Ticket <strong>#12</strong></span>
<span></span>
<span>Created <strong>2 hours ago</strong></span>
<span></span>
<span>By <strong>Nguyễn A</strong></span>
<span></span>
<span>Via <strong>Zalo</strong></span>
</div>
</div>
<div class="btn-group">
<button class="btn btn--secondary btn--sm">
<span class="material-symbols-outlined" style="font-size:16px;">edit</span>
Edit
</button>
<button class="btn btn--secondary btn--sm">
<span class="material-symbols-outlined" style="font-size:16px;">print</span>
Print
</button>
</div>
</div>
<!-- Main Grid -->
<div class="detail-grid">
<!-- Left Column -->
<div class="detail-main">
<!-- Feedback Content -->
<div class="card mb-6">
<div class="card-header">
<div class="card-title">
<div class="card-title-icon">
<span class="material-symbols-outlined">description</span>
</div>
Feedback Details
</div>
</div>
<div class="card-body">
<div style="margin-bottom:20px;">
<div style="font-size:13px; font-weight:600; color:var(--crm-text-tertiary); text-transform:uppercase; letter-spacing:0.04em; margin-bottom:8px;">Content</div>
<div style="font-size:15px; line-height:1.7; color:var(--crm-text-primary); background:var(--crm-surface-dim); padding:16px 20px; border-radius:var(--crm-radius-md); border-left:3px solid var(--crm-primary);">
Water is leaking from the bathroom ceiling. The leak started 3 days ago and has gotten worse. There's water damage on the ceiling and water dripping onto the floor. I've placed a bucket but need urgent repair. The apartment is Sunrise A1, Floor 12, Unit 1205.
</div>
</div>
<div style="display:grid; grid-template-columns:1fr 1fr; gap:20px;">
<div>
<div style="font-size:13px; font-weight:600; color:var(--crm-text-tertiary); text-transform:uppercase; letter-spacing:0.04em; margin-bottom:8px;">Customer</div>
<div style="display:flex; align-items:center; gap:10px; padding:10px 14px; background:var(--crm-surface-dim); border-radius:var(--crm-radius-md);">
<div class="table-avatar table-avatar--blue" style="width:36px;height:36px;font-size:13px;">NA</div>
<div>
<div style="font-size:14px; font-weight:600; color:var(--crm-text-primary);">Nguyễn A</div>
<div style="font-size:12px; color:var(--crm-text-tertiary);">nguyen.a@email.com</div>
</div>
</div>
</div>
<div>
<div style="font-size:13px; font-weight:600; color:var(--crm-text-tertiary); text-transform:uppercase; letter-spacing:0.04em; margin-bottom:8px;">Product</div>
<div style="display:flex; align-items:center; gap:10px; padding:10px 14px; background:var(--crm-surface-dim); border-radius:var(--crm-radius-md);">
<div style="width:36px;height:36px;border-radius:var(--crm-radius-sm);background:linear-gradient(135deg,#3b82f6,#6366f1);display:flex;align-items:center;justify-content:center;">
<span class="material-symbols-outlined" style="font-size:20px;color:white;">apartment</span>
</div>
<div>
<div style="font-size:14px; font-weight:600; color:var(--crm-text-primary);">Sunrise A1</div>
<div style="font-size:12px; color:var(--crm-text-tertiary);">Floor 12, Unit 1205</div>
</div>
</div>
</div>
</div>
<div style="margin-top:20px;">
<div style="font-size:13px; font-weight:600; color:var(--crm-text-tertiary); text-transform:uppercase; letter-spacing:0.04em; margin-bottom:8px;">Tags</div>
<div style="display:flex; gap:8px; flex-wrap:wrap;">
<span class="badge" style="background:#ede9fe;color:#7c3aed;border:1px solid #c4b5fd;">Plumbing</span>
<span class="badge" style="background:#fee2e2;color:#991b1b;border:1px solid #fecaca;">Urgent</span>
<span class="badge" style="background:#dbeafe;color:#1e40af;border:1px solid #bfdbfe;">Water Damage</span>
</div>
</div>
</div>
</div>
<!-- Interaction History -->
<div class="card mb-6">
<div class="card-header">
<div class="card-title">
<div class="card-title-icon" style="background:var(--crm-processing-bg); color:var(--crm-processing);">
<span class="material-symbols-outlined">forum</span>
</div>
Interaction History
</div>
<button class="btn btn--primary btn--sm">
<span class="material-symbols-outlined" style="font-size:16px;">add</span>
Add Note
</button>
</div>
<div class="card-body">
<div class="timeline">
<!-- Note -->
<div class="timeline-item">
<div class="timeline-dot timeline-dot--note">
<span class="material-symbols-outlined">edit_note</span>
</div>
<div class="timeline-content">
<div class="timeline-header">
<span class="timeline-author">Staff User</span>
<span class="timeline-type">Note</span>
<span class="timeline-time">2 hours ago</span>
</div>
<div class="timeline-body">
Contacted customer to schedule inspection. Customer available tomorrow 2-4 PM. Will send maintenance team to check the ceiling leak. Need to coordinate with building management for access to the unit above.
</div>
<a href="#" class="timeline-attachment">
<span class="material-symbols-outlined">attach_file</span>
inspection_photo.jpg (2.3 MB)
</a>
</div>
</div>
<!-- Status Change -->
<div class="timeline-item">
<div class="timeline-dot timeline-dot--status">
<span class="material-symbols-outlined">sync</span>
</div>
<div class="timeline-content">
<div class="timeline-header">
<span class="timeline-author">Manager User</span>
<span class="timeline-type">Status Change</span>
<span class="timeline-time">5 hours ago</span>
</div>
<div class="timeline-body">
<div style="display:flex; align-items:center; gap:8px;">
<span class="badge badge--pending" style="font-size:11px;">Pending</span>
<span class="material-symbols-outlined" style="font-size:16px; color:var(--crm-text-tertiary);">arrow_forward</span>
<span class="badge badge--processing" style="font-size:11px;">Processing</span>
</div>
</div>
</div>
</div>
<!-- Assignment -->
<div class="timeline-item">
<div class="timeline-dot timeline-dot--assignment">
<span class="material-symbols-outlined">person_add</span>
</div>
<div class="timeline-content">
<div class="timeline-header">
<span class="timeline-author">Manager User</span>
<span class="timeline-type">Assignment</span>
<span class="timeline-time">5 hours ago</span>
</div>
<div class="timeline-body">
Assigned to <strong>Staff User</strong> for handling
</div>
</div>
</div>
<!-- Transfer -->
<div class="timeline-item">
<div class="timeline-dot timeline-dot--transfer">
<span class="material-symbols-outlined">swap_horiz</span>
</div>
<div class="timeline-content">
<div class="timeline-header">
<span class="timeline-author">Admin User</span>
<span class="timeline-type">Transfer</span>
<span class="timeline-time">1 day ago</span>
</div>
<div class="timeline-body">
<div style="display:flex; align-items:center; gap:8px; margin-bottom:8px;">
<span class="badge" style="background:var(--crm-processing-bg); color:var(--crm-processing); border:1px solid var(--crm-processing-border);">Kỹ thuật</span>
<span class="material-symbols-outlined" style="font-size:16px; color:var(--crm-text-tertiary);">arrow_forward</span>
<span class="badge" style="background:var(--crm-resolved-bg); color:var(--crm-resolved); border:1px solid var(--crm-resolved-border);">CSKH</span>
</div>
<div style="font-size:13px; color:var(--crm-text-secondary); font-style:italic;">
"Đã kiểm tra kỹ thuật, cần CSKH liên hệ khách hàng để sắp lịch sửa chữa"
</div>
</div>
</div>
</div>
<!-- Initial Note -->
<div class="timeline-item">
<div class="timeline-dot timeline-dot--note">
<span class="material-symbols-outlined">edit_note</span>
</div>
<div class="timeline-content">
<div class="timeline-header">
<span class="timeline-author">System</span>
<span class="timeline-type">Created</span>
<span class="timeline-time">2 days ago</span>
</div>
<div class="timeline-body">
Feedback created via <strong>Zalo</strong> channel. Auto-assigned to <strong>CSKH</strong> department.
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Attachments -->
<div class="card mb-6">
<div class="card-header">
<div class="card-title">
<div class="card-title-icon" style="background:var(--crm-accent-light); color:var(--crm-accent);">
<span class="material-symbols-outlined">attach_file</span>
</div>
Attachments
</div>
<span style="font-size:13px; color:var(--crm-text-tertiary);">3 files</span>
</div>
<div class="card-body card-body--compact">
<table>
<thead>
<tr>
<th>File</th>
<th>Collection</th>
<th>Size</th>
<th>Uploaded By</th>
<th>Date</th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<td>
<div style="display:flex; align-items:center; gap:10px;">
<div style="width:36px;height:36px;border-radius:var(--crm-radius-sm);background:#fee2e2;display:flex;align-items:center;justify-content:center;">
<span class="material-symbols-outlined" style="font-size:20px;color:#dc2626;">image</span>
</div>
<div>
<div style="font-size:14px; font-weight:600; color:var(--crm-text-primary);">inspection_photo.jpg</div>
<div style="font-size:12px; color:var(--crm-text-tertiary);">image/jpeg</div>
</div>
</div>
</td>
<td><span class="badge" style="background:var(--crm-surface-dim); color:var(--crm-text-secondary); border:1px solid var(--crm-border); font-size:11px;">customer_evidence</span></td>
<td style="font-size:13px; color:var(--crm-text-secondary);">2.3 MB</td>
<td>
<div class="table-meta">
<div class="table-avatar table-avatar--green" style="width:24px;height:24px;font-size:10px;">ST</div>
Staff User
</div>
</td>
<td style="font-size:13px; color:var(--crm-text-tertiary);">2h ago</td>
<td>
<button class="btn btn--ghost btn--sm" style="padding:4px;">
<span class="material-symbols-outlined" style="font-size:18px;">download</span>
</button>
</td>
</tr>
<tr>
<td>
<div style="display:flex; align-items:center; gap:10px;">
<div style="width:36px;height:36px;border-radius:var(--crm-radius-sm);background:#dbeafe;display:flex;align-items:center;justify-content:center;">
<span class="material-symbols-outlined" style="font-size:20px;color:#2563eb;">description</span>
</div>
<div>
<div style="font-size:14px; font-weight:600; color:var(--crm-text-primary);">repair_estimate.pdf</div>
<div style="font-size:12px; color:var(--crm-text-tertiary);">application/pdf</div>
</div>
</div>
</td>
<td><span class="badge" style="background:var(--crm-surface-dim); color:var(--crm-text-secondary); border:1px solid var(--crm-border); font-size:11px;">general</span></td>
<td style="font-size:13px; color:var(--crm-text-secondary);">1.1 MB</td>
<td>
<div class="table-meta">
<div class="table-avatar table-avatar--amber" style="width:24px;height:24px;font-size:10px;">MG</div>
Manager User
</div>
</td>
<td style="font-size:13px; color:var(--crm-text-tertiary);">1d ago</td>
<td>
<button class="btn btn--ghost btn--sm" style="padding:4px;">
<span class="material-symbols-outlined" style="font-size:18px;">download</span>
</button>
</td>
</tr>
<tr>
<td>
<div style="display:flex; align-items:center; gap:10px;">
<div style="width:36px;height:36px;border-radius:var(--crm-radius-sm);background:#ecfdf5;display:flex;align-items:center;justify-content:center;">
<span class="material-symbols-outlined" style="font-size:20px;color:#059669;">check_circle</span>
</div>
<div>
<div style="font-size:14px; font-weight:600; color:var(--crm-text-primary);">proof_fixed.jpg</div>
<div style="font-size:12px; color:var(--crm-text-tertiary);">image/jpeg</div>
</div>
</div>
</td>
<td><span class="badge" style="background:var(--crm-resolved-bg); color:var(--crm-resolved); border:1px solid var(--crm-resolved-border); font-size:11px;">proof_of_resolution</span></td>
<td style="font-size:13px; color:var(--crm-text-secondary);">3.8 MB</td>
<td>
<div class="table-meta">
<div class="table-avatar table-avatar--green" style="width:24px;height:24px;font-size:10px;">ST</div>
Staff User
</div>
</td>
<td style="font-size:13px; color:var(--crm-text-tertiary);">3d ago</td>
<td>
<button class="btn btn--ghost btn--sm" style="padding:4px;">
<span class="material-symbols-outlined" style="font-size:18px;">download</span>
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Transfer History -->
<div class="card">
<div class="card-header">
<div class="card-title">
<div class="card-title-icon" style="background:var(--crm-pending-bg); color:var(--crm-pending);">
<span class="material-symbols-outlined">swap_horiz</span>
</div>
Transfer History
</div>
</div>
<div class="card-body">
<div class="transfer-log">
<div class="transfer-item">
<span class="transfer-dept">Kỹ thuật</span>
<span class="transfer-arrow material-symbols-outlined">arrow_forward</span>
<span class="transfer-dept">CSKH</span>
<span style="font-size:13px; color:var(--crm-text-secondary);">— Admin User</span>
<div class="transfer-meta">
<div>"Đã kiểm tra kỹ thuật, cần CSKH liên hệ"</div>
<div style="margin-top:2px;">1 day ago</div>
</div>
</div>
<div class="transfer-item">
<span class="transfer-dept">CSKH</span>
<span class="transfer-arrow material-symbols-outlined">arrow_forward</span>
<span class="transfer-dept">Kỹ thuật</span>
<span style="font-size:13px; color:var(--crm-text-secondary);">— Manager User</span>
<div class="transfer-meta">
<div>"Cần kiểm tra kỹ thuật"</div>
<div style="margin-top:2px;">2 days ago</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Right Sidebar -->
<div class="detail-sidebar">
<!-- Actions -->
<div class="action-card">
<div class="action-card-title">
<span class="material-symbols-outlined">bolt</span>
Quick Actions
</div>
<button class="action-btn">
<span class="material-symbols-outlined">swap_horiz</span>
Transfer Department
</button>
<button class="action-btn">
<span class="material-symbols-outlined">check_circle</span>
Mark as Resolved
</button>
<button class="action-btn action-btn--danger">
<span class="material-symbols-outlined">lock</span>
Close Ticket
</button>
</div>
<!-- Ticket Info -->
<div class="action-card">
<div class="action-card-title">
<span class="material-symbols-outlined">info</span>
Ticket Information
</div>
<div class="info-list">
<div class="info-item">
<div class="info-item-icon">
<span class="material-symbols-outlined">sync</span>
</div>
<div class="info-item-content">
<div class="info-item-label">Status</div>
<div class="info-item-value"><span class="badge badge--pending badge--dot" style="font-size:12px;">Pending</span></div>
</div>
</div>
<div class="info-item">
<div class="info-item-icon">
<span class="material-symbols-outlined">apartment</span>
</div>
<div class="info-item-content">
<div class="info-item-label">Department</div>
<div class="info-item-value">CSKH</div>
</div>
</div>
<div class="info-item">
<div class="info-item-icon">
<span class="material-symbols-outlined">person</span>
</div>
<div class="info-item-content">
<div class="info-item-label">Assigned To</div>
<div class="info-item-value">
<div style="display:flex; align-items:center; gap:8px;">
<div class="table-avatar table-avatar--green" style="width:24px;height:24px;font-size:10px;">ST</div>
Staff User
</div>
</div>
</div>
</div>
<div class="info-item">
<div class="info-item-icon">
<span class="material-symbols-outlined">description</span>
</div>
<div class="info-item-content">
<div class="info-item-label">Contract</div>
<div class="info-item-value"><span class="badge badge--sale" style="font-size:12px;">Sale</span> Active</div>
</div>
</div>
<div class="info-item">
<div class="info-item-icon">
<span class="material-symbols-outlined">inbox_stack</span>
</div>
<div class="info-item-content">
<div class="info-item-label">Channel</div>
<div class="info-item-value" style="display:flex; align-items:center; gap:6px;">
<span class="material-symbols-outlined" style="font-size:16px; color:#0066ff;">chat</span>
Zalo
</div>
</div>
</div>
<div class="info-item">
<div class="info-item-icon">
<span class="material-symbols-outlined">warning</span>
</div>
<div class="info-item-content">
<div class="info-item-label">Escalated</div>
<div class="info-item-value"><span style="color:var(--crm-escalated); font-weight:600;">Yes</span></div>
</div>
</div>
<div class="info-item">
<div class="info-item-icon">
<span class="material-symbols-outlined">calendar_today</span>
</div>
<div class="info-item-content">
<div class="info-item-label">Created</div>
<div class="info-item-value">Jan 15, 2026 10:30 AM</div>
</div>
</div>
<div class="info-item">
<div class="info-item-icon">
<span class="material-symbols-outlined">update</span>
</div>
<div class="info-item-content">
<div class="info-item-label">Last Updated</div>
<div class="info-item-value">Jan 15, 2026 12:45 PM</div>
</div>
</div>
</div>
</div>
<!-- Related Feedbacks -->
<div class="action-card">
<div class="action-card-title">
<span class="material-symbols-outlined">lightbulb</span>
Similar Cases
</div>
<div style="display:flex; flex-direction:column; gap:10px;">
<a href="#" style="display:block; padding:10px 12px; background:var(--crm-surface-dim); border-radius:var(--crm-radius-sm); border:1px solid var(--crm-border); text-decoration:none; transition:all 0.15s;">
<div style="font-size:13px; font-weight:600; color:var(--crm-primary);">Water leak in kitchen</div>
<div style="font-size:12px; color:var(--crm-text-tertiary); margin-top:2px;">Sunrise A2 • Resolved • 2 weeks ago</div>
</a>
<a href="#" style="display:block; padding:10px 12px; background:var(--crm-surface-dim); border-radius:var(--crm-radius-sm); border:1px solid var(--crm-border); text-decoration:none; transition:all 0.15s;">
<div style="font-size:13px; font-weight:600; color:var(--crm-primary);">Ceiling water damage</div>
<div style="font-size:12px; color:var(--crm-text-tertiary); margin-top:2px;">Green Valley • Closed • 1 month ago</div>
</a>
<a href="#" style="display:block; padding:10px 12px; background:var(--crm-surface-dim); border-radius:var(--crm-radius-sm); border:1px solid var(--crm-border); text-decoration:none; transition:all 0.15s;">
<div style="font-size:13px; font-weight:600; color:var(--crm-primary);">Bathroom pipe burst</div>
<div style="font-size:12px; color:var(--crm-text-tertiary); margin-top:2px;">Ocean Tower • Resolved • 3 weeks ago</div>
</a>
</div>
</div>
</div>
</div>
</div>
</main>
</div>
</body>
</html>

429
webappUI/feedback-list.html Normal file
View File

@@ -0,0 +1,429 @@
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Feedbacks — AfterSales CRM</title>
<link rel="stylesheet" href="shared.css">
</head>
<body>
<div class="app-layout">
<!-- Sidebar -->
<aside class="sidebar">
<div class="sidebar-brand">
<div class="sidebar-brand-icon">
<span class="material-symbols-outlined">support_agent</span>
</div>
<span class="sidebar-brand-text">AfterSales</span>
<span class="sidebar-brand-badge">CRM</span>
</div>
<nav class="sidebar-nav">
<div class="sidebar-group">
<div class="sidebar-group-label">Main</div>
<a class="sidebar-item" href="dashboard.html">
<span class="sidebar-item-icon material-symbols-outlined">dashboard</span>
Dashboard
</a>
</div>
<div class="sidebar-group">
<div class="sidebar-group-label">Customer Care</div>
<a class="sidebar-item active" href="feedback-list.html">
<span class="sidebar-item-icon material-symbols-outlined">chat_bubble_left_right</span>
Feedbacks
<span class="sidebar-item-badge">8</span>
</a>
</div>
<div class="sidebar-group">
<div class="sidebar-group-label">Management</div>
<a class="sidebar-item" href="#">
<span class="sidebar-item-icon material-symbols-outlined">building_storefront</span>
Products
</a>
<a class="sidebar-item" href="#">
<span class="sidebar-item-icon material-symbols-outlined">group</span>
Customers
</a>
<a class="sidebar-item" href="#">
<span class="sidebar-item-icon material-symbols-outlined">inbox_stack</span>
Channels
</a>
<a class="sidebar-item" href="#">
<span class="sidebar-item-icon material-symbols-outlined">description</span>
Contracts
</a>
<a class="sidebar-item" href="#">
<span class="sidebar-item-icon material-symbols-outlined">label</span>
Tags
</a>
</div>
</nav>
<div class="sidebar-footer">
<div class="sidebar-user">
<div class="sidebar-user-avatar">AD</div>
<div class="sidebar-user-info">
<div class="sidebar-user-name">Admin User</div>
<div class="sidebar-user-role">admin</div>
</div>
<span class="material-symbols-outlined" style="font-size:20px;color:var(--crm-text-tertiary)">unfold_more</span>
</div>
</div>
</aside>
<!-- Main Content -->
<main class="main-content">
<header class="topbar">
<div class="topbar-left">
<div class="breadcrumb">
<a href="dashboard.html">Home</a>
<span class="breadcrumb-sep material-symbols-outlined">chevron_right</span>
<span style="color:var(--crm-text-tertiary)">Customer Care</span>
<span class="breadcrumb-sep material-symbols-outlined">chevron_right</span>
<span class="breadcrumb-current">Feedbacks</span>
</div>
</div>
<div class="topbar-right">
<button class="topbar-btn">
<span class="material-symbols-outlined">search</span>
</button>
<button class="topbar-btn">
<span class="material-symbols-outlined">notifications</span>
<span class="topbar-btn-badge"></span>
</button>
<button class="topbar-btn">
<span class="material-symbols-outlined">settings</span>
</button>
</div>
</header>
<div class="page-content">
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:24px;">
<div>
<h1 class="page-title">Feedbacks</h1>
<p class="page-subtitle">Manage customer feedback and support tickets</p>
</div>
<button class="btn btn--primary">
<span class="material-symbols-outlined">add</span>
New Feedback
</button>
</div>
<!-- Quick Stats -->
<div style="display:flex; gap:16px; margin-bottom:24px;">
<div style="display:flex; align-items:center; gap:8px; padding:8px 16px; background:var(--crm-surface); border:1px solid var(--crm-border); border-radius:var(--crm-radius-full);">
<span class="material-symbols-outlined" style="font-size:18px; color:var(--crm-text-tertiary)">list</span>
<span style="font-size:13px; font-weight:600; color:var(--crm-text-primary);">All</span>
<span style="font-size:12px; font-weight:700; background:var(--crm-primary-light); color:var(--crm-primary); padding:1px 8px; border-radius:var(--crm-radius-full);">24</span>
</div>
<div style="display:flex; align-items:center; gap:8px; padding:8px 16px; background:var(--crm-surface); border:1px solid var(--crm-border); border-radius:var(--crm-radius-full); cursor:pointer;">
<span class="material-symbols-outlined" style="font-size:18px; color:var(--crm-pending)">schedule</span>
<span style="font-size:13px; font-weight:500; color:var(--crm-text-secondary);">Pending</span>
<span style="font-size:12px; font-weight:700; background:var(--crm-pending-bg); color:var(--crm-pending); padding:1px 8px; border-radius:var(--crm-radius-full);">8</span>
</div>
<div style="display:flex; align-items:center; gap:8px; padding:8px 16px; background:var(--crm-surface); border:1px solid var(--crm-border); border-radius:var(--crm-radius-full); cursor:pointer;">
<span class="material-symbols-outlined" style="font-size:18px; color:var(--crm-processing)">autorenew</span>
<span style="font-size:13px; font-weight:500; color:var(--crm-text-secondary);">Processing</span>
<span style="font-size:12px; font-weight:700; background:var(--crm-processing-bg); color:var(--crm-processing); padding:1px 8px; border-radius:var(--crm-radius-full);">6</span>
</div>
<div style="display:flex; align-items:center; gap:8px; padding:8px 16px; background:var(--crm-surface); border:1px solid var(--crm-border); border-radius:var(--crm-radius-full); cursor:pointer;">
<span class="material-symbols-outlined" style="font-size:18px; color:var(--crm-resolved)">check_circle</span>
<span style="font-size:13px; font-weight:500; color:var(--crm-text-secondary);">Resolved</span>
<span style="font-size:12px; font-weight:700; background:var(--crm-resolved-bg); color:var(--crm-resolved); padding:1px 8px; border-radius:var(--crm-radius-full);">5</span>
</div>
<div style="display:flex; align-items:center; gap:8px; padding:8px 16px; background:var(--crm-surface); border:1px solid var(--crm-border); border-radius:var(--crm-radius-full); cursor:pointer;">
<span class="material-symbols-outlined" style="font-size:18px; color:var(--crm-closed)">cancel</span>
<span style="font-size:13px; font-weight:500; color:var(--crm-text-secondary);">Closed</span>
<span style="font-size:12px; font-weight:700; background:var(--crm-closed-bg); color:var(--crm-closed); padding:1px 8px; border-radius:var(--crm-radius-full);">5</span>
</div>
<div style="display:flex; align-items:center; gap:8px; padding:8px 16px; background:var(--crm-escalated-bg); border:1px solid rgba(220,38,38,0.2); border-radius:var(--crm-radius-full); cursor:pointer;">
<span class="material-symbols-outlined" style="font-size:18px; color:var(--crm-escalated)">warning</span>
<span style="font-size:13px; font-weight:600; color:var(--crm-escalated);">Escalated</span>
<span style="font-size:12px; font-weight:700; background:var(--crm-escalated); color:white; padding:1px 8px; border-radius:var(--crm-radius-full);">3</span>
</div>
</div>
<!-- Filter Bar -->
<div class="filter-bar">
<div class="search-input">
<span class="material-symbols-outlined">search</span>
<input type="text" placeholder="Search feedbacks by title, customer...">
</div>
<div class="filter-select">
<span class="material-symbols-outlined" style="font-size:18px">filter_list</span>
All Status
<span class="material-symbols-outlined" style="font-size:18px">expand_more</span>
</div>
<div class="filter-select">
<span class="material-symbols-outlined" style="font-size:18px">inbox_stack</span>
All Channels
<span class="material-symbols-outlined" style="font-size:18px">expand_more</span>
</div>
<div class="filter-select">
<span class="material-symbols-outlined" style="font-size:18px">apartment</span>
All Departments
<span class="material-symbols-outlined" style="font-size:18px">expand_more</span>
</div>
<div class="filter-select">
<span class="material-symbols-outlined" style="font-size:18px">person</span>
All Handlers
<span class="material-symbols-outlined" style="font-size:18px">expand_more</span>
</div>
<button class="btn btn--ghost btn--sm" style="margin-left:auto;">
<span class="material-symbols-outlined" style="font-size:18px">download</span>
Export
</button>
</div>
<!-- Table -->
<div class="card">
<div class="card-body card-body--compact">
<div class="table-container">
<table>
<thead>
<tr>
<th style="width:40px;"><input type="checkbox" style="accent-color:var(--crm-primary);"></th>
<th>Title</th>
<th>Customer</th>
<th>Product</th>
<th>Contract</th>
<th>Channel</th>
<th>Tags</th>
<th>Status</th>
<th>Department</th>
<th>Assigned To</th>
<th>Created</th>
<th style="width:60px;"></th>
</tr>
</thead>
<tbody>
<tr>
<td><input type="checkbox" style="accent-color:var(--crm-primary);"></td>
<td><a href="feedback-detail.html" class="table-link">Leak in bathroom</a></td>
<td>
<div class="table-meta">
<div class="table-avatar table-avatar--blue">NA</div>
Nguyễn A
</div>
</td>
<td>Sunrise A1</td>
<td><span class="badge badge--sale">Sale</span></td>
<td><span style="display:inline-flex;align-items:center;gap:4px;font-size:13px;"><span class="material-symbols-outlined" style="font-size:16px;color:#0066ff">chat</span> Zalo</span></td>
<td><span class="badge" style="background:#ede9fe;color:#7c3aed;border:1px solid #c4b5fd;font-size:11px;">Plumbing</span></td>
<td><span class="badge badge--pending badge--dot">Pending</span></td>
<td>CSKH</td>
<td>
<div class="table-meta">
<div class="table-avatar table-avatar--green">ST</div>
Staff
</div>
</td>
<td style="color:var(--crm-text-tertiary); font-size:13px;">2h ago</td>
<td>
<button class="btn btn--ghost btn--sm" style="padding:4px;">
<span class="material-symbols-outlined" style="font-size:18px;">more_vert</span>
</button>
</td>
</tr>
<tr>
<td><input type="checkbox" style="accent-color:var(--crm-primary);"></td>
<td><a href="#" class="table-link">AC not working</a></td>
<td>
<div class="table-meta">
<div class="table-avatar table-avatar--purple">TB</div>
Trần B
</div>
</td>
<td>Green Valley</td>
<td><span class="badge badge--lease">Lease</span></td>
<td><span style="display:inline-flex;align-items:center;gap:4px;font-size:13px;"><span class="material-symbols-outlined" style="font-size:16px;color:#ea4335">mail</span> Email</span></td>
<td><span class="badge" style="background:#fef3c7;color:#92400e;border:1px solid #fde68a;font-size:11px;">HVAC</span></td>
<td><span class="badge badge--processing badge--dot">Processing</span></td>
<td>Kỹ thuật</td>
<td>
<div class="table-meta">
<div class="table-avatar table-avatar--amber">MG</div>
Manager
</div>
</td>
<td style="color:var(--crm-text-tertiary); font-size:13px;">5h ago</td>
<td>
<button class="btn btn--ghost btn--sm" style="padding:4px;">
<span class="material-symbols-outlined" style="font-size:18px;">more_vert</span>
</button>
</td>
</tr>
<tr style="background:var(--crm-escalated-bg);">
<td><input type="checkbox" style="accent-color:var(--crm-primary);"></td>
<td>
<div style="display:flex;align-items:center;gap:8px;">
<a href="#" class="table-link">Door lock broken</a>
<span class="material-symbols-outlined" style="font-size:16px;color:var(--crm-escalated)" title="Escalated">priority_high</span>
</div>
</td>
<td>
<div class="table-meta">
<div class="table-avatar table-avatar--blue">LC</div>
Lê C
</div>
</td>
<td>Ocean Tower</td>
<td><span class="badge badge--bcc">BCC</span></td>
<td><span style="display:inline-flex;align-items:center;gap:4px;font-size:13px;"><span class="material-symbols-outlined" style="font-size:16px;color:#34a853">phone</span> Phone</span></td>
<td><span class="badge" style="background:#fee2e2;color:#991b1b;border:1px solid #fecaca;font-size:11px;">Urgent</span></td>
<td><span class="badge badge--escalated badge--dot">Escalated</span></td>
<td>CSKH</td>
<td>
<div class="table-meta">
<div class="table-avatar table-avatar--green">ST</div>
Staff
</div>
</td>
<td style="color:var(--crm-text-tertiary); font-size:13px;">1d ago</td>
<td>
<button class="btn btn--ghost btn--sm" style="padding:4px;">
<span class="material-symbols-outlined" style="font-size:18px;">more_vert</span>
</button>
</td>
</tr>
<tr>
<td><input type="checkbox" style="accent-color:var(--crm-primary);"></td>
<td><a href="#" class="table-link">Paint crack in living room</a></td>
<td>
<div class="table-meta">
<div class="table-avatar table-avatar--amber">PD</div>
Phạm D
</div>
</td>
<td>Park Hill</td>
<td><span class="badge badge--sale">Sale</span></td>
<td><span style="display:inline-flex;align-items:center;gap:4px;font-size:13px;"><span class="material-symbols-outlined" style="font-size:16px;color:#6b7280">description</span> Doc</span></td>
<td><span class="badge" style="background:#dbeafe;color:#1e40af;border:1px solid #bfdbfe;font-size:11px;">Painting</span></td>
<td><span class="badge badge--resolved badge--dot">Resolved</span></td>
<td>Kỹ thuật</td>
<td>
<div class="table-meta">
<div class="table-avatar table-avatar--purple">AD</div>
Admin
</div>
</td>
<td style="color:var(--crm-text-tertiary); font-size:13px;">3d ago</td>
<td>
<button class="btn btn--ghost btn--sm" style="padding:4px;">
<span class="material-symbols-outlined" style="font-size:18px;">more_vert</span>
</button>
</td>
</tr>
<tr>
<td><input type="checkbox" style="accent-color:var(--crm-primary);"></td>
<td><a href="#" class="table-link">Window seal broken</a></td>
<td>
<div class="table-meta">
<div class="table-avatar table-avatar--blue">HE</div>
Hoàng E
</div>
</td>
<td>Sunrise A2</td>
<td><span class="badge badge--lease">Lease</span></td>
<td><span style="display:inline-flex;align-items:center;gap:4px;font-size:13px;"><span class="material-symbols-outlined" style="font-size:16px;color:#9c27b0">person</span> In Person</span></td>
<td><span class="badge" style="background:#f0fdf4;color:#166534;border:1px solid #bbf7d0;font-size:11px;">Window</span></td>
<td><span class="badge badge--closed badge--dot">Closed</span></td>
<td>CSKH</td>
<td>
<div class="table-meta">
<div class="table-avatar table-avatar--green">ST</div>
Staff
</div>
</td>
<td style="color:var(--crm-text-tertiary); font-size:13px;">1w ago</td>
<td>
<button class="btn btn--ghost btn--sm" style="padding:4px;">
<span class="material-symbols-outlined" style="font-size:18px;">more_vert</span>
</button>
</td>
</tr>
<tr>
<td><input type="checkbox" style="accent-color:var(--crm-primary);"></td>
<td><a href="#" class="table-link">Elevator noise complaint</a></td>
<td>
<div class="table-meta">
<div class="table-avatar table-avatar--purple">TB</div>
Trần B
</div>
</td>
<td>Green Valley</td>
<td><span class="badge badge--lease">Lease</span></td>
<td><span style="display:inline-flex;align-items:center;gap:4px;font-size:13px;"><span class="material-symbols-outlined" style="font-size:16px;color:#0066ff">chat</span> Zalo</span></td>
<td><span class="badge" style="background:#fef3c7;color:#92400e;border:1px solid #fde68a;font-size:11px;">Noise</span></td>
<td><span class="badge badge--pending badge--dot">Pending</span></td>
<td>Kỹ thuật</td>
<td>
<div class="table-meta">
<div class="table-avatar table-avatar--amber">MG</div>
Manager
</div>
</td>
<td style="color:var(--crm-text-tertiary); font-size:13px;">2d ago</td>
<td>
<button class="btn btn--ghost btn--sm" style="padding:4px;">
<span class="material-symbols-outlined" style="font-size:18px;">more_vert</span>
</button>
</td>
</tr>
<tr>
<td><input type="checkbox" style="accent-color:var(--crm-primary);"></td>
<td><a href="#" class="table-link">Billing discrepancy</a></td>
<td>
<div class="table-meta">
<div class="table-avatar table-avatar--blue">NA</div>
Nguyễn A
</div>
</td>
<td>Sunrise A1</td>
<td><span class="badge badge--sale">Sale</span></td>
<td><span style="display:inline-flex;align-items:center;gap:4px;font-size:13px;"><span class="material-symbols-outlined" style="font-size:16px;color:#ea4335">mail</span> Email</span></td>
<td><span class="badge" style="background:#ede9fe;color:#7c3aed;border:1px solid #c4b5fd;font-size:11px;">Billing</span></td>
<td><span class="badge badge--processing badge--dot">Processing</span></td>
<td>Kế toán</td>
<td>
<div class="table-meta">
<div class="table-avatar table-avatar--green">ST</div>
Staff
</div>
</td>
<td style="color:var(--crm-text-tertiary); font-size:13px;">4d ago</td>
<td>
<button class="btn btn--ghost btn--sm" style="padding:4px;">
<span class="material-symbols-outlined" style="font-size:18px;">more_vert</span>
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- Pagination -->
<div class="pagination">
<div class="pagination-info">Showing <strong>1-7</strong> of <strong>24</strong> feedbacks</div>
<div class="pagination-buttons">
<button class="pagination-btn">
<span class="material-symbols-outlined" style="font-size:18px">chevron_left</span>
</button>
<button class="pagination-btn active">1</button>
<button class="pagination-btn">2</button>
<button class="pagination-btn">3</button>
<button class="pagination-btn">4</button>
<button class="pagination-btn">
<span class="material-symbols-outlined" style="font-size:18px">chevron_right</span>
</button>
</div>
</div>
</div>
</main>
</div>
</body>
</html>

127
webappUI/login.html Normal file
View File

@@ -0,0 +1,127 @@
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login — AfterSales CRM</title>
<link rel="stylesheet" href="shared.css">
</head>
<body>
<div class="login-layout">
<!-- Left Panel -->
<div class="login-left">
<div class="login-left-content">
<div class="login-left-icon">
<span class="material-symbols-outlined">support_agent</span>
</div>
<h1>AfterSales CRM</h1>
<p>Real-estate after-sales customer care system. Manage feedbacks, track tickets, and deliver exceptional service.</p>
<div style="margin-top:48px; display:flex; gap:32px; justify-content:center;">
<div style="text-align:center;">
<div style="font-size:32px; font-weight:800; font-family:var(--font-heading);">3,658</div>
<div style="font-size:13px; opacity:0.8; margin-top:4px;">Records Imported</div>
</div>
<div style="text-align:center;">
<div style="font-size:32px; font-weight:800; font-family:var(--font-heading);">4</div>
<div style="font-size:13px; opacity:0.8; margin-top:4px;">Departments</div>
</div>
<div style="text-align:center;">
<div style="font-size:32px; font-weight:800; font-family:var(--font-heading);">8</div>
<div style="font-size:13px; opacity:0.8; margin-top:4px;">Tests Passing</div>
</div>
</div>
<div style="margin-top:48px; padding:16px 24px; background:rgba(255,255,255,0.15); border-radius:var(--crm-radius-lg); backdrop-filter:blur(10px);">
<div style="display:flex; align-items:center; gap:12px; margin-bottom:12px;">
<span class="material-symbols-outlined" style="font-size:20px;">format_quote</span>
<span style="font-size:13px; font-weight:600;">Customer Feedback</span>
</div>
<p style="font-size:14px; line-height:1.6; opacity:0.9; font-style:italic;">
"The support team resolved my issue within 24 hours. Excellent service and very professional follow-up."
</p>
<div style="display:flex; align-items:center; gap:8px; margin-top:12px;">
<div style="width:28px; height:28px; border-radius:50%; background:rgba(255,255,255,0.3); display:flex; align-items:center; justify-content:center; font-size:11px; font-weight:700;">NA</div>
<div>
<div style="font-size:13px; font-weight:600;">Nguyễn A</div>
<div style="font-size:11px; opacity:0.7;">Sunrise A1 Resident</div>
</div>
</div>
</div>
</div>
</div>
<!-- Right Panel - Login Form -->
<div class="login-right">
<div class="login-form-container">
<div class="login-form-header">
<h2>Welcome back</h2>
<p>Sign in to your account to continue</p>
</div>
<form class="login-form" onsubmit="return false;">
<div class="form-group">
<label class="form-label">Email</label>
<input type="email" class="form-input" placeholder="Enter your email" value="admin@minicrm.local">
</div>
<div class="form-group">
<label class="form-label">Password</label>
<div style="position:relative;">
<input type="password" class="form-input" placeholder="Enter your password" value="password" style="padding-right:44px;">
<button type="button" style="position:absolute; right:8px; top:50%; transform:translateY(-50%); background:none; border:none; cursor:pointer; color:var(--crm-text-tertiary); padding:4px;">
<span class="material-symbols-outlined" style="font-size:20px;">visibility_off</span>
</button>
</div>
</div>
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:24px;">
<label class="form-toggle">
<div class="toggle-switch active"></div>
<span class="toggle-label" style="font-size:13px;">Remember me</span>
</label>
<a href="#" style="font-size:13px; font-weight:600; color:var(--crm-primary); text-decoration:none;">Forgot password?</a>
</div>
<button type="submit" class="btn btn--primary btn--lg" style="width:100%;">
Sign In
</button>
</form>
<div class="login-divider">or continue with</div>
<div class="login-demo">
<div class="login-demo-title">Demo Accounts</div>
<div class="login-demo-accounts">
<div class="login-demo-account">
<div>
<div class="login-demo-role">Admin</div>
<div class="login-demo-email">admin@minicrm.local</div>
</div>
<span class="material-symbols-outlined" style="font-size:18px; color:var(--crm-text-tertiary);">arrow_forward</span>
</div>
<div class="login-demo-account">
<div>
<div class="login-demo-role">Manager</div>
<div class="login-demo-email">manager@minicrm.local</div>
</div>
<span class="material-symbols-outlined" style="font-size:18px; color:var(--crm-text-tertiary);">arrow_forward</span>
</div>
<div class="login-demo-account">
<div>
<div class="login-demo-role">Staff</div>
<div class="login-demo-email">staff@minicrm.local</div>
</div>
<span class="material-symbols-outlined" style="font-size:18px; color:var(--crm-text-tertiary);">arrow_forward</span>
</div>
</div>
</div>
<div style="text-align:center; margin-top:24px; font-size:12px; color:var(--crm-text-tertiary);">
Laravel 13.6 • Filament 5.6 • Spatie RBAC
</div>
</div>
</div>
</div>
</body>
</html>

1363
webappUI/shared.css Normal file

File diff suppressed because it is too large Load Diff