feat: add i18n support for Vietnamese and English
This commit is contained in:
@@ -11,9 +11,9 @@ enum ContractStatus: string
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::ACTIVE => 'Đang hiệu lực',
|
||||
self::EXPIRED => 'Hết hạn',
|
||||
self::LIQUIDATED => 'Đã thanh lý',
|
||||
self::ACTIVE => __('enums.contract_status.active'),
|
||||
self::EXPIRED => __('enums.contract_status.expired'),
|
||||
self::LIQUIDATED => __('enums.contract_status.liquidated'),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -11,9 +11,9 @@ enum ContractType: string
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::SALE => 'HĐ Mua bán',
|
||||
self::LEASE => 'HĐ Thuê',
|
||||
self::BCC => 'HĐ Hợp tác kinh doanh (BCC)',
|
||||
self::SALE => __('enums.contract_type.sale'),
|
||||
self::LEASE => __('enums.contract_type.lease'),
|
||||
self::BCC => __('enums.contract_type.bcc'),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -12,10 +12,10 @@ enum HandoverStatus: string
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::NOT_READY => 'Chưa đủ điều kiện',
|
||||
self::READY => 'Đủ điều kiện',
|
||||
self::HANDED_OVER => 'Đã bàn giao',
|
||||
self::SCHEDULED => 'Đã lên lịch',
|
||||
self::NOT_READY => __('enums.handover.not_ready'),
|
||||
self::READY => __('enums.handover.ready'),
|
||||
self::HANDED_OVER => __('enums.handover.handed_over'),
|
||||
self::SCHEDULED => __('enums.handover.scheduled'),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -11,9 +11,9 @@ enum ServiceType: string
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::MANAGEMENT_FEE => 'Phí quản lý',
|
||||
self::GRATITUDE => 'Tri ân',
|
||||
self::SELF_BUSINESS => 'Tự doanh',
|
||||
self::MANAGEMENT_FEE => __('enums.service.management_fee'),
|
||||
self::GRATITUDE => __('enums.service.gratitude'),
|
||||
self::SELF_BUSINESS => __('enums.service.self_business'),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -20,13 +20,18 @@ class ContractResource extends Resource
|
||||
|
||||
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;
|
||||
|
||||
public static function getPluralLabel(): ?string
|
||||
{
|
||||
return __('app.resource_contracts');
|
||||
}
|
||||
|
||||
public static function getNavigationGroup(): ?string
|
||||
{
|
||||
return 'Management';
|
||||
return __('app.nav_management');
|
||||
}
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
|
||||
@@ -16,33 +16,37 @@ class ContractForm
|
||||
{
|
||||
return $schema
|
||||
->components([
|
||||
Section::make('Thông tin hợp đồng')
|
||||
Section::make(__('app.contract_info'))
|
||||
->schema([
|
||||
Select::make('product_id')
|
||||
->label(__('app.product'))
|
||||
->relationship('product', 'name')
|
||||
->searchable()
|
||||
->required(),
|
||||
Select::make('customer_id')
|
||||
->label(__('app.widget_customer'))
|
||||
->relationship('customer', 'name')
|
||||
->searchable()
|
||||
->required(),
|
||||
Select::make('type')
|
||||
->label(__('app.contract'))
|
||||
->options(ContractType::options())
|
||||
->required()
|
||||
->native(false),
|
||||
Select::make('status')
|
||||
->label(__('app.status'))
|
||||
->options(ContractStatus::ACTIVE->options())
|
||||
->default('active')
|
||||
->required()
|
||||
->native(false),
|
||||
DatePicker::make('start_date')
|
||||
->label('Ngày bắt đầu'),
|
||||
->label(__('app.start_date')),
|
||||
DatePicker::make('end_date')
|
||||
->label('Ngày kết thúc'),
|
||||
->label(__('app.end_date')),
|
||||
DatePicker::make('printed_at')
|
||||
->label('Ngày in HĐ'),
|
||||
->label(__('app.printed_at')),
|
||||
TextInput::make('signed_status')
|
||||
->label('Tình trạng ký')
|
||||
->label(__('app.signed_status'))
|
||||
->maxLength(255),
|
||||
])
|
||||
->columns(2),
|
||||
|
||||
@@ -46,7 +46,7 @@ class ContractsTable
|
||||
->toggleable(),
|
||||
TextColumn::make('feedbacks_count')
|
||||
->counts('feedbacks')
|
||||
->label('Tickets'),
|
||||
->label(__('app.tickets')),
|
||||
TextColumn::make('created_at')
|
||||
->dateTime()
|
||||
->sortable()
|
||||
|
||||
@@ -21,13 +21,18 @@ class CustomerResource extends Resource
|
||||
|
||||
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;
|
||||
|
||||
public static function getPluralLabel(): ?string
|
||||
{
|
||||
return __('app.resource_customers');
|
||||
}
|
||||
|
||||
public static function getNavigationGroup(): ?string
|
||||
{
|
||||
return 'Management';
|
||||
return __('app.nav_management');
|
||||
}
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
|
||||
@@ -12,7 +12,7 @@ class FeedbacksRelationManager extends RelationManager
|
||||
{
|
||||
protected static string $relationship = 'feedbacks';
|
||||
|
||||
protected static ?string $title = 'Feedback History';
|
||||
protected static ?string $title = 'app.feedback_history';
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
@@ -23,8 +23,8 @@ class FeedbacksRelationManager extends RelationManager
|
||||
->searchable()
|
||||
->url(fn ($record) => FeedbackResource::getUrl('edit', ['record' => $record])),
|
||||
TextColumn::make('customerProduct.product.name')
|
||||
->label('Product')
|
||||
->placeholder('General'),
|
||||
->label(__('app.product'))
|
||||
->placeholder(__('app.general')),
|
||||
TextColumn::make('status')
|
||||
->badge()
|
||||
->color(fn (string $state): string => match ($state) {
|
||||
@@ -37,10 +37,10 @@ class FeedbacksRelationManager extends RelationManager
|
||||
TextColumn::make('tags.name')
|
||||
->badge(),
|
||||
TextColumn::make('feedbackChannel.name')
|
||||
->label('Channel'),
|
||||
->label(__('app.channel')),
|
||||
TextColumn::make('interactions_count')
|
||||
->counts('interactions')
|
||||
->label('Interactions'),
|
||||
->label(__('app.interactions')),
|
||||
TextColumn::make('created_at')
|
||||
->dateTime('d/m/Y')
|
||||
->sortable(),
|
||||
@@ -48,10 +48,10 @@ class FeedbacksRelationManager extends RelationManager
|
||||
->filters([
|
||||
SelectFilter::make('status')
|
||||
->options([
|
||||
'pending' => 'Pending',
|
||||
'processing' => 'Processing',
|
||||
'resolved' => 'Resolved',
|
||||
'closed' => 'Closed',
|
||||
'pending' => __('app.status_pending'),
|
||||
'processing' => __('app.status_processing'),
|
||||
'resolved' => __('app.status_resolved'),
|
||||
'closed' => __('app.status_closed'),
|
||||
]),
|
||||
SelectFilter::make('tags')
|
||||
->relationship('tags', 'name'),
|
||||
|
||||
@@ -14,32 +14,37 @@ class CustomerForm
|
||||
{
|
||||
return $schema
|
||||
->components([
|
||||
Section::make('Customer Information')
|
||||
Section::make(__('app.customer_information'))
|
||||
->schema([
|
||||
TextInput::make('name')
|
||||
->label(__('app.name'))
|
||||
->required()
|
||||
->maxLength(255),
|
||||
TextInput::make('email')
|
||||
->label(__('app.email'))
|
||||
->email()
|
||||
->maxLength(255),
|
||||
TextInput::make('phone')
|
||||
->label(__('app.phone'))
|
||||
->tel()
|
||||
->maxLength(255),
|
||||
Textarea::make('address')
|
||||
->label(__('app.address'))
|
||||
->columnSpanFull(),
|
||||
Select::make('status')
|
||||
->options([
|
||||
'active' => 'Active',
|
||||
'inactive' => 'Inactive',
|
||||
'active' => __('app.status_active'),
|
||||
'inactive' => __('app.status_inactive'),
|
||||
])
|
||||
->default('active')
|
||||
->required(),
|
||||
])
|
||||
->columns(2),
|
||||
|
||||
Section::make('Owned Products')
|
||||
Section::make(__('app.owned_products'))
|
||||
->schema([
|
||||
Select::make('products')
|
||||
->label(__('app.resource_products'))
|
||||
->relationship('products', 'name')
|
||||
->multiple()
|
||||
->preload()
|
||||
|
||||
@@ -33,10 +33,10 @@ class CustomersTable
|
||||
}),
|
||||
TextColumn::make('products_count')
|
||||
->counts('products')
|
||||
->label('Products'),
|
||||
->label(__('app.resource_products')),
|
||||
TextColumn::make('feedbacks_count')
|
||||
->counts('feedbacks')
|
||||
->label('Feedbacks'),
|
||||
->label(__('app.feedbacks')),
|
||||
TextColumn::make('created_at')
|
||||
->dateTime()
|
||||
->sortable()
|
||||
@@ -45,8 +45,8 @@ class CustomersTable
|
||||
->filters([
|
||||
SelectFilter::make('status')
|
||||
->options([
|
||||
'active' => 'Active',
|
||||
'inactive' => 'Inactive',
|
||||
'active' => __('app.status_active'),
|
||||
'inactive' => __('app.status_inactive'),
|
||||
]),
|
||||
])
|
||||
->recordActions([
|
||||
|
||||
@@ -21,13 +21,18 @@ class FeedbackResource extends Resource
|
||||
|
||||
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;
|
||||
|
||||
public static function getPluralLabel(): ?string
|
||||
{
|
||||
return __('app.resource_feedbacks');
|
||||
}
|
||||
|
||||
public static function getNavigationGroup(): ?string
|
||||
{
|
||||
return 'Customer Care';
|
||||
return __('app.nav_customer_care');
|
||||
}
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
|
||||
@@ -29,7 +29,7 @@ class CreateFeedback extends CreateRecord
|
||||
$feedback->interactions()->create([
|
||||
'user_id' => auth()->id(),
|
||||
'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]],
|
||||
]);
|
||||
|
||||
|
||||
@@ -33,18 +33,18 @@ class EditFeedback extends EditRecord
|
||||
protected function transferDepartmentAction(): Action
|
||||
{
|
||||
return Action::make('transferDepartment')
|
||||
->label('Transfer Department')
|
||||
->label(__('feedback.transfer_department'))
|
||||
->icon('heroicon-o-arrows-right-left')
|
||||
->color('warning')
|
||||
->visible(fn (): bool => auth()->user()->hasPermissionTo('transfer-department'))
|
||||
->form([
|
||||
Select::make('to_department_id')
|
||||
->label('Target Department')
|
||||
->label(__('feedback.target_department'))
|
||||
->options(Department::pluck('name', 'id')->toArray())
|
||||
->required()
|
||||
->searchable(),
|
||||
Select::make('new_assignee_id')
|
||||
->label('Assign To (optional)')
|
||||
->label(__('feedback.assign_to_optional'))
|
||||
->options(function (\Filament\Forms\Get $get) {
|
||||
$deptId = $get('to_department_id');
|
||||
if (! $deptId) {
|
||||
@@ -57,11 +57,11 @@ class EditFeedback extends EditRecord
|
||||
->searchable()
|
||||
->nullable(),
|
||||
Textarea::make('reason')
|
||||
->label('Reason')
|
||||
->label(__('feedback.reason'))
|
||||
->required()
|
||||
->rows(3),
|
||||
FileUpload::make('transfer_attachments')
|
||||
->label('Attachments')
|
||||
->label(__('app.attachments'))
|
||||
->multiple()
|
||||
->disk('local')
|
||||
->directory('feedback-attachments')
|
||||
@@ -92,7 +92,7 @@ class EditFeedback extends EditRecord
|
||||
}
|
||||
|
||||
Notification::make()
|
||||
->title('Transferred successfully')
|
||||
->title(__('feedback.transferred_successfully'))
|
||||
->success()
|
||||
->send();
|
||||
|
||||
@@ -103,14 +103,14 @@ class EditFeedback extends EditRecord
|
||||
protected function closeTicketAction(): Action
|
||||
{
|
||||
return Action::make('closeTicket')
|
||||
->label('Close Ticket')
|
||||
->label(__('feedback.close_ticket'))
|
||||
->icon('heroicon-o-check-circle')
|
||||
->color('success')
|
||||
->visible(fn (): bool => $this->getRecord()->status !== 'closed' && auth()->user()->hasPermissionTo('close-ticket'))
|
||||
->requiresConfirmation()
|
||||
->modalHeading('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.')
|
||||
->modalSubmitActionLabel('Close')
|
||||
->modalHeading(__('feedback.close_ticket'))
|
||||
->modalDescription(__('feedback.close_confirm'))
|
||||
->modalSubmitActionLabel(__('feedback.close_submit'))
|
||||
->action(function (): void {
|
||||
$feedback = $this->getRecord();
|
||||
$actor = auth()->user();
|
||||
@@ -121,7 +121,7 @@ class EditFeedback extends EditRecord
|
||||
$closingService->close($feedback, $actor);
|
||||
|
||||
Notification::make()
|
||||
->title('Ticket closed successfully')
|
||||
->title(__('feedback.closed_successfully'))
|
||||
->success()
|
||||
->send();
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ class InteractionsRelationManager extends RelationManager
|
||||
{
|
||||
protected static string $relationship = 'interactions';
|
||||
|
||||
protected static ?string $title = 'Interaction History';
|
||||
protected static ?string $title = 'feedback.interaction_history';
|
||||
|
||||
public function form(Schema $schema): Schema
|
||||
{
|
||||
@@ -35,9 +35,9 @@ class InteractionsRelationManager extends RelationManager
|
||||
->components([
|
||||
Select::make('type')
|
||||
->options([
|
||||
'note' => 'Note',
|
||||
'status_change' => 'Status Change',
|
||||
'assignment' => 'Assignment / Forward',
|
||||
'note' => __('feedback.interaction_type_note'),
|
||||
'status_change' => __('feedback.interaction_type_status_change'),
|
||||
'assignment' => __('feedback.interaction_type_assignment'),
|
||||
])
|
||||
->default('note')
|
||||
->required()
|
||||
@@ -52,28 +52,28 @@ class InteractionsRelationManager extends RelationManager
|
||||
->columnSpanFull(),
|
||||
|
||||
Select::make('new_status')
|
||||
->label('New Status')
|
||||
->label(__('app.new_status'))
|
||||
->options(fn (): array => auth()->user()->hasPermissionTo('close-ticket')
|
||||
? ['pending' => 'Pending', 'processing' => 'Processing', 'resolved' => 'Resolved', 'closed' => 'Closed']
|
||||
: ['pending' => 'Pending', 'processing' => 'Processing', 'resolved' => 'Resolved'],
|
||||
? ['pending' => __('app.status_pending'), 'processing' => __('app.status_processing'), 'resolved' => __('app.status_resolved'), 'closed' => __('app.status_closed')]
|
||||
: ['pending' => __('app.status_pending'), 'processing' => __('app.status_processing'), 'resolved' => __('app.status_resolved')],
|
||||
)
|
||||
->visible(fn ($get): bool => $get('type') === 'status_change'),
|
||||
|
||||
Select::make('new_assignee')
|
||||
->label('Assign To')
|
||||
->label(__('app.assign_to'))
|
||||
->options(User::pluck('name', 'id')->toArray())
|
||||
->searchable()
|
||||
->visible(fn ($get): bool => $get('type') === 'assignment'),
|
||||
|
||||
Select::make('department_id')
|
||||
->label('Target Department')
|
||||
->label(__('app.target_department'))
|
||||
->options(Department::pluck('name', 'id')->toArray())
|
||||
->searchable()
|
||||
->nullable()
|
||||
->visible(fn ($get): bool => $get('type') === 'assignment'),
|
||||
|
||||
FileUpload::make('attachments')
|
||||
->label('Attachments')
|
||||
->label(__('app.attachments'))
|
||||
->multiple()
|
||||
->disk('local')
|
||||
->directory('feedback-attachments')
|
||||
@@ -90,13 +90,13 @@ class InteractionsRelationManager extends RelationManager
|
||||
->dateTime('d/m/Y H:i')
|
||||
->sortable(),
|
||||
TextColumn::make('user.name')
|
||||
->label('By'),
|
||||
->label(__('app.by')),
|
||||
TextColumn::make('type')
|
||||
->badge()
|
||||
->formatStateUsing(fn (string $state): string => match ($state) {
|
||||
'note' => 'Note',
|
||||
'status_change' => 'Status Change',
|
||||
'assignment' => 'Assignment',
|
||||
'note' => __('feedback.interaction_type_note'),
|
||||
'status_change' => __('feedback.interaction_type_status_change'),
|
||||
'assignment' => __('feedback.interaction_type_assignment'),
|
||||
default => $state,
|
||||
})
|
||||
->color(fn (string $state): string => match ($state) {
|
||||
@@ -110,7 +110,7 @@ class InteractionsRelationManager extends RelationManager
|
||||
->limit(80)
|
||||
->wrap(),
|
||||
TextColumn::make('changes')
|
||||
->label('Details')
|
||||
->label(__('app.details'))
|
||||
->formatStateUsing(function (?array $state): string {
|
||||
if (! $state) return '';
|
||||
$lines = [];
|
||||
@@ -123,7 +123,7 @@ class InteractionsRelationManager extends RelationManager
|
||||
})
|
||||
->html(),
|
||||
TextColumn::make('attachments_count')
|
||||
->label('Files')
|
||||
->label(__('app.files'))
|
||||
->counts('attachments')
|
||||
->badge()
|
||||
->color(fn (int $state): string => $state > 0 ? 'primary' : 'gray'),
|
||||
@@ -131,9 +131,9 @@ class InteractionsRelationManager extends RelationManager
|
||||
->defaultSort('created_at', 'desc')
|
||||
->recordActions([
|
||||
Action::make('viewAttachments')
|
||||
->label('View Attachments')
|
||||
->label(__('feedback.view_attachments'))
|
||||
->icon('heroicon-o-paper-clip')
|
||||
->modalHeading('Attachments')
|
||||
->modalHeading(__('app.attachments'))
|
||||
->modalContent(function (FeedbackInteraction $record): \Illuminate\Contracts\View\View {
|
||||
$fileService = App::make(FileService::class);
|
||||
$attachments = $record->attachments;
|
||||
@@ -155,7 +155,7 @@ class InteractionsRelationManager extends RelationManager
|
||||
])
|
||||
->headerActions([
|
||||
CreateAction::make()
|
||||
->label('Add Interaction')
|
||||
->label(__('feedback.add_interaction'))
|
||||
->icon('heroicon-o-plus')
|
||||
->mutateFormDataUsing(function (array $data, $livewire): array {
|
||||
$data['user_id'] = auth()->id();
|
||||
@@ -177,8 +177,8 @@ class InteractionsRelationManager extends RelationManager
|
||||
|
||||
if ($data['type'] === 'assignment' && isset($data['new_assignee'])) {
|
||||
$feedback = $livewire->getOwnerRecord();
|
||||
$oldAssignee = $feedback->assignedTo?->name ?? 'Unassigned';
|
||||
$newAssignee = User::find($data['new_assignee'])?->name ?? 'Unknown';
|
||||
$oldAssignee = $feedback->assignedTo?->name ?? __('app.unassigned');
|
||||
$newAssignee = User::find($data['new_assignee'])?->name ?? __('app.widget_unknown');
|
||||
$data['changes'] = [
|
||||
'assignment' => ['old' => $oldAssignee, 'new' => $newAssignee],
|
||||
];
|
||||
@@ -221,13 +221,13 @@ class InteractionsRelationManager extends RelationManager
|
||||
|
||||
if ($record->type === 'assignment') {
|
||||
Notification::make()
|
||||
->title('Feedback assigned successfully')
|
||||
->title(__('feedback.assigned_successfully'))
|
||||
->success()
|
||||
->send();
|
||||
}
|
||||
if ($record->type === 'status_change') {
|
||||
Notification::make()
|
||||
->title('Status updated successfully')
|
||||
->title(__('feedback.status_updated'))
|
||||
->success()
|
||||
->send();
|
||||
}
|
||||
|
||||
@@ -20,8 +20,8 @@ class FeedbackForm
|
||||
{
|
||||
return $schema
|
||||
->components([
|
||||
Section::make('Customer & Product')
|
||||
->description('Select the customer and their associated product')
|
||||
Section::make(__('app.customer_product'))
|
||||
->description(__('app.select_customer_product'))
|
||||
->icon('heroicon-o-user-group')
|
||||
->schema([
|
||||
Select::make('customer_id')
|
||||
@@ -32,13 +32,13 @@ class FeedbackForm
|
||||
->columnSpan(1),
|
||||
|
||||
Toggle::make('is_general')
|
||||
->label('General feedback (not related to a product)')
|
||||
->label(__('app.general_feedback'))
|
||||
->default(false)
|
||||
->live()
|
||||
->columnSpan(1),
|
||||
|
||||
Select::make('customer_product_id')
|
||||
->label('Product')
|
||||
->label(__('app.product'))
|
||||
->searchable()
|
||||
->getSearchResultsUsing(function (string $search, $get): array {
|
||||
$customerId = $get('customer_id');
|
||||
@@ -62,7 +62,7 @@ class FeedbackForm
|
||||
->live(),
|
||||
|
||||
Select::make('contract_id')
|
||||
->label('Contract')
|
||||
->label(__('app.contract'))
|
||||
->visible(fn ($get): bool => ! $get('is_general') && filled($get('customer_product_id')))
|
||||
->options(function ($get): array {
|
||||
$customerProductId = $get('customer_product_id');
|
||||
@@ -110,8 +110,8 @@ class FeedbackForm
|
||||
])
|
||||
->columns(2),
|
||||
|
||||
Section::make('Feedback Details')
|
||||
->description('Describe the customer feedback')
|
||||
Section::make(__('app.feedback_details'))
|
||||
->description(__('app.describe_feedback'))
|
||||
->icon('heroicon-o-chat-bubble-left-right')
|
||||
->schema([
|
||||
Select::make('feedback_channel_id')
|
||||
@@ -143,16 +143,16 @@ class FeedbackForm
|
||||
])
|
||||
->columns(2),
|
||||
|
||||
Section::make('Assignment & Status')
|
||||
->description('Assign handler and set initial status')
|
||||
Section::make(__('app.assignment_status'))
|
||||
->description(__('app.assign_handler'))
|
||||
->icon('heroicon-o-arrow-path-rounded-square')
|
||||
->schema([
|
||||
Select::make('status')
|
||||
->options([
|
||||
'pending' => 'Pending',
|
||||
'processing' => 'Processing',
|
||||
'resolved' => 'Resolved',
|
||||
'closed' => 'Closed',
|
||||
'pending' => __('app.status_pending'),
|
||||
'processing' => __('app.status_processing'),
|
||||
'resolved' => __('app.status_resolved'),
|
||||
'closed' => __('app.status_closed'),
|
||||
])
|
||||
->default('pending')
|
||||
->required()
|
||||
@@ -165,35 +165,35 @@ class FeedbackForm
|
||||
->columnSpan(1),
|
||||
|
||||
Select::make('current_department_id')
|
||||
->label('Department')
|
||||
->label(__('app.department'))
|
||||
->options(Department::pluck('name', 'id')->toArray())
|
||||
->searchable()
|
||||
->nullable()
|
||||
->columnSpan(1),
|
||||
|
||||
Toggle::make('is_escalated')
|
||||
->label('Escalated')
|
||||
->label(__('app.escalated'))
|
||||
->visible(fn (): bool => auth()->user()?->hasPermissionTo('close-ticket') ?? false)
|
||||
->columnSpan(1),
|
||||
])
|
||||
->columns(2),
|
||||
|
||||
Section::make('Attachments')
|
||||
->description('Upload files related to this feedback')
|
||||
Section::make(__('app.attachments'))
|
||||
->description(__('app.upload_files'))
|
||||
->icon('heroicon-o-paper-clip')
|
||||
->schema([
|
||||
Select::make('attachment_collection')
|
||||
->label('Collection')
|
||||
->label(__('app.collection'))
|
||||
->options([
|
||||
'general' => 'General',
|
||||
'proof_of_resolution' => 'Proof of Resolution',
|
||||
'customer_evidence' => 'Customer Evidence',
|
||||
'general' => __('app.collection_general'),
|
||||
'proof_of_resolution' => __('app.collection_proof_of_resolution'),
|
||||
'customer_evidence' => __('app.collection_customer_evidence'),
|
||||
])
|
||||
->default('general')
|
||||
->columnSpan(1),
|
||||
|
||||
FileUpload::make('attachments')
|
||||
->label('Files')
|
||||
->label(__('app.files'))
|
||||
->multiple()
|
||||
->disk('local')
|
||||
->directory('feedback-attachments')
|
||||
|
||||
@@ -33,13 +33,13 @@ class FeedbackTable
|
||||
->sortable(),
|
||||
|
||||
TextColumn::make('customerProduct.product.name')
|
||||
->label('Product')
|
||||
->placeholder('General')
|
||||
->label(__('app.product'))
|
||||
->placeholder(__('app.general'))
|
||||
->sortable()
|
||||
->toggleable(),
|
||||
|
||||
TextColumn::make('contract.type')
|
||||
->label('Contract')
|
||||
->label(__('app.contract'))
|
||||
->badge()
|
||||
->formatStateUsing(fn (?string $state): string => $state ? \App\Enums\ContractType::from($state)->label() : '-')
|
||||
->color(fn (?string $state): string => match ($state) {
|
||||
@@ -52,7 +52,7 @@ class FeedbackTable
|
||||
->toggleable(),
|
||||
|
||||
TextColumn::make('feedbackChannel.name')
|
||||
->label('Channel')
|
||||
->label(__('app.channel'))
|
||||
->badge()
|
||||
->color('gray'),
|
||||
|
||||
@@ -72,22 +72,22 @@ class FeedbackTable
|
||||
}),
|
||||
|
||||
TextColumn::make('currentDepartment.name')
|
||||
->label('Department')
|
||||
->label(__('app.department'))
|
||||
->badge()
|
||||
->color('info')
|
||||
->toggleable(),
|
||||
|
||||
TextColumn::make('assignedTo.name')
|
||||
->label('Assigned To')
|
||||
->label(__('app.assigned_to'))
|
||||
->toggleable(),
|
||||
|
||||
IconColumn::make('is_escalated')
|
||||
->label('Escalated')
|
||||
->label(__('app.escalated'))
|
||||
->boolean()
|
||||
->toggleable(),
|
||||
|
||||
TextColumn::make('created_at')
|
||||
->label('Created')
|
||||
->label(__('app.created'))
|
||||
->since()
|
||||
->sortable()
|
||||
->toggleable()
|
||||
@@ -96,20 +96,20 @@ class FeedbackTable
|
||||
->filters([
|
||||
SelectFilter::make('status')
|
||||
->options([
|
||||
'pending' => 'Pending',
|
||||
'processing' => 'Processing',
|
||||
'resolved' => 'Resolved',
|
||||
'closed' => 'Closed',
|
||||
'pending' => __('app.status_pending'),
|
||||
'processing' => __('app.status_processing'),
|
||||
'resolved' => __('app.status_resolved'),
|
||||
'closed' => __('app.status_closed'),
|
||||
]),
|
||||
SelectFilter::make('feedback_channel_id')
|
||||
->relationship('feedbackChannel', 'name')
|
||||
->label('Channel'),
|
||||
->label(__('app.channel')),
|
||||
SelectFilter::make('current_department_id')
|
||||
->relationship('currentDepartment', 'name')
|
||||
->label('Department'),
|
||||
->label(__('app.department')),
|
||||
SelectFilter::make('assigned_to')
|
||||
->relationship('assignedTo', 'name')
|
||||
->label('Assigned To'),
|
||||
->label(__('app.assigned_to')),
|
||||
])
|
||||
->recordActions([
|
||||
EditAction::make(),
|
||||
|
||||
@@ -20,13 +20,18 @@ class FeedbackChannelResource extends Resource
|
||||
|
||||
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;
|
||||
|
||||
public static function getPluralLabel(): ?string
|
||||
{
|
||||
return __('app.resource_channels');
|
||||
}
|
||||
|
||||
public static function getNavigationGroup(): ?string
|
||||
{
|
||||
return 'Management';
|
||||
return __('app.nav_management');
|
||||
}
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
|
||||
@@ -17,18 +17,22 @@ class FeedbackChannelForm
|
||||
Section::make()
|
||||
->schema([
|
||||
TextInput::make('name')
|
||||
->label(__('app.name'))
|
||||
->required()
|
||||
->maxLength(255),
|
||||
TextInput::make('slug')
|
||||
->label(__('app.slug'))
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->unique(ignoreRecord: true),
|
||||
TextInput::make('icon')
|
||||
->label(__('app.icon'))
|
||||
->maxLength(255)
|
||||
->hint('Heroicon name or emoji'),
|
||||
->hint(__('app.icon_hint')),
|
||||
ColorPicker::make('color')
|
||||
->label('Color'),
|
||||
->label(__('app.color')),
|
||||
Toggle::make('is_active')
|
||||
->label(__('app.status_active'))
|
||||
->default(true),
|
||||
])
|
||||
->columns(2),
|
||||
|
||||
@@ -27,7 +27,7 @@ class FeedbackChannelsTable
|
||||
TextColumn::make('slug')
|
||||
->searchable(),
|
||||
TextColumn::make('color')
|
||||
->label('Color')
|
||||
->label(__('app.color'))
|
||||
->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>',
|
||||
$state ?: '#6b7280',
|
||||
@@ -36,10 +36,10 @@ class FeedbackChannelsTable
|
||||
->html(),
|
||||
IconColumn::make('is_active')
|
||||
->boolean()
|
||||
->label('Active'),
|
||||
->label(__('app.status_active')),
|
||||
TextColumn::make('feedbacks_count')
|
||||
->counts('feedbacks')
|
||||
->label('Feedbacks'),
|
||||
->label(__('app.feedbacks')),
|
||||
TextColumn::make('created_at')
|
||||
->dateTime()
|
||||
->sortable()
|
||||
|
||||
@@ -20,11 +20,14 @@ class FeedbackTagResource extends Resource
|
||||
|
||||
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
|
||||
{
|
||||
return 'Management';
|
||||
return __('app.nav_management');
|
||||
}
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
|
||||
@@ -16,13 +16,15 @@ class FeedbackTagForm
|
||||
Section::make()
|
||||
->schema([
|
||||
TextInput::make('name')
|
||||
->label(__('app.name'))
|
||||
->required()
|
||||
->maxLength(255),
|
||||
TextInput::make('slug')
|
||||
->label(__('app.slug'))
|
||||
->required()
|
||||
->maxLength(255),
|
||||
ColorPicker::make('color')
|
||||
->label('Color')
|
||||
->label(__('app.color'))
|
||||
->default('#3b82f6'),
|
||||
])
|
||||
->columns(2),
|
||||
|
||||
@@ -26,7 +26,7 @@ class FeedbackTagsTable
|
||||
TextColumn::make('slug')
|
||||
->searchable(),
|
||||
TextColumn::make('color')
|
||||
->label('Color')
|
||||
->label(__('app.color'))
|
||||
->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>',
|
||||
$state ?: '#6b7280',
|
||||
@@ -35,7 +35,7 @@ class FeedbackTagsTable
|
||||
->html(),
|
||||
TextColumn::make('feedbacks_count')
|
||||
->counts('feedbacks')
|
||||
->label('Used'),
|
||||
->label(__('app.used')),
|
||||
TextColumn::make('created_at')
|
||||
->dateTime()
|
||||
->sortable()
|
||||
|
||||
@@ -23,13 +23,18 @@ class ProductResource extends Resource
|
||||
|
||||
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;
|
||||
|
||||
public static function getPluralLabel(): ?string
|
||||
{
|
||||
return __('app.resource_products');
|
||||
}
|
||||
|
||||
public static function getNavigationGroup(): ?string
|
||||
{
|
||||
return 'Management';
|
||||
return __('app.nav_management');
|
||||
}
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
|
||||
@@ -15,7 +15,7 @@ class ContractsRelationManager extends RelationManager
|
||||
{
|
||||
protected static string $relationship = 'contracts';
|
||||
|
||||
protected static ?string $title = 'Contracts';
|
||||
protected static ?string $title = 'app.contracts';
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
@@ -26,7 +26,7 @@ class ContractsRelationManager extends RelationManager
|
||||
->badge()
|
||||
->formatStateUsing(fn (string $state): string => ContractType::from($state)->label()),
|
||||
TextColumn::make('customer.name')
|
||||
->label('Customer')
|
||||
->label(__('app.widget_customer'))
|
||||
->searchable(),
|
||||
TextColumn::make('status')
|
||||
->badge()
|
||||
@@ -44,10 +44,10 @@ class ContractsRelationManager extends RelationManager
|
||||
->date('d/m/Y')
|
||||
->sortable(),
|
||||
TextColumn::make('signed_status')
|
||||
->label('Signed'),
|
||||
->label(__('app.signed')),
|
||||
TextColumn::make('feedbacks_count')
|
||||
->counts('feedbacks')
|
||||
->label('Tickets'),
|
||||
->label(__('app.tickets')),
|
||||
])
|
||||
->filters([
|
||||
SelectFilter::make('type')
|
||||
|
||||
@@ -12,7 +12,7 @@ class HandoversRelationManager extends RelationManager
|
||||
{
|
||||
protected static string $relationship = 'handovers';
|
||||
|
||||
protected static ?string $title = 'Handovers';
|
||||
protected static ?string $title = 'app.handovers';
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
@@ -20,11 +20,11 @@ class HandoversRelationManager extends RelationManager
|
||||
->recordTitleAttribute('status')
|
||||
->columns([
|
||||
TextColumn::make('customer.name')
|
||||
->label('Customer')
|
||||
->label(__('app.widget_customer'))
|
||||
->searchable()
|
||||
->sortable(),
|
||||
TextColumn::make('handover_date')
|
||||
->label('Ngày BG')
|
||||
->label(__('app.handover_date'))
|
||||
->date('d/m/Y')
|
||||
->sortable(),
|
||||
TextColumn::make('status')
|
||||
@@ -38,7 +38,7 @@ class HandoversRelationManager extends RelationManager
|
||||
})
|
||||
->formatStateUsing(fn (string $state): string => HandoverStatus::from($state)->label()),
|
||||
TextColumn::make('remark')
|
||||
->label('Ghi chú')
|
||||
->label(__('app.remark'))
|
||||
->limit(50)
|
||||
->toggleable(),
|
||||
TextColumn::make('created_at')
|
||||
|
||||
@@ -12,7 +12,7 @@ class ProductServicesRelationManager extends RelationManager
|
||||
{
|
||||
protected static string $relationship = 'productServices';
|
||||
|
||||
protected static ?string $title = 'Services';
|
||||
protected static ?string $title = 'app.services';
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
@@ -38,11 +38,11 @@ class ProductServicesRelationManager extends RelationManager
|
||||
default => 'gray',
|
||||
}),
|
||||
TextColumn::make('actual_collection_date')
|
||||
->label('Ngày thu')
|
||||
->label(__('app.collection_date'))
|
||||
->date('d/m/Y')
|
||||
->sortable(),
|
||||
TextColumn::make('remark')
|
||||
->label('Ghi chú')
|
||||
->label(__('app.remark'))
|
||||
->limit(50)
|
||||
->toggleable(),
|
||||
TextColumn::make('created_at')
|
||||
@@ -55,10 +55,10 @@ class ProductServicesRelationManager extends RelationManager
|
||||
->options(ServiceType::MANAGEMENT_FEE->options()),
|
||||
SelectFilter::make('status')
|
||||
->options([
|
||||
'active' => 'Active',
|
||||
'pending' => 'Pending',
|
||||
'completed' => 'Completed',
|
||||
'cancelled' => 'Cancelled',
|
||||
'active' => __('app.status_active'),
|
||||
'pending' => __('app.status_pending'),
|
||||
'completed' => __('app.status_completed'),
|
||||
'cancelled' => __('app.status_cancelled'),
|
||||
]),
|
||||
])
|
||||
->defaultSort('created_at', 'desc')
|
||||
|
||||
@@ -17,15 +17,17 @@ class ProductForm
|
||||
Section::make()
|
||||
->schema([
|
||||
TextInput::make('name')
|
||||
->label(__('app.name'))
|
||||
->required()
|
||||
->maxLength(255),
|
||||
Textarea::make('description')
|
||||
->label(__('app.description'))
|
||||
->columnSpanFull(),
|
||||
Select::make('status')
|
||||
->options([
|
||||
'active' => 'Active',
|
||||
'inactive' => 'Inactive',
|
||||
'sold_out' => 'Sold Out',
|
||||
'active' => __('app.status_active'),
|
||||
'inactive' => __('app.status_inactive'),
|
||||
'sold_out' => __('app.status_sold_out'),
|
||||
])
|
||||
->default('active')
|
||||
->required(),
|
||||
|
||||
@@ -31,7 +31,7 @@ class ProductsTable
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
TextColumn::make('customers_count')
|
||||
->counts('customers')
|
||||
->label('Owners'),
|
||||
->label(__('app.owners')),
|
||||
TextColumn::make('created_at')
|
||||
->dateTime()
|
||||
->sortable()
|
||||
@@ -40,9 +40,9 @@ class ProductsTable
|
||||
->filters([
|
||||
SelectFilter::make('status')
|
||||
->options([
|
||||
'active' => 'Active',
|
||||
'inactive' => 'Inactive',
|
||||
'sold_out' => 'Sold Out',
|
||||
'active' => __('app.status_active'),
|
||||
'inactive' => __('app.status_inactive'),
|
||||
'sold_out' => __('app.status_sold_out'),
|
||||
]),
|
||||
])
|
||||
->recordActions([
|
||||
|
||||
@@ -20,15 +20,20 @@ class RoleResource extends Resource
|
||||
|
||||
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 ?string $recordTitleAttribute = 'name';
|
||||
|
||||
public static function getPluralLabel(): ?string
|
||||
{
|
||||
return __('app.resource_roles');
|
||||
}
|
||||
|
||||
public static function getNavigationGroup(): ?string
|
||||
{
|
||||
return 'Management';
|
||||
return __('app.nav_management');
|
||||
}
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
|
||||
@@ -17,14 +17,14 @@ class RoleForm
|
||||
Section::make()
|
||||
->schema([
|
||||
TextInput::make('name')
|
||||
->label('Role Name')
|
||||
->label(__('app.role_name'))
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->unique(ignoreRecord: true),
|
||||
]),
|
||||
|
||||
Section::make('Permissions')
|
||||
->description('Chọn quyền cho role này')
|
||||
Section::make(__('app.permissions'))
|
||||
->description(__('app.permissions'))
|
||||
->schema([
|
||||
CheckboxList::make('permissions')
|
||||
->label('')
|
||||
|
||||
@@ -19,7 +19,7 @@ class RolesTable
|
||||
->sortable(),
|
||||
|
||||
TextColumn::make('name')
|
||||
->label('Role Name')
|
||||
->label(__('app.role_name'))
|
||||
->searchable()
|
||||
->sortable()
|
||||
->badge()
|
||||
@@ -31,13 +31,13 @@ class RolesTable
|
||||
}),
|
||||
|
||||
TextColumn::make('permissions_count')
|
||||
->label('Permissions')
|
||||
->label(__('app.permissions'))
|
||||
->counts('permissions')
|
||||
->badge()
|
||||
->color('primary'),
|
||||
|
||||
TextColumn::make('users_count')
|
||||
->label('Users')
|
||||
->label(__('app.resource_users'))
|
||||
->counts('users')
|
||||
->badge()
|
||||
->color('success'),
|
||||
|
||||
@@ -22,14 +22,18 @@ class ListUsers extends ListRecords
|
||||
public function getTabs(): array
|
||||
{
|
||||
return [
|
||||
'all' => Tab::make(),
|
||||
'all' => Tab::make()
|
||||
->label(__('app.tab_all')),
|
||||
'admin' => Tab::make()
|
||||
->label('Admin')
|
||||
->modifyQueryUsing(fn (Builder $query) => $query->where('role', 'admin'))
|
||||
->badge(fn () => static::getResource()::getModel()::where('role', 'admin')->count()),
|
||||
'manager' => Tab::make()
|
||||
->label('Manager')
|
||||
->modifyQueryUsing(fn (Builder $query) => $query->where('role', 'manager'))
|
||||
->badge(fn () => static::getResource()::getModel()::where('role', 'manager')->count()),
|
||||
'staff' => Tab::make()
|
||||
->label('Staff')
|
||||
->modifyQueryUsing(fn (Builder $query) => $query->where('role', 'staff'))
|
||||
->badge(fn () => static::getResource()::getModel()::where('role', 'staff')->count()),
|
||||
];
|
||||
|
||||
@@ -14,16 +14,19 @@ class UserForm
|
||||
return $schema
|
||||
->components([
|
||||
TextInput::make('name')
|
||||
->label(__('app.name'))
|
||||
->required()
|
||||
->maxLength(255),
|
||||
|
||||
TextInput::make('email')
|
||||
->label(__('app.email'))
|
||||
->email()
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->unique(ignoreRecord: true),
|
||||
|
||||
TextInput::make('password')
|
||||
->label(__('app.password'))
|
||||
->password()
|
||||
->revealable()
|
||||
->required(fn (string $operation): bool => $operation === 'create')
|
||||
@@ -33,7 +36,7 @@ class UserForm
|
||||
->columnSpanFull(),
|
||||
|
||||
Select::make('role')
|
||||
->label('Role')
|
||||
->label(__('app.role'))
|
||||
->options(Role::pluck('name', 'name')->toArray())
|
||||
->required()
|
||||
->searchable(),
|
||||
|
||||
@@ -20,15 +20,20 @@ class UserResource extends Resource
|
||||
|
||||
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 ?string $recordTitleAttribute = 'name';
|
||||
|
||||
public static function getPluralLabel(): ?string
|
||||
{
|
||||
return __('app.resource_users');
|
||||
}
|
||||
|
||||
public static function getNavigationGroup(): ?string
|
||||
{
|
||||
return 'Management';
|
||||
return __('app.nav_management');
|
||||
}
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
|
||||
@@ -35,29 +35,29 @@ class AvgProcessingTimeWidget extends BaseWidget
|
||||
|
||||
$avgDisplay = $avgMinutes ? round($avgMinutes) . ' min' : 'N/A';
|
||||
$avgDescription = $avgMinutes
|
||||
? 'Avg time from creation to resolution'
|
||||
: 'No resolved tickets yet';
|
||||
? __('app.widget_avg_time_desc')
|
||||
: __('app.widget_no_resolved_tickets');
|
||||
|
||||
return [
|
||||
Stat::make('Resolved Tickets', (string) $totalResolved)
|
||||
->description('Total resolved/closed tickets')
|
||||
Stat::make(__('app.widget_resolved_tickets'), (string) $totalResolved)
|
||||
->description(__('app.widget_total_resolved_closed'))
|
||||
->descriptionIcon('heroicon-m-check-circle')
|
||||
->color('success')
|
||||
->chart([7, 3, 4, 5, 6, 8, $totalResolved]),
|
||||
|
||||
Stat::make('Avg Processing Time', $avgDisplay)
|
||||
Stat::make(__('app.widget_avg_processing_time'), $avgDisplay)
|
||||
->description($avgDescription)
|
||||
->descriptionIcon('heroicon-m-clock')
|
||||
->color('info'),
|
||||
|
||||
Stat::make('Pending Tickets', (string) $totalPending)
|
||||
->description('Still in progress')
|
||||
Stat::make(__('app.widget_pending_tickets'), (string) $totalPending)
|
||||
->description(__('app.widget_still_in_progress'))
|
||||
->descriptionIcon('heroicon-m-arrow-path')
|
||||
->color('warning')
|
||||
->chart([3, 5, 4, 6, 5, 4, $totalPending]),
|
||||
|
||||
Stat::make('Escalated', (string) $escalatedCount)
|
||||
->description('Tickets marked as escalated')
|
||||
Stat::make(__('app.widget_escalated'), (string) $escalatedCount)
|
||||
->description(__('app.widget_escalated_desc'))
|
||||
->descriptionIcon('heroicon-m-exclamation-triangle')
|
||||
->color('danger'),
|
||||
];
|
||||
|
||||
@@ -5,15 +5,22 @@ namespace App\Filament\Widgets;
|
||||
use App\Models\Department;
|
||||
use App\Models\Feedback;
|
||||
use Filament\Widgets\ChartWidget;
|
||||
use Illuminate\Contracts\Support\Htmlable;
|
||||
|
||||
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';
|
||||
|
||||
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
|
||||
{
|
||||
return 'bar';
|
||||
@@ -39,19 +46,18 @@ class HandlerPerformanceWidget extends ChartWidget
|
||||
->with('assignedTo')
|
||||
->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();
|
||||
|
||||
// Generate gradient colors based on performance
|
||||
$maxMinutes = max($data) ?: 1;
|
||||
$colors = collect($data)->map(function ($minutes) use ($maxMinutes) {
|
||||
$ratio = $minutes / $maxMinutes;
|
||||
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) {
|
||||
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();
|
||||
|
||||
$borderColors = collect($data)->map(function ($minutes) use ($maxMinutes) {
|
||||
@@ -67,7 +73,7 @@ class HandlerPerformanceWidget extends ChartWidget
|
||||
return [
|
||||
'datasets' => [
|
||||
[
|
||||
'label' => 'Avg Time (minutes)',
|
||||
'label' => __('app.widget_avg_time_minutes'),
|
||||
'data' => $data,
|
||||
'backgroundColor' => $colors,
|
||||
'borderColor' => $borderColors,
|
||||
|
||||
@@ -35,37 +35,37 @@ class ResolvedTicketsWidget extends BaseWidget
|
||||
->color('primary'),
|
||||
|
||||
TextColumn::make('title')
|
||||
->label('Ticket')
|
||||
->label(__('app.widget_ticket'))
|
||||
->searchable()
|
||||
->weight('semibold')
|
||||
->url(fn (Feedback $record): string => FeedbackResource::getUrl('edit', ['record' => $record]))
|
||||
->color('primary'),
|
||||
|
||||
TextColumn::make('customer.name')
|
||||
->label('Customer')
|
||||
->label(__('app.widget_customer'))
|
||||
->searchable(),
|
||||
|
||||
TextColumn::make('customerProduct.product.name')
|
||||
->label('Product')
|
||||
->placeholder('General'),
|
||||
->label(__('app.product'))
|
||||
->placeholder(__('app.general')),
|
||||
|
||||
TextColumn::make('assignedTo.name')
|
||||
->label('Handler'),
|
||||
->label(__('app.widget_handler')),
|
||||
|
||||
TextColumn::make('currentDepartment.name')
|
||||
->label('Department')
|
||||
->label(__('app.department'))
|
||||
->badge()
|
||||
->color('info'),
|
||||
|
||||
TextColumn::make('updated_at')
|
||||
->label('Resolved')
|
||||
->label(__('app.widget_resolved'))
|
||||
->since()
|
||||
->sortable()
|
||||
->color('secondary'),
|
||||
])
|
||||
->defaultSort('updated_at', 'desc')
|
||||
->heading('Tickets Awaiting Closure')
|
||||
->description('Tickets that have been resolved and are pending manager review for closure.')
|
||||
->heading(__('app.widget_tickets_awaiting_closure'))
|
||||
->description(__('app.widget_tickets_awaiting_desc'))
|
||||
->paginated([5]);
|
||||
}
|
||||
|
||||
|
||||
@@ -25,16 +25,16 @@ class TicketClosed extends Notification
|
||||
public function toMail(object $notifiable): MailMessage
|
||||
{
|
||||
return (new MailMessage)
|
||||
->subject(__('Ticket #{id} đã được đóng', ['id' => $this->feedback->id]))
|
||||
->line(__('Ticket: :title', ['title' => $this->feedback->title]))
|
||||
->line(__('Đóng bởi: :name', ['name' => $this->actor->name]))
|
||||
->action(__('Xem Ticket'), url('/admin/feedbacks/' . $this->feedback->id . '/edit'));
|
||||
->subject(__('feedback.notif_ticket_closed_subject', ['id' => $this->feedback->id]))
|
||||
->line(__('feedback.notif_ticket_closed_ticket', ['title' => $this->feedback->title]))
|
||||
->line(__('feedback.notif_ticket_closed_by', ['name' => $this->actor->name]))
|
||||
->action(__('feedback.notif_view_ticket'), url('/admin/feedbacks/' . $this->feedback->id . '/edit'));
|
||||
}
|
||||
|
||||
public function toDatabase(object $notifiable): array
|
||||
{
|
||||
return [
|
||||
'message' => __('Ticket #{id} ":title" đã được đóng bởi :actor.', [
|
||||
'message' => __('feedback.notif_ticket_closed_message', [
|
||||
'id' => $this->feedback->id,
|
||||
'title' => $this->feedback->title,
|
||||
'actor' => $this->actor->name,
|
||||
|
||||
@@ -27,17 +27,17 @@ class TicketTransferred extends Notification
|
||||
public function toMail(object $notifiable): MailMessage
|
||||
{
|
||||
return (new MailMessage)
|
||||
->subject(__('Ticket #{id} đã được chuyển đến phòng của bạn', ['id' => $this->feedback->id]))
|
||||
->line(__('Ticket: :title', ['title' => $this->feedback->title]))
|
||||
->line(__('Người chuyển: :name', ['name' => $this->sender->name]))
|
||||
->line(__('Lý do: :reason', ['reason' => $this->transferLog->reason]))
|
||||
->action(__('Xem Ticket'), url('/admin/feedbacks/' . $this->feedback->id . '/edit'));
|
||||
->subject(__('feedback.notif_ticket_transferred_subject', ['id' => $this->feedback->id]))
|
||||
->line(__('feedback.notif_ticket_closed_ticket', ['title' => $this->feedback->title]))
|
||||
->line(__('feedback.notif_ticket_transferred_by', ['name' => $this->sender->name]))
|
||||
->line(__('feedback.notif_ticket_transferred_reason', ['reason' => $this->transferLog->reason]))
|
||||
->action(__('feedback.notif_view_ticket'), url('/admin/feedbacks/' . $this->feedback->id . '/edit'));
|
||||
}
|
||||
|
||||
public function toDatabase(object $notifiable): array
|
||||
{
|
||||
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,
|
||||
'title' => $this->feedback->title,
|
||||
'sender' => $this->sender->name,
|
||||
|
||||
@@ -56,10 +56,10 @@ class AdminPanelProvider extends PanelProvider
|
||||
])
|
||||
->navigationGroups([
|
||||
NavigationGroup::make()
|
||||
->label('Customer Care')
|
||||
->label(__('app.nav_customer_care'))
|
||||
->icon('heroicon-o-chat-bubble-left-right'),
|
||||
NavigationGroup::make()
|
||||
->label('Management')
|
||||
->label(__('app.nav_management'))
|
||||
->icon('heroicon-o-cog-6-tooth'),
|
||||
])
|
||||
->middleware([
|
||||
|
||||
@@ -26,7 +26,7 @@ class RequireProofOfResolution implements ValidationRule
|
||||
$fileService = App::make(FileService::class);
|
||||
|
||||
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'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,25 +11,22 @@ use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
|
||||
class ClosingService
|
||||
{
|
||||
/**
|
||||
* Close a feedback. Throws if staff tries to close or no proof exists.
|
||||
*/
|
||||
public function close(Feedback $feedback, User $actor): void
|
||||
{
|
||||
if ($feedback->status === 'closed') {
|
||||
throw ValidationException::withMessages([
|
||||
'status' => __('Phiếu này đã được đóng.'),
|
||||
'status' => __('feedback.already_closed'),
|
||||
]);
|
||||
}
|
||||
|
||||
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')) {
|
||||
$managedDeptIds = Department::where('manager_id', $actor->id)->pluck('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')) {
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a feedback. Any role can resolve.
|
||||
*/
|
||||
public function resolve(Feedback $feedback, User $actor): void
|
||||
{
|
||||
if (in_array($feedback->status, ['closed', 'resolved'])) {
|
||||
throw ValidationException::withMessages([
|
||||
'status' => __('Phiếu này đã được xử lý hoặc đóng.'),
|
||||
'status' => __('feedback.already_resolved'),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -126,11 +126,11 @@ class FileService
|
||||
$errors = [];
|
||||
|
||||
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) {
|
||||
$errors['file'] = __('File size exceeds :max KB.', ['max' => $this->maxSize]);
|
||||
$errors['file'] = __('feedback.file_size_exceeds', ['max' => $this->maxSize]);
|
||||
}
|
||||
|
||||
if (! empty($errors)) {
|
||||
|
||||
@@ -10,9 +10,6 @@ use Illuminate\Validation\ValidationException;
|
||||
|
||||
class TransferService
|
||||
{
|
||||
/**
|
||||
* Transfer a feedback to another department.
|
||||
*/
|
||||
public function transfer(
|
||||
Feedback $feedback,
|
||||
Department $toDepartment,
|
||||
@@ -22,7 +19,7 @@ class TransferService
|
||||
): TicketTransferLog {
|
||||
if (empty(trim($reason))) {
|
||||
throw ValidationException::withMessages([
|
||||
'reason' => __('Vui lòng nhập lý do chuyển tiếp.'),
|
||||
'reason' => __('feedback.transfer_reason_required'),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user