548 lines
18 KiB
Go
548 lines
18 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"encoding/json"
|
|
"mime"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.118im.com/im_team/im_workbench/internal/store"
|
|
)
|
|
|
|
func workbenchModulePayloads(modules []store.WorkbenchModule) []map[string]any {
|
|
items := make([]map[string]any, 0, len(modules))
|
|
for _, module := range modules {
|
|
items = append(items, map[string]any{
|
|
"code": module.Code,
|
|
"name": module.Name,
|
|
"icon_file_id": module.IconFileID,
|
|
"route": module.Route,
|
|
"sort_order": module.SortOrder,
|
|
"unread_count": module.UnreadCount,
|
|
"enabled": module.Enabled,
|
|
"permission": firstNonEmpty(module.Permission, "enabled"),
|
|
})
|
|
}
|
|
return items
|
|
}
|
|
|
|
func organizationDepartmentPayloads(departments []store.OrganizationDepartment) []map[string]any {
|
|
items := make([]map[string]any, 0, len(departments))
|
|
for _, department := range departments {
|
|
items = append(items, organizationDepartmentPayload(department))
|
|
}
|
|
return items
|
|
}
|
|
|
|
func organizationDepartmentPayload(department store.OrganizationDepartment) map[string]any {
|
|
return map[string]any{
|
|
"department_id": department.DepartmentID,
|
|
"name": department.Name,
|
|
"parent_id": department.ParentID,
|
|
"child_count": department.ChildCount,
|
|
"member_count": department.MemberCount,
|
|
"version": department.Version,
|
|
}
|
|
}
|
|
|
|
func organizationMemberPayloads(members []store.OrganizationMember) []map[string]any {
|
|
items := make([]map[string]any, 0, len(members))
|
|
for _, member := range members {
|
|
items = append(items, organizationMemberPayload(member))
|
|
}
|
|
return items
|
|
}
|
|
|
|
func organizationMemberPayload(member store.OrganizationMember) map[string]any {
|
|
payload := map[string]any{
|
|
"user_id": formatUserID(member.UserID),
|
|
"department_id": member.DepartmentID,
|
|
"display_name": member.DisplayName,
|
|
"position": member.Position,
|
|
"avatar_file_id": member.AvatarFileID,
|
|
"status": member.Status,
|
|
"contactable": member.Contactable,
|
|
"version": member.Version,
|
|
}
|
|
if member.Mobile != "" {
|
|
payload["mobile"] = member.Mobile
|
|
}
|
|
if member.Email != "" {
|
|
payload["email"] = member.Email
|
|
}
|
|
return payload
|
|
}
|
|
|
|
func attendanceConfigPayload(config *store.AttendanceConfig) map[string]any {
|
|
return map[string]any{
|
|
"date": config.Date.Format("2006-01-02"),
|
|
"timezone": config.Timezone,
|
|
"rule": attendanceRulePayload(config.Rule),
|
|
"shifts": attendanceShiftPayloads(config.Shifts),
|
|
"allowed_actions": []string{"check_in", "check_out"},
|
|
"today_record": attendanceRecordPayload(config.TodayRecord),
|
|
"permissions": map[string]any{
|
|
"can_checkin": true,
|
|
"can_view_managed": true,
|
|
"can_appeal": config.Rule.AppealAllowed,
|
|
},
|
|
}
|
|
}
|
|
|
|
func attendanceRulePayload(rule store.AttendanceRule) map[string]any {
|
|
data := map[string]any{
|
|
"rule_id": rule.RuleID,
|
|
"name": rule.Name,
|
|
"workday": rule.Workday,
|
|
"location_required": rule.LocationRequired,
|
|
"wifi_required": rule.WifiRequired,
|
|
"allow_outside_checkin": rule.AllowOutsideCheckin,
|
|
"appeal_allowed": rule.AppealAllowed,
|
|
}
|
|
if rule.AppealDeadline != nil {
|
|
data["appeal_deadline"] = rule.AppealDeadline.UTC().Format(time.RFC3339Nano)
|
|
}
|
|
return data
|
|
}
|
|
|
|
func attendanceShiftPayloads(shifts []store.AttendanceShift) []map[string]any {
|
|
items := make([]map[string]any, 0, len(shifts))
|
|
for _, shift := range shifts {
|
|
items = append(items, map[string]any{
|
|
"shift_id": shift.ShiftID,
|
|
"name": shift.Name,
|
|
"work_start": shift.WorkStart,
|
|
"work_end": shift.WorkEnd,
|
|
"checkin_start": shift.CheckinStart,
|
|
"checkout_end": shift.CheckoutEnd,
|
|
})
|
|
}
|
|
return items
|
|
}
|
|
|
|
func attendanceRecordPayloads(records []store.AttendanceRecord) []map[string]any {
|
|
items := make([]map[string]any, 0, len(records))
|
|
for i := range records {
|
|
items = append(items, attendanceRecordPayload(&records[i]))
|
|
}
|
|
return items
|
|
}
|
|
|
|
func attendanceRecordPayload(record *store.AttendanceRecord) map[string]any {
|
|
if record == nil {
|
|
return nil
|
|
}
|
|
return map[string]any{
|
|
"record_id": record.RecordID,
|
|
"user_id": formatUserID(record.UserID),
|
|
"date": record.Date.Format("2006-01-02"),
|
|
"rule_id": record.RuleID,
|
|
"shift_id": record.ShiftID,
|
|
"status": record.Status,
|
|
"checkins": attendanceCheckinPayloads(record.Checkins),
|
|
"exceptions": jsonList(record.Exceptions),
|
|
"appeal_status": record.AppealStatus,
|
|
"version": record.Version,
|
|
"created_at": record.CreatedAt.UTC().Format(time.RFC3339Nano),
|
|
"updated_at": record.UpdatedAt.UTC().Format(time.RFC3339Nano),
|
|
}
|
|
}
|
|
|
|
func attendanceCheckinPayloads(checkins []store.AttendanceCheckin) []map[string]any {
|
|
items := make([]map[string]any, 0, len(checkins))
|
|
for _, checkin := range checkins {
|
|
items = append(items, attendanceCheckinPayload(checkin))
|
|
}
|
|
return items
|
|
}
|
|
|
|
func attendanceCheckinPayload(checkin store.AttendanceCheckin) map[string]any {
|
|
return map[string]any{
|
|
"checkin_id": checkin.CheckinID,
|
|
"action": checkin.Action,
|
|
"client_time": checkin.ClientTime.UTC().Format(time.RFC3339Nano),
|
|
"server_time": checkin.ServerTime.UTC().Format(time.RFC3339Nano),
|
|
"result": checkin.Result,
|
|
"location": jsonObject(checkin.Location),
|
|
"wifi": jsonObject(checkin.Wifi),
|
|
"device": jsonObject(checkin.Device),
|
|
}
|
|
}
|
|
|
|
func attendanceStatsPayload(stats *store.AttendanceStats) map[string]any {
|
|
if stats == nil {
|
|
return map[string]any{"expected_count": 0, "checked_count": 0, "exception_count": 0, "not_checked_count": 0}
|
|
}
|
|
return map[string]any{
|
|
"expected_count": stats.ExpectedCount,
|
|
"checked_count": stats.CheckedCount,
|
|
"exception_count": stats.ExceptionCount,
|
|
"not_checked_count": stats.NotCheckedCount,
|
|
}
|
|
}
|
|
|
|
func attendanceAppealPayloads(appeals []store.AttendanceAppeal) []map[string]any {
|
|
items := make([]map[string]any, 0, len(appeals))
|
|
for i := range appeals {
|
|
items = append(items, attendanceAppealPayload(&appeals[i]))
|
|
}
|
|
return items
|
|
}
|
|
|
|
func attendanceAppealPayload(appeal *store.AttendanceAppeal) map[string]any {
|
|
if appeal == nil {
|
|
return nil
|
|
}
|
|
data := map[string]any{
|
|
"appeal_id": appeal.AppealID,
|
|
"record_id": appeal.RecordID,
|
|
"user_id": formatUserID(appeal.UserID),
|
|
"appeal_type": appeal.AppealType,
|
|
"reason": appeal.Reason,
|
|
"target_action": appeal.TargetAction,
|
|
"attachment_file_ids": jsonList(appeal.AttachmentFileIDs),
|
|
"status": appeal.Status,
|
|
"version": appeal.Version,
|
|
"submitted_at": appeal.SubmittedAt.UTC().Format(time.RFC3339Nano),
|
|
"created_at": appeal.CreatedAt.UTC().Format(time.RFC3339Nano),
|
|
"updated_at": appeal.UpdatedAt.UTC().Format(time.RFC3339Nano),
|
|
}
|
|
if appeal.TargetTime != nil {
|
|
data["target_time"] = appeal.TargetTime.UTC().Format(time.RFC3339Nano)
|
|
}
|
|
return data
|
|
}
|
|
|
|
func meetingPayloads(meetings []store.Meeting, viewer int64) []map[string]any {
|
|
items := make([]map[string]any, 0, len(meetings))
|
|
for _, meeting := range meetings {
|
|
items = append(items, meetingPayload(meeting, viewer))
|
|
}
|
|
return items
|
|
}
|
|
|
|
func meetingPayload(meeting store.Meeting, viewer int64) map[string]any {
|
|
participants := meetingParticipantPayloads(meeting.Participants)
|
|
recorderID := meeting.RecorderID
|
|
if recorderID == 0 {
|
|
recorderID = meeting.OrganizerID
|
|
}
|
|
organizer := userSummary(meeting.OrganizerID, meeting.Organizer)
|
|
recorder := userSummary(recorderID, meeting.Recorder)
|
|
state := meetingState(meeting)
|
|
data := map[string]any{
|
|
"meeting_id": meeting.MeetingID,
|
|
"title": meeting.Title,
|
|
"organizer_id": formatUserID(meeting.OrganizerID),
|
|
"organizer": organizer,
|
|
"organizer_display_name": organizer["display_name"],
|
|
"recorder_user_id": formatUserID(recorderID),
|
|
"recorder": recorder,
|
|
"recorder_display_name": recorder["display_name"],
|
|
"start_at": meeting.StartAt.UTC().Format(time.RFC3339Nano),
|
|
"end_at": meeting.EndAt.UTC().Format(time.RFC3339Nano),
|
|
"meeting_time": timeRange(meeting.StartAt, meeting.EndAt),
|
|
"time_range": timeRange(meeting.StartAt, meeting.EndAt),
|
|
"location": meeting.Location,
|
|
"state": state,
|
|
"reminder_minutes": jsonList(meeting.ReminderMinutes),
|
|
"version": meeting.Version,
|
|
"participants": participants,
|
|
"participant_count": len(participants),
|
|
"participant_names": meetingParticipantNames(participants),
|
|
"permissions": meetingPermissions(meeting, viewer),
|
|
"created_at": meeting.CreatedAt.UTC().Format(time.RFC3339Nano),
|
|
"updated_at": meeting.UpdatedAt.UTC().Format(time.RFC3339Nano),
|
|
}
|
|
if meeting.CanceledAt != nil {
|
|
data["canceled_at"] = meeting.CanceledAt.UTC().Format(time.RFC3339Nano)
|
|
}
|
|
for _, participant := range meeting.Participants {
|
|
if participant.UserID == viewer {
|
|
data["my_response"] = participant.Response
|
|
break
|
|
}
|
|
}
|
|
return data
|
|
}
|
|
|
|
func meetingParticipantPayloads(participants []store.MeetingParticipant) []map[string]any {
|
|
items := make([]map[string]any, 0, len(participants))
|
|
for _, participant := range participants {
|
|
summary := userSummary(participant.UserID, participant.User)
|
|
item := map[string]any{"user_id": formatUserID(participant.UserID), "response": participant.Response, "display_name": summary["display_name"]}
|
|
if avatarFileID, ok := summary["avatar_file_id"]; ok {
|
|
item["avatar_file_id"] = avatarFileID
|
|
}
|
|
if participant.ResponseAt != nil {
|
|
item["response_at"] = participant.ResponseAt.UTC().Format(time.RFC3339Nano)
|
|
}
|
|
items = append(items, item)
|
|
}
|
|
return items
|
|
}
|
|
|
|
func meetingNoteListPayloads(meetings []store.Meeting, viewer int64) []map[string]any {
|
|
items := make([]map[string]any, 0, len(meetings))
|
|
for _, meeting := range meetings {
|
|
item := meetingPayload(meeting, viewer)
|
|
item["note_status"] = "empty"
|
|
item["file_count"] = 0
|
|
item["permissions"] = meetingNotePermissions(meeting, viewer)
|
|
items = append(items, item)
|
|
}
|
|
return items
|
|
}
|
|
|
|
func meetingNotesDetailPayload(meeting store.Meeting, files []store.MeetingNoteFile, viewer int64) map[string]any {
|
|
meetingData := meetingPayload(meeting, viewer)
|
|
fileItems := make([]map[string]any, 0, len(files))
|
|
for _, file := range files {
|
|
fileItems = append(fileItems, meetingNoteFilePayload(file, viewer))
|
|
}
|
|
data := map[string]any{}
|
|
for k, v := range meetingData {
|
|
data[k] = v
|
|
}
|
|
data["meeting"] = meetingData
|
|
data["note_status"] = "empty"
|
|
if len(files) > 0 {
|
|
data["note_status"] = "available"
|
|
}
|
|
data["file_count"] = len(files)
|
|
data["files"] = fileItems
|
|
data["permissions"] = meetingNotePermissions(meeting, viewer)
|
|
return data
|
|
}
|
|
|
|
func meetingNoteFilePayload(file store.MeetingNoteFile, viewer int64) map[string]any {
|
|
extension := meetingNoteFileExtension(file.FileName, file.MIMEType)
|
|
return map[string]any{
|
|
"note_file_id": file.NoteFileID,
|
|
"meeting_id": file.MeetingID,
|
|
"file_id": file.FileID,
|
|
"name": file.FileName,
|
|
"file_name": file.FileName,
|
|
"mime": file.MIMEType,
|
|
"mime_type": file.MIMEType,
|
|
"size": file.FileSize,
|
|
"file_size": file.FileSize,
|
|
"file_extension": extension,
|
|
"file_etag": file.FileETag,
|
|
"status": file.Status,
|
|
"preview_status": file.PreviewStatus,
|
|
"preview_mode": file.PreviewMode,
|
|
"preview_file_id": file.PreviewFileID,
|
|
"uploader_user_id": formatUserID(file.UploaderID),
|
|
"uploader": userSummary(file.UploaderID, store.UserSummary{}),
|
|
"version": file.Version,
|
|
"permissions": map[string]any{
|
|
"can_preview": file.Status == "active",
|
|
"can_download": file.Status == "active",
|
|
"can_replace": file.Status == "active" && file.UploaderID == viewer,
|
|
"can_delete": file.Status == "active" && file.UploaderID == viewer,
|
|
},
|
|
"created_at": file.CreatedAt.UTC().Format(time.RFC3339Nano),
|
|
"updated_at": file.UpdatedAt.UTC().Format(time.RFC3339Nano),
|
|
}
|
|
}
|
|
|
|
func meetingNoteFileExtension(fileName, mimeType string) string {
|
|
if ext := strings.ToLower(strings.TrimPrefix(filepath.Ext(strings.TrimSpace(fileName)), ".")); ext != "" {
|
|
return ext
|
|
}
|
|
exts, _ := mime.ExtensionsByType(strings.TrimSpace(mimeType))
|
|
if len(exts) == 0 {
|
|
return ""
|
|
}
|
|
return strings.ToLower(strings.TrimPrefix(exts[0], "."))
|
|
}
|
|
|
|
func dailyReportPayloads(reports []store.DailyReport) []map[string]any {
|
|
items := make([]map[string]any, 0, len(reports))
|
|
for _, report := range reports {
|
|
items = append(items, dailyReportPayload(report))
|
|
}
|
|
return items
|
|
}
|
|
|
|
func dailyReportPayload(report store.DailyReport) map[string]any {
|
|
data := map[string]any{
|
|
"report_id": report.ReportID,
|
|
"author_id": formatUserID(report.AuthorID),
|
|
"report_date": report.ReportDate.Format("2006-01-02"),
|
|
"content": report.Content,
|
|
"content_short": preview(report.Content),
|
|
"attachments": jsonList(report.Attachments),
|
|
"status": report.Status,
|
|
"read_state": report.ReadState,
|
|
"version": report.Version,
|
|
"draft_updated_at": report.UpdatedAt.UTC().Format(time.RFC3339Nano),
|
|
"created_at": report.CreatedAt.UTC().Format(time.RFC3339Nano),
|
|
"updated_at": report.UpdatedAt.UTC().Format(time.RFC3339Nano),
|
|
}
|
|
if report.SubmittedAt != nil {
|
|
data["submitted_at"] = report.SubmittedAt.UTC().Format(time.RFC3339Nano)
|
|
}
|
|
return data
|
|
}
|
|
|
|
func monthlyReportPayloads(reports []store.MonthlyReport) []map[string]any {
|
|
items := make([]map[string]any, 0, len(reports))
|
|
for _, report := range reports {
|
|
items = append(items, monthlyReportPayload(report))
|
|
}
|
|
return items
|
|
}
|
|
|
|
func monthlyReportPayload(report store.MonthlyReport) map[string]any {
|
|
data := map[string]any{
|
|
"report_id": report.ReportID,
|
|
"author_id": formatUserID(report.AuthorID),
|
|
"month": report.Month,
|
|
"work_summary": report.WorkSummary,
|
|
"next_plan": report.NextPlan,
|
|
"attachments": jsonList(report.Attachments),
|
|
"status": report.Status,
|
|
"read_state": report.ReadState,
|
|
"version": report.Version,
|
|
"draft_updated_at": report.UpdatedAt.UTC().Format(time.RFC3339Nano),
|
|
"created_at": report.CreatedAt.UTC().Format(time.RFC3339Nano),
|
|
"updated_at": report.UpdatedAt.UTC().Format(time.RFC3339Nano),
|
|
}
|
|
if report.SubmittedAt != nil {
|
|
data["submitted_at"] = report.SubmittedAt.UTC().Format(time.RFC3339Nano)
|
|
}
|
|
return data
|
|
}
|
|
|
|
func reportStatsPayload(stats *store.ReportStats) map[string]any {
|
|
if stats == nil {
|
|
return map[string]any{
|
|
"expected_count": 0,
|
|
"submitted_count": 0,
|
|
"missing_count": 0,
|
|
"scope": map[string]any{"department_id": ""},
|
|
}
|
|
}
|
|
return map[string]any{
|
|
"expected_count": stats.ExpectedCount,
|
|
"submitted_count": stats.SubmittedCount,
|
|
"missing_count": stats.MissingCount,
|
|
"scope": map[string]any{"department_id": stats.DepartmentID},
|
|
}
|
|
}
|
|
|
|
func mailboxConfigPayload(config store.MailboxConfig) map[string]any {
|
|
return map[string]any{
|
|
"enabled": config.Enabled,
|
|
"allow_anonymous": config.AllowAnonymous,
|
|
"required_fields": jsonList(config.RequiredFields),
|
|
"max_length": config.MaxLength,
|
|
"attachment_policy": jsonObject(config.AttachmentPolicy),
|
|
"notice": map[string]any{
|
|
"title": config.NoticeTitle,
|
|
"content": config.NoticeContent,
|
|
"version": config.Version,
|
|
},
|
|
}
|
|
}
|
|
|
|
func mailboxSubmissionPayload(submission store.MailboxSubmission) map[string]any {
|
|
return map[string]any{
|
|
"submission_id": submission.SubmissionID,
|
|
"sender_id": formatUserID(submission.SenderID),
|
|
"subject": submission.Subject,
|
|
"anonymous": submission.Anonymous,
|
|
"attachment_file_ids": jsonList(submission.AttachmentFileIDs),
|
|
"notice_version": submission.NoticeVersion,
|
|
"status": submission.Status,
|
|
"submitted_at": submission.SubmittedAt.UTC().Format(time.RFC3339Nano),
|
|
}
|
|
}
|
|
|
|
func meetingPermissions(meeting store.Meeting, viewer int64) map[string]any {
|
|
isOrganizer := meeting.OrganizerID == viewer
|
|
isScheduled := meetingState(meeting) == "scheduled"
|
|
return map[string]any{
|
|
"can_update": isOrganizer && isScheduled,
|
|
"can_cancel": isOrganizer && isScheduled,
|
|
"can_remind": isOrganizer && isScheduled,
|
|
"can_respond": !isOrganizer && isScheduled,
|
|
}
|
|
}
|
|
|
|
func meetingNotePermissions(meeting store.Meeting, viewer int64) map[string]any {
|
|
recorderID := meeting.RecorderID
|
|
if recorderID == 0 {
|
|
recorderID = meeting.OrganizerID
|
|
}
|
|
readable := meetingState(meeting) == "ended"
|
|
canWrite := readable && (recorderID == viewer || meeting.OrganizerID == viewer)
|
|
return map[string]any{"can_view": true, "can_upload": canWrite, "can_replace": canWrite, "can_delete": canWrite, "can_download": true}
|
|
}
|
|
|
|
func userSummary(userID int64, summary store.UserSummary) map[string]any {
|
|
displayName := strings.TrimSpace(summary.DisplayName)
|
|
if displayName == "" {
|
|
displayName = formatUserID(userID)
|
|
}
|
|
data := map[string]any{"user_id": formatUserID(userID), "display_name": displayName}
|
|
if strings.TrimSpace(summary.AvatarFileID) != "" {
|
|
data["avatar_file_id"] = strings.TrimSpace(summary.AvatarFileID)
|
|
}
|
|
return data
|
|
}
|
|
|
|
func meetingState(meeting store.Meeting) string {
|
|
state := strings.TrimSpace(meeting.State)
|
|
if state == "scheduled" && !meeting.EndAt.IsZero() && !time.Now().UTC().Before(meeting.EndAt.UTC()) {
|
|
return "ended"
|
|
}
|
|
return state
|
|
}
|
|
|
|
func meetingParticipantNames(participants []map[string]any) []string {
|
|
names := make([]string, 0, len(participants))
|
|
for _, participant := range participants {
|
|
name, _ := participant["display_name"].(string)
|
|
name = strings.TrimSpace(name)
|
|
if name != "" {
|
|
names = append(names, name)
|
|
}
|
|
}
|
|
return names
|
|
}
|
|
|
|
func jsonList(raw string) []any {
|
|
var items []any
|
|
if err := json.Unmarshal([]byte(strings.TrimSpace(raw)), &items); err != nil {
|
|
return []any{}
|
|
}
|
|
return items
|
|
}
|
|
|
|
func jsonObject(raw string) map[string]any {
|
|
var item map[string]any
|
|
if err := json.Unmarshal([]byte(strings.TrimSpace(raw)), &item); err != nil {
|
|
return map[string]any{}
|
|
}
|
|
return item
|
|
}
|
|
|
|
func preview(value string) string {
|
|
runes := []rune(strings.TrimSpace(value))
|
|
if len(runes) <= 40 {
|
|
return string(runes)
|
|
}
|
|
return string(runes[:40])
|
|
}
|
|
|
|
func timeRange(startAt, endAt time.Time) string {
|
|
if startAt.Format("2006-01-02") == endAt.Format("2006-01-02") {
|
|
return startAt.Format("2006-01-02 15:04") + "-" + endAt.Format("15:04")
|
|
}
|
|
return startAt.Format("2006-01-02 15:04") + "-" + endAt.Format("2006-01-02 15:04")
|
|
}
|