Files
akmon/pages/ec/family/communication.uvue
2026-01-20 08:04:15 +08:00

1528 lines
38 KiB
Plaintext

<!-- 家属沟通 - 重构版本 -->
<template>
<view class="family-communication">
<!-- Header -->
<view class="header">
<text class="header-title">沟通交流</text>
<view class="header-actions">
<button class="action-btn" @click="showNewMessage">
<text class="btn-text">✏️ 发消息</text>
</button>
<button class="action-btn" @click="showVideoCall">
<text class="btn-text">📹 视频通话</text>
</button>
</view>
</view>
<!-- Communication Stats -->
<view class="stats-section">
<view class="stat-card">
<view class="stat-icon">💬</view>
<view class="stat-content">
<text class="stat-number">{{ stats.unread_messages }}</text>
<text class="stat-label">未读消息</text>
</view>
</view>
<view class="stat-card">
<view class="stat-icon">📞</view>
<view class="stat-content">
<text class="stat-number">{{ stats.call_requests }}</text>
<text class="stat-label">通话申请</text>
</view>
</view>
<view class="stat-card">
<view class="stat-icon">🗓️</view>
<view class="stat-content">
<text class="stat-number">{{ stats.visit_requests }}</text>
<text class="stat-label">探视预约</text>
</view>
</view>
<view class="stat-card">
<view class="stat-icon">📄</view>
<view class="stat-content">
<text class="stat-number">{{ stats.new_reports }}</text>
<text class="stat-label">新报告</text>
</view>
</view>
</view>
<!-- Tab Navigation -->
<view class="tab-navigation">
<button
class="tab-btn"
:class="{ active: activeTab === 'messages' }"
@click="setActiveTab('messages')"
>
<text class="tab-text">消息</text>
</button>
<button
class="tab-btn"
:class="{ active: activeTab === 'calls' }"
@click="setActiveTab('calls')"
>
<text class="tab-text">通话</text>
</button>
<button
class="tab-btn"
:class="{ active: activeTab === 'visits' }"
@click="setActiveTab('visits')"
>
<text class="tab-text">探视</text>
</button>
<button
class="tab-btn"
:class="{ active: activeTab === 'announcements' }"
@click="setActiveTab('announcements')"
>
<text class="tab-text">公告</text>
</button>
</view>
<!-- Messages Tab -->
<view v-if="activeTab === 'messages'" class="messages-section">
<scroll-view class="messages-list" scroll-y="true" :style="{ height: '500px' }">
<view
v-for="message in messages"
:key="message.id"
class="message-item"
:class="{ unread: !message.is_read }"
@click="readMessage(message)"
>
<view class="message-avatar">
<text class="avatar-text">{{ message.sender_name.charAt(0) }}</text>
</view>
<view class="message-content">
<view class="message-header">
<text class="sender-name">{{ message.sender_name }}</text>
<text class="sender-role">{{ getSenderRoleText(message.sender_role) }}</text>
<text class="message-time">{{ formatDateTime(message.created_at) }}</text>
</view>
<view class="message-preview">
<text class="message-text">{{ message.content }}</text>
</view>
<view class="message-meta" v-if="message.attachments && message.attachments.length > 0">
<text class="attachment-count">📎 {{ message.attachments.length }}个附件</text>
</view>
</view>
<view class="message-actions">
<button class="action-btn small" @click.stop="replyMessage(message)">
<text class="btn-text">回复</text>
</button>
</view>
</view>
</scroll-view>
</view>
<!-- Calls Tab -->
<view v-if="activeTab === 'calls'" class="calls-section">
<view class="quick-call-section">
<text class="section-title">快速通话</text>
<view class="contact-list">
<button
v-for="contact in quickContacts"
:key="contact.id"
class="contact-btn"
@click="initiateCall(contact)"
>
<view class="contact-avatar">
<text class="avatar-text">{{ contact.name.charAt(0) }}</text>
</view>
<view class="contact-info">
<text class="contact-name">{{ contact.name }}</text>
<text class="contact-role">{{ contact.role }}</text>
</view>
<view class="contact-status">
<text class="status-indicator" :class="contact.status"></text>
</view>
</button>
</view>
</view>
<view class="call-history-section">
<text class="section-title">通话记录</text>
<scroll-view class="call-history-list" scroll-y="true" :style="{ height: '300px' }">
<view
v-for="call in callHistory"
:key="call.id"
class="call-item"
>
<view class="call-icon">
<text class="icon-text">{{ getCallIcon(call.type, call.status) }}</text>
</view>
<view class="call-info">
<text class="call-contact">{{ call.contact_name }}</text>
<text class="call-details">{{ getCallDetails(call) }}</text>
</view>
<view class="call-time">
<text class="time-text">{{ formatDateTime(call.created_at) }}</text>
</view>
<view class="call-actions">
<button class="action-btn small" @click="callBack(call)">
<text class="btn-text">回拨</text>
</button>
</view>
</view>
</scroll-view>
</view>
</view>
<!-- Visits Tab -->
<view v-if="activeTab === 'visits'" class="visits-section">
<view class="visit-request-section">
<button class="request-visit-btn" @click="showVisitRequest">
<text class="request-icon">📅</text>
<text class="request-text">申请探视</text>
</button>
</view>
<scroll-view class="visits-list" scroll-y="true" :style="{ height: '400px' }">
<view
v-for="visit in visits"
:key="visit.id"
class="visit-item"
:class="getVisitStatusClass(visit)"
>
<view class="visit-header">
<text class="visit-date">{{ formatDate(visit.visit_date) }}</text>
<text class="visit-status">{{ getVisitStatusText(visit.status) }}</text>
</view>
<view class="visit-details">
<view class="detail-row">
<text class="detail-label">时间:</text>
<text class="detail-value">{{ formatTime(visit.start_time) }} - {{ formatTime(visit.end_time) }}</text>
</view>
<view class="detail-row">
<text class="detail-label">访客:</text>
<text class="detail-value">{{ visit.visitor_name }}</text>
</view>
<view class="detail-row" v-if="visit.purpose">
<text class="detail-label">目的:</text>
<text class="detail-value">{{ visit.purpose }}</text>
</view>
</view>
<view class="visit-actions">
<button
v-if="visit.status === 'pending'"
class="action-btn small warning"
@click="cancelVisit(visit)"
>
<text class="btn-text">取消</text>
</button>
<button
v-if="visit.status === 'approved'"
class="action-btn small info"
@click="viewVisitDetails(visit)"
>
<text class="btn-text">详情</text>
</button>
</view>
</view>
</scroll-view>
</view>
<!-- Announcements Tab -->
<view v-if="activeTab === 'announcements'" class="announcements-section">
<scroll-view class="announcements-list" scroll-y="true" :style="{ height: '500px' }">
<view
v-for="announcement in announcements"
:key="announcement.id"
class="announcement-item"
:class="{ important: announcement.priority === 'high' }"
>
<view class="announcement-header">
<text class="announcement-title">{{ announcement.title }}</text>
<text class="announcement-date">{{ formatDate(announcement.created_at) }}</text>
</view>
<view class="announcement-content">
<text class="announcement-text">{{ announcement.content }}</text>
</view>
<view class="announcement-meta">
<text class="announcement-type">{{ getAnnouncementTypeText(announcement.type) }}</text>
<text class="announcement-priority" :class="announcement.priority">
{{ getPriorityText(announcement.priority) }}
</text>
</view>
</view>
</scroll-view>
</view>
<!-- New Message Modal -->
<view v-if="showNewMessageModal" class="modal-overlay" @click="hideNewMessageModal">
<view class="modal-content" @click.stop>
<view class="modal-header">
<text class="modal-title">发送消息</text>
<button class="close-btn" @click="hideNewMessageModal">
<text class="close-text">✕</text>
</button>
</view>
<view class="modal-body">
<view class="form-group">
<text class="form-label">收件人</text>
<picker
:value="newMessageRecipientIndex"
:range="recipients"
range-key="name"
@change="onNewMessageRecipientChange"
>
<text class="picker-text">{{ newMessageRecipient?.name || '选择收件人' }}</text>
</picker>
</view>
<view class="form-group">
<text class="form-label">消息内容</text>
<textarea
class="form-textarea"
placeholder="请输入消息内容"
v-model="newMessage.content"
/>
</view>
</view>
<view class="modal-footer">
<button class="cancel-btn" @click="hideNewMessageModal">
<text class="btn-text">取消</text>
</button>
<button class="confirm-btn" @click="sendMessage" :disabled="!isMessageValid">
<text class="btn-text">发送</text>
</button>
</view>
</view>
</view>
<!-- Visit Request Modal -->
<view v-if="showVisitRequestModal" class="modal-overlay" @click="hideVisitRequestModal">
<view class="modal-content" @click.stop>
<view class="modal-header">
<text class="modal-title">申请探视</text>
<button class="close-btn" @click="hideVisitRequestModal">
<text class="close-text">✕</text>
</button>
</view>
<view class="modal-body">
<view class="form-group">
<text class="form-label">探视日期</text>
<picker mode="date" @change="onVisitDateChange">
<text class="picker-text">{{ newVisit.visit_date || '选择日期' }}</text>
</picker>
</view>
<view class="form-group">
<text class="form-label">开始时间</text>
<picker mode="time" @change="onVisitStartTimeChange">
<text class="picker-text">{{ newVisit.start_time || '选择时间' }}</text>
</picker>
</view>
<view class="form-group">
<text class="form-label">结束时间</text>
<picker mode="time" @change="onVisitEndTimeChange">
<text class="picker-text">{{ newVisit.end_time || '选择时间' }}</text>
</picker>
</view>
<view class="form-group">
<text class="form-label">访客姓名</text>
<input
class="form-input"
placeholder="请输入访客姓名"
v-model="newVisit.visitor_name"
/>
</view>
<view class="form-group">
<text class="form-label">探视目的</text>
<textarea
class="form-textarea"
placeholder="请输入探视目的"
v-model="newVisit.purpose"
/>
</view>
</view>
<view class="modal-footer">
<button class="cancel-btn" @click="hideVisitRequestModal">
<text class="btn-text">取消</text>
</button>
<button class="confirm-btn" @click="submitVisitRequest" :disabled="!isVisitValid">
<text class="btn-text">提交申请</text>
</button>
</view>
</view>
</view>
</view>
</template>
<script setup lang="uts">
import { ref, computed, onMounted } from 'vue'
import { formatDateTime, formatDate, formatTime } from '../types.uts'
// Message type
type Message = {
id: string
sender_id: string
sender_name: string
sender_role: string
recipient_id: string
content: string
attachments: string[] | null
is_read: boolean
created_at: string
}
// Call record type
type CallRecord = {
id: string
contact_name: string
type: string
status: string
duration: number | null
created_at: string
}
// Visit request type
type VisitRequest = {
id: string
visit_date: string
start_time: string
end_time: string
visitor_name: string
purpose: string | null
status: string
created_at: string
}
// Announcement type
type Announcement = {
id: string
title: string
content: string
type: string
priority: string
created_at: string
}
// Contact type
type Contact = {
id: string
name: string
role: string
status: string
}
// Communication stats type
type CommunicationStats = {
unread_messages: number
call_requests: number
visit_requests: number
new_reports: number
}
// Data
const messages = ref<Message[]>([])
const callHistory = ref<CallRecord[]>([])
const visits = ref<VisitRequest[]>([])
const announcements = ref<Announcement[]>([])
const quickContacts = ref<Contact[]>([])
const recipients = ref<Contact[]>([])
const stats = ref<CommunicationStats>({
unread_messages: 0,
call_requests: 0,
visit_requests: 0,
new_reports: 0
})
const activeTab = ref('messages')
// Modal states
const showNewMessageModal = ref(false)
const showVisitRequestModal = ref(false)
// Form data
const newMessage = ref({
recipient_id: '',
content: ''
})
const newVisit = ref<VisitRequest>({
id: '',
visit_date: '',
start_time: '',
end_time: '',
visitor_name: '',
purpose: '',
status: 'pending',
created_at: ''
})
const newMessageRecipientIndex = ref(-1)
// Computed
const newMessageRecipient = computed<Contact | null>(() => {
return newMessageRecipientIndex.value >= 0 ? recipients.value[newMessageRecipientIndex.value] : null
})
const isMessageValid = computed<boolean>(() => {
return newMessage.value.recipient_id !== '' && newMessage.value.content.trim() !== ''
})
const isVisitValid = computed<boolean>(() => {
return newVisit.value.visit_date !== '' &&
newVisit.value.start_time !== '' &&
newVisit.value.end_time !== '' &&
newVisit.value.visitor_name.trim() !== ''
})
// Methods
const loadMessages = async (): Promise<void> => {
try {
const response = await supa.executeAs('rpc/get_family_messages', {
family_id: getCurrentFamilyId()
})
if (response.success && response.data) {
messages.value = response.data as Message[]
}
} catch (error) {
console.error('加载消息失败:', error)
}
}
const loadCallHistory = async (): Promise<void> => {
try {
const response = await supa.executeAs('rpc/get_family_call_history', {
family_id: getCurrentFamilyId()
})
if (response.success && response.data) {
callHistory.value = response.data as CallRecord[]
}
} catch (error) {
console.error('加载通话记录失败:', error)
}
}
const loadVisits = async (): Promise<void> => {
try {
const response = await supa.executeAs('select', {
table: 'visit_requests',
select: '*',
match: { family_id: getCurrentFamilyId() },
order: 'visit_date desc'
})
if (response.success && response.data) {
visits.value = response.data as VisitRequest[]
}
} catch (error) {
console.error('加载探视记录失败:', error)
}
}
const loadAnnouncements = async (): Promise<void> => {
try {
const response = await supa.executeAs('select', {
table: 'announcements',
select: '*',
order: 'created_at desc',
limit: 50
})
if (response.success && response.data) {
announcements.value = response.data as Announcement[]
}
} catch (error) {
console.error('加载公告失败:', error)
}
}
const loadContacts = async (): Promise<void> => {
try {
const response = await supa.executeAs('rpc/get_family_contacts', {
family_id: getCurrentFamilyId()
})
if (response.success && response.data) {
quickContacts.value = response.data as Contact[]
recipients.value = response.data as Contact[]
}
} catch (error) {
console.error('加载联系人失败:', error)
}
}
const loadStats = async (): Promise<void> => {
try {
const response = await supa.executeAs('rpc/get_family_communication_stats', {
family_id: getCurrentFamilyId()
})
if (response.success && response.data && response.data.length > 0) {
stats.value = response.data[0] as CommunicationStats
}
} catch (error) {
console.error('加载统计数据失败:', error)
}
}
const getCurrentFamilyId = (): string => {
// 这里应该从用户状态获取当前家属ID
return 'current_family_id'
}
const getSenderRoleText = (role: string): string => {
const roleMap: Record<string, string> = {
'caregiver': '护理员',
'nurse': '护士',
'doctor': '医生',
'admin': '管理员',
'family': '家属'
}
return roleMap[role] ?? role
}
const getCallIcon = (type: string, status: string): string => {
if (status === 'missed') return '📞❌'
if (type === 'video') return '📹'
return '📞'
}
const getCallDetails = (call: CallRecord): string => {
const statusMap: Record<string, string> = {
'completed': '已接听',
'missed': '未接听',
'declined': '已拒绝',
'failed': '连接失败'
}
const statusText = statusMap[call.status] ?? call.status
if (call.duration) {
const minutes = Math.floor(call.duration / 60)
const seconds = call.duration % 60
return `${statusText} · ${minutes}:${seconds.toString().padStart(2, '0')}`
}
return statusText
}
const getVisitStatusClass = (visit: VisitRequest): string => {
switch (visit.status) {
case 'pending': return 'status-pending'
case 'approved': return 'status-approved'
case 'rejected': return 'status-rejected'
case 'completed': return 'status-completed'
default: return ''
}
}
const getVisitStatusText = (status: string): string => {
const statusMap: Record<string, string> = {
'pending': '待审核',
'approved': '已批准',
'rejected': '已拒绝',
'completed': '已完成'
}
return statusMap[status] ?? status
}
const getAnnouncementTypeText = (type: string): string => {
const typeMap: Record<string, string> = {
'general': '一般通知',
'health': '健康提醒',
'activity': '活动通知',
'policy': '政策公告',
'emergency': '紧急通知'
}
return typeMap[type] ?? type
}
const getPriorityText = (priority: string): string => {
const priorityMap: Record<string, string> = {
'low': '普通',
'normal': '一般',
'high': '重要',
'urgent': '紧急'
}
return priorityMap[priority] ?? priority
}
// Event handlers
const setActiveTab = (tab: string): void => {
activeTab.value = tab
}
const readMessage = async (message: Message): Promise<void> => {
if (!message.is_read) {
try {
await supa.executeAs('update', {
table: 'messages',
data: { is_read: true },
match: { id: message.id }
})
message.is_read = true
await loadStats()
} catch (error) {
console.error('标记消息已读失败:', error)
}
}
}
const replyMessage = (message: Message): void => {
newMessage.value.recipient_id = message.sender_id
const recipientIndex = recipients.value.findIndex(r => r.id === message.sender_id)
newMessageRecipientIndex.value = recipientIndex
showNewMessageModal.value = true
}
const initiateCall = (contact: Contact): void => {
console.log('发起通话:', contact)
}
const callBack = (call: CallRecord): void => {
console.log('回拨:', call)
}
const viewVisitDetails = (visit: VisitRequest): void => {
console.log('查看探视详情:', visit)
}
const cancelVisit = async (visit: VisitRequest): Promise<void> => {
try {
const response = await supa.executeAs('update', {
table: 'visit_requests',
data: { status: 'cancelled' },
match: { id: visit.id }
})
if (response.success) {
await loadVisits()
await loadStats()
}
} catch (error) {
console.error('取消探视失败:', error)
}
}
// Modal methods
const showNewMessage = (): void => {
newMessage.value = {
recipient_id: '',
content: ''
}
newMessageRecipientIndex.value = -1
showNewMessageModal.value = true
}
const hideNewMessageModal = (): void => {
showNewMessageModal.value = false
}
const showVideoCall = (): void => {
console.log('发起视频通话')
}
const showVisitRequest = (): void => {
newVisit.value = {
id: '',
visit_date: '',
start_time: '',
end_time: '',
visitor_name: '',
purpose: '',
status: 'pending',
created_at: ''
}
showVisitRequestModal.value = true
}
const hideVisitRequestModal = (): void => {
showVisitRequestModal.value = false
}
const onNewMessageRecipientChange = (e: any): void => {
newMessageRecipientIndex.value = e.detail.value
if (newMessageRecipient.value) {
newMessage.value.recipient_id = newMessageRecipient.value.id
}
}
const onVisitDateChange = (e: any): void => {
newVisit.value.visit_date = e.detail.value
}
const onVisitStartTimeChange = (e: any): void => {
newVisit.value.start_time = e.detail.value
}
const onVisitEndTimeChange = (e: any): void => {
newVisit.value.end_time = e.detail.value
}
const sendMessage = async (): Promise<void> => {
if (!isMessageValid.value) return
try {
const response = await supa.executeAs('insert', {
table: 'messages',
data: {
sender_id: getCurrentFamilyId(),
recipient_id: newMessage.value.recipient_id,
content: newMessage.value.content,
is_read: false
}
})
if (response.success) {
hideNewMessageModal()
await loadMessages()
await loadStats()
}
} catch (error) {
console.error('发送消息失败:', error)
}
}
const submitVisitRequest = async (): Promise<void> => {
if (!isVisitValid.value) return
try {
const response = await supa.executeAs('insert', {
table: 'visit_requests',
data: {
family_id: getCurrentFamilyId(),
visit_date: newVisit.value.visit_date,
start_time: newVisit.value.start_time,
end_time: newVisit.value.end_time,
visitor_name: newVisit.value.visitor_name,
purpose: newVisit.value.purpose,
status: 'pending'
}
})
if (response.success) {
hideVisitRequestModal()
await loadVisits()
await loadStats()
}
} catch (error) {
console.error('提交探视申请失败:', error)
}
}
// Lifecycle
onMounted(async () => {
await Promise.all([
loadMessages(),
loadCallHistory(),
loadVisits(),
loadAnnouncements(),
loadContacts(),
loadStats()
])
})
</script>
<style lang="scss">
.family-communication {
padding: 20px;
background: #f5f7fa;
min-height: 100vh;
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 12px;
color: white;
.header-title {
font-size: 24px;
font-weight: 600;
}
.header-actions {
display: flex;
gap: 12px;
.action-btn {
padding: 8px 16px;
background: rgba(255, 255, 255, 0.2);
color: white;
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 8px;
font-size: 14px;
.btn-text {
color: white;
}
}
}
}
.stats-section {
display: flex;
gap: 16px;
margin-bottom: 24px;
flex-wrap: wrap;
.stat-card {
flex: 1;
min-width: 150px;
padding: 20px;
background: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
display: flex;
align-items: center;
gap: 16px;
.stat-icon {
font-size: 32px;
}
.stat-content {
.stat-number {
display: block;
font-size: 28px;
font-weight: 700;
color: #1a202c;
margin-bottom: 4px;
}
.stat-label {
font-size: 14px;
color: #64748b;
}
}
}
}
.tab-navigation {
display: flex;
background: white;
border-radius: 12px;
padding: 8px;
margin-bottom: 24px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
.tab-btn {
flex: 1;
padding: 12px 16px;
background: transparent;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
color: #6b7280;
&.active {
background: #4a90e2;
color: white;
.tab-text {
color: white;
}
}
.tab-text {
color: inherit;
}
}
}
.messages-section {
background: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
.messages-list {
padding: 16px;
.message-item {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 16px;
border-radius: 12px;
margin-bottom: 12px;
background: #f8fafc;
cursor: pointer;
&.unread {
background: #eff6ff;
border-left: 4px solid #3b82f6;
}
&:hover {
background: #e5e7eb;
}
.message-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
background: #4a90e2;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
.avatar-text {
color: white;
font-size: 16px;
font-weight: 600;
}
}
.message-content {
flex: 1;
.message-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
.sender-name {
font-size: 14px;
font-weight: 600;
color: #1a202c;
}
.sender-role {
font-size: 12px;
color: #6b7280;
background: #e5e7eb;
padding: 2px 6px;
border-radius: 4px;
}
.message-time {
font-size: 12px;
color: #9ca3af;
margin-left: auto;
}
}
.message-preview {
margin-bottom: 8px;
.message-text {
font-size: 14px;
color: #374151;
line-height: 1.4;
}
}
.message-meta {
.attachment-count {
font-size: 12px;
color: #6b7280;
}
}
}
.message-actions {
.action-btn {
padding: 4px 8px;
background: #e5e7eb;
border: none;
border-radius: 4px;
font-size: 12px;
.btn-text {
color: #374151;
}
}
}
}
}
}
.calls-section {
.quick-call-section {
background: white;
border-radius: 12px;
padding: 20px;
margin-bottom: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
.section-title {
display: block;
font-size: 16px;
font-weight: 600;
color: #1a202c;
margin-bottom: 16px;
}
.contact-list {
display: flex;
gap: 12px;
flex-wrap: wrap;
.contact-btn {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
background: #f8fafc;
border: 1px solid #e5e7eb;
border-radius: 8px;
min-width: 150px;
&:hover {
background: #e5e7eb;
}
.contact-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
background: #10b981;
display: flex;
align-items: center;
justify-content: center;
.avatar-text {
color: white;
font-size: 14px;
font-weight: 600;
}
}
.contact-info {
flex: 1;
.contact-name {
display: block;
font-size: 14px;
font-weight: 500;
color: #1a202c;
margin-bottom: 2px;
}
.contact-role {
font-size: 12px;
color: #6b7280;
}
}
.contact-status {
.status-indicator {
width: 8px;
height: 8px;
border-radius: 50%;
background: #10b981;
}
}
}
}
}
.call-history-section {
background: white;
border-radius: 12px;
padding: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
.section-title {
display: block;
font-size: 16px;
font-weight: 600;
color: #1a202c;
margin-bottom: 16px;
}
.call-history-list {
.call-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
border-radius: 8px;
margin-bottom: 8px;
&:hover {
background: #f8fafc;
}
.call-icon {
.icon-text {
font-size: 16px;
}
}
.call-info {
flex: 1;
.call-contact {
display: block;
font-size: 14px;
font-weight: 500;
color: #1a202c;
margin-bottom: 2px;
}
.call-details {
font-size: 12px;
color: #6b7280;
}
}
.call-time {
.time-text {
font-size: 12px;
color: #9ca3af;
}
}
.call-actions {
.action-btn {
padding: 4px 8px;
background: #e5e7eb;
border: none;
border-radius: 4px;
font-size: 12px;
.btn-text {
color: #374151;
}
}
}
}
}
}
}
.visits-section {
.visit-request-section {
margin-bottom: 16px;
.request-visit-btn {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 12px;
font-size: 16px;
font-weight: 500;
.request-icon {
font-size: 20px;
}
.request-text {
color: white;
}
}
}
.visits-list {
background: white;
border-radius: 12px;
padding: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
.visit-item {
padding: 16px;
border: 1px solid #e5e7eb;
border-radius: 8px;
margin-bottom: 12px;
&.status-pending {
border-left: 4px solid #f59e0b;
}
&.status-approved {
border-left: 4px solid #10b981;
}
&.status-rejected {
border-left: 4px solid #ef4444;
}
.visit-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
.visit-date {
font-size: 16px;
font-weight: 600;
color: #1a202c;
}
.visit-status {
font-size: 12px;
font-weight: 500;
padding: 4px 8px;
border-radius: 4px;
background: #e5e7eb;
color: #374151;
}
}
.visit-details {
margin-bottom: 12px;
.detail-row {
display: flex;
gap: 8px;
margin-bottom: 4px;
font-size: 14px;
.detail-label {
color: #6b7280;
min-width: 50px;
}
.detail-value {
color: #374151;
}
}
}
.visit-actions {
display: flex;
gap: 8px;
justify-content: flex-end;
.action-btn {
padding: 4px 12px;
border: none;
border-radius: 4px;
font-size: 12px;
&.warning {
background: #fef3c7;
color: #d97706;
}
&.info {
background: #e0e7ff;
color: #3730a3;
}
.btn-text {
color: inherit;
}
}
}
}
}
}
.announcements-section {
background: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
.announcements-list {
padding: 16px;
.announcement-item {
padding: 16px;
border: 1px solid #e5e7eb;
border-radius: 8px;
margin-bottom: 12px;
&.important {
border-left: 4px solid #ef4444;
background: #fef2f2;
}
.announcement-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
.announcement-title {
font-size: 16px;
font-weight: 600;
color: #1a202c;
}
.announcement-date {
font-size: 12px;
color: #9ca3af;
}
}
.announcement-content {
margin-bottom: 12px;
.announcement-text {
font-size: 14px;
color: #374151;
line-height: 1.5;
}
}
.announcement-meta {
display: flex;
gap: 12px;
.announcement-type {
font-size: 12px;
color: #6b7280;
background: #e5e7eb;
padding: 2px 6px;
border-radius: 4px;
}
.announcement-priority {
font-size: 12px;
padding: 2px 6px;
border-radius: 4px;
&.high {
background: #fef3c7;
color: #d97706;
}
&.urgent {
background: #fee2e2;
color: #dc2626;
}
}
}
}
}
}
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
.modal-content {
background: white;
border-radius: 12px;
width: 90%;
max-width: 500px;
max-height: 80vh;
overflow-y: auto;
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
border-bottom: 1px solid #e5e7eb;
.modal-title {
font-size: 18px;
font-weight: 600;
color: #1a202c;
}
.close-btn {
padding: 4px;
background: none;
border: none;
font-size: 18px;
color: #6b7280;
}
}
.modal-body {
padding: 20px;
.form-group {
margin-bottom: 16px;
.form-label {
display: block;
font-size: 14px;
font-weight: 500;
color: #374151;
margin-bottom: 8px;
}
.form-input, .form-textarea {
width: 100%;
padding: 10px 16px;
border: 1px solid #d1d5db;
border-radius: 8px;
font-size: 14px;
color: #374151;
}
.form-textarea {
height: 80px;
resize: vertical;
}
.picker-text {
padding: 10px 16px;
border: 1px solid #d1d5db;
border-radius: 8px;
background: white;
color: #374151;
width: 100%;
display: block;
}
}
}
.modal-footer {
display: flex;
gap: 12px;
justify-content: flex-end;
padding: 20px;
border-top: 1px solid #e5e7eb;
.cancel-btn, .confirm-btn {
padding: 10px 20px;
border: none;
border-radius: 8px;
font-size: 14px;
.btn-text {
color: inherit;
}
}
.cancel-btn {
background: #f3f4f6;
color: #374151;
}
.confirm-btn {
background: #4a90e2;
color: white;
&:disabled {
background: #d1d5db;
cursor: not-allowed;
}
}
}
}
}
}
</style>