feat: Ipad app production readiness, Colony orchestration, Social posting (#44)
#38 Ipad app production readiness, Colony orchestration, Social posting Co-authored-by: Sayan Datta <sayan@Sayans-MacBook-Air.local> Reviewed-on: #44
This commit was merged in pull request #44.
This commit is contained in:
@@ -1,4 +1,3 @@
|
||||
import Combine
|
||||
import SwiftUI
|
||||
|
||||
private struct CalendarAgendaItem: Identifiable {
|
||||
@@ -9,6 +8,7 @@ private struct CalendarAgendaItem: Identifiable {
|
||||
let location: String
|
||||
let type: String
|
||||
let color: Color
|
||||
let pendingSync: Bool
|
||||
let sortDate: Date?
|
||||
let event: VelocityCalendarEventDTO?
|
||||
let task: VelocityTaskDTO?
|
||||
@@ -58,15 +58,19 @@ struct CalendarView: View {
|
||||
@State private var actionError: String?
|
||||
@State private var actionMessage: String?
|
||||
@State private var actionMessageDismissTask: Task<Void, Never>?
|
||||
@State private var activeDashboardFocus: VelocityDashboardCalendarFocus?
|
||||
@State private var activeTaskMutationID: String?
|
||||
@State private var activeEventMutationID: String?
|
||||
@State private var undoCancelledTask: VelocityTaskDTO?
|
||||
@State private var undoCancelledEvent: VelocityCalendarEventDTO?
|
||||
@State private var isCreateEventPresented = false
|
||||
@State private var editingEvent: VelocityCalendarEventDTO?
|
||||
@State private var eventDraft = CalendarEventDraft()
|
||||
@State private var isCreatingEvent = false
|
||||
@State private var isSavingEvent = false
|
||||
@State private var createEventError: String?
|
||||
private let refreshTimer = Timer.publish(every: 15, on: .main, in: .common).autoconnect()
|
||||
@State private var schedulingClientPersonID: String?
|
||||
@State private var targetedDropDay: String?
|
||||
private let visibleWeekdays = Calendar.current.weekdaySymbols
|
||||
|
||||
var body: some View {
|
||||
@@ -82,10 +86,14 @@ struct CalendarView: View {
|
||||
if let actionMessage {
|
||||
successBanner(actionMessage)
|
||||
}
|
||||
if let activeDashboardFocus {
|
||||
dashboardFocusBanner(activeDashboardFocus)
|
||||
}
|
||||
if store.isLoading && store.lastRefreshAt == nil {
|
||||
loadingPanel
|
||||
} else {
|
||||
metricsRow
|
||||
clientSchedulingStrip
|
||||
HStack(alignment: .top, spacing: 18) {
|
||||
scheduleRail
|
||||
agendaPanel
|
||||
@@ -96,11 +104,14 @@ struct CalendarView: View {
|
||||
}
|
||||
.background(VelocityTheme.background)
|
||||
.scrollContentBackground(.hidden)
|
||||
.task { await store.refresh() }
|
||||
.refreshable { await store.refresh() }
|
||||
.onReceive(refreshTimer) { _ in
|
||||
Task { await store.refresh(silent: true) }
|
||||
.task {
|
||||
await store.refresh()
|
||||
consumeRequestedCalendarFocus()
|
||||
}
|
||||
.onChange(of: store.requestedCalendarFocus) { _, _ in
|
||||
consumeRequestedCalendarFocus()
|
||||
}
|
||||
.refreshable { await store.refresh() }
|
||||
.onDisappear {
|
||||
actionMessageDismissTask?.cancel()
|
||||
actionMessageDismissTask = nil
|
||||
@@ -108,6 +119,9 @@ struct CalendarView: View {
|
||||
.sheet(isPresented: $isCreateEventPresented) {
|
||||
createEventSheet
|
||||
}
|
||||
.sheet(item: $editingEvent) { event in
|
||||
editEventSheet(event)
|
||||
}
|
||||
}
|
||||
|
||||
private var header: some View {
|
||||
@@ -141,6 +155,9 @@ struct CalendarView: View {
|
||||
Button {
|
||||
selectedDay = Self.currentWeekdayName()
|
||||
actionError = nil
|
||||
withAnimation(.interactiveSpring(response: 0.35, dampingFraction: 0.86)) {
|
||||
activeDashboardFocus = nil
|
||||
}
|
||||
} label: {
|
||||
Text("This week")
|
||||
.font(.system(size: 11, weight: .semibold))
|
||||
@@ -207,14 +224,25 @@ struct CalendarView: View {
|
||||
.padding(14)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 14)
|
||||
.fill(selectedDay == day ? VelocityTheme.accent.opacity(0.12) : VelocityTheme.surface)
|
||||
.fill(targetedDropDay == day ? VelocityTheme.accent.opacity(0.22) : (selectedDay == day ? VelocityTheme.accent.opacity(0.12) : VelocityTheme.surface))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 14)
|
||||
.stroke(selectedDay == day ? VelocityTheme.borderAccent : VelocityTheme.borderSubtle, lineWidth: 1)
|
||||
.stroke(targetedDropDay == day || selectedDay == day ? VelocityTheme.borderAccent : VelocityTheme.borderSubtle, lineWidth: 1)
|
||||
)
|
||||
)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.dropDestination(for: String.self) { personIDs, _ in
|
||||
guard let personID = personIDs.first?.trimmedNonEmpty else {
|
||||
return false
|
||||
}
|
||||
Task { await scheduleSiteVisit(forPersonID: personID, on: day) }
|
||||
return true
|
||||
} isTargeted: { isTargeted in
|
||||
withAnimation(.interactiveSpring(response: 0.34, dampingFraction: 0.82)) {
|
||||
targetedDropDay = isTargeted ? day : nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(18)
|
||||
@@ -222,6 +250,46 @@ struct CalendarView: View {
|
||||
.glassCard(cornerRadius: 20)
|
||||
}
|
||||
|
||||
private var clientSchedulingStrip: some View {
|
||||
Group {
|
||||
if !store.contacts.isEmpty {
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: 10) {
|
||||
ForEach(store.contacts.prefix(12)) { contact in
|
||||
HStack(spacing: 8) {
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(VelocityTheme.accent.opacity(0.16))
|
||||
.frame(width: 32, height: 32)
|
||||
Text(initials(for: contact.fullName))
|
||||
.font(.system(size: 10, weight: .bold))
|
||||
.foregroundStyle(VelocityTheme.accent)
|
||||
}
|
||||
Text(contact.fullName)
|
||||
.font(.system(size: 12, weight: .semibold))
|
||||
.lineLimit(1)
|
||||
}
|
||||
.foregroundStyle(VelocityTheme.foreground)
|
||||
.padding(.horizontal, 10)
|
||||
.padding(.vertical, 8)
|
||||
.background(
|
||||
Capsule()
|
||||
.fill(VelocityTheme.surface)
|
||||
.overlay(Capsule().stroke(VelocityTheme.borderSubtle, lineWidth: 1))
|
||||
)
|
||||
.opacity(schedulingClientPersonID == contact.personId ? 0.55 : 1)
|
||||
.scaleEffect(schedulingClientPersonID == contact.personId ? 0.97 : 1)
|
||||
.draggable(contact.personId)
|
||||
.animation(.interactiveSpring(response: 0.32, dampingFraction: 0.86), value: schedulingClientPersonID)
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 2)
|
||||
}
|
||||
.transition(.opacity.combined(with: .move(edge: .top)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var agendaPanel: some View {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
HStack {
|
||||
@@ -263,6 +331,12 @@ struct CalendarView: View {
|
||||
.font(.system(size: 14, weight: .semibold))
|
||||
.foregroundStyle(VelocityTheme.foreground)
|
||||
Spacer()
|
||||
if item.pendingSync {
|
||||
Circle()
|
||||
.fill(VelocityTheme.warning)
|
||||
.frame(width: 8, height: 8)
|
||||
.shadow(color: VelocityTheme.warning.opacity(0.75), radius: 6)
|
||||
}
|
||||
Text(item.type)
|
||||
.font(.system(size: 10, weight: .semibold))
|
||||
.foregroundStyle(item.color)
|
||||
@@ -499,7 +573,157 @@ struct CalendarView: View {
|
||||
.presentationDragIndicator(.visible)
|
||||
}
|
||||
|
||||
private func editEventSheet(_ event: VelocityCalendarEventDTO) -> some View {
|
||||
NavigationStack {
|
||||
ZStack {
|
||||
VelocityTheme.background.ignoresSafeArea()
|
||||
VStack(alignment: .leading, spacing: 18) {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text("Edit Event")
|
||||
.font(.system(size: 24, weight: .bold))
|
||||
.foregroundStyle(VelocityTheme.foreground)
|
||||
Text("Update the backend-owned calendar slot details.")
|
||||
.font(.system(size: 12))
|
||||
.foregroundStyle(VelocityTheme.mutedFg)
|
||||
}
|
||||
|
||||
if let createEventError {
|
||||
errorBanner(createEventError)
|
||||
}
|
||||
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: 14) {
|
||||
formLabel("Title")
|
||||
eventTextField("Site visit with client", text: $eventDraft.title)
|
||||
|
||||
HStack(spacing: 12) {
|
||||
Picker("Status", selection: $eventDraft.status) {
|
||||
Text("Normal Task").tag("tentative")
|
||||
Text("Confirmed Task").tag("confirmed")
|
||||
Text("Done").tag("done")
|
||||
}
|
||||
.pickerStyle(.menu)
|
||||
.tint(VelocityTheme.accent)
|
||||
|
||||
Picker("Reminder", selection: $eventDraft.reminderMinutes) {
|
||||
Text("None").tag(0)
|
||||
Text("5 min").tag(5)
|
||||
Text("15 min").tag(15)
|
||||
Text("30 min").tag(30)
|
||||
Text("1 hour").tag(60)
|
||||
Text("1 day").tag(1_440)
|
||||
}
|
||||
.pickerStyle(.menu)
|
||||
.tint(VelocityTheme.accent)
|
||||
}
|
||||
.padding(12)
|
||||
.background(fieldBackground)
|
||||
|
||||
DatePicker(
|
||||
"Starts",
|
||||
selection: eventStartBinding,
|
||||
displayedComponents: eventDatePickerComponents
|
||||
)
|
||||
.tint(VelocityTheme.accent)
|
||||
.foregroundStyle(VelocityTheme.foreground)
|
||||
.padding(12)
|
||||
.background(fieldBackground)
|
||||
|
||||
if !eventDraft.allDay {
|
||||
DatePicker(
|
||||
"Ends",
|
||||
selection: $eventDraft.endDate,
|
||||
in: eventDraft.startDate.addingTimeInterval(60)...,
|
||||
displayedComponents: [.date, .hourAndMinute]
|
||||
)
|
||||
.tint(VelocityTheme.accent)
|
||||
.foregroundStyle(VelocityTheme.foreground)
|
||||
.padding(12)
|
||||
.background(fieldBackground)
|
||||
}
|
||||
|
||||
formLabel("Location")
|
||||
eventTextField("Project site, sales lounge, video call", text: $eventDraft.location)
|
||||
|
||||
formLabel("Description")
|
||||
TextEditor(text: $eventDraft.description)
|
||||
.font(.system(size: 14))
|
||||
.foregroundStyle(VelocityTheme.foreground)
|
||||
.scrollContentBackground(.hidden)
|
||||
.frame(minHeight: 110)
|
||||
.padding(10)
|
||||
.background(fieldBackground)
|
||||
}
|
||||
.padding(18)
|
||||
.glassCard(cornerRadius: 18)
|
||||
}
|
||||
.frame(height: 436)
|
||||
|
||||
HStack(spacing: 12) {
|
||||
Button {
|
||||
editingEvent = nil
|
||||
} label: {
|
||||
Text("Cancel")
|
||||
.font(.system(size: 14, weight: .semibold))
|
||||
.foregroundStyle(VelocityTheme.foreground)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 13)
|
||||
.background(fieldBackground)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
||||
Button {
|
||||
saveEventEdits(event)
|
||||
} label: {
|
||||
HStack(spacing: 8) {
|
||||
if isSavingEvent {
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||||
}
|
||||
Text(isSavingEvent ? "Saving..." : "Save Event")
|
||||
}
|
||||
.font(.system(size: 14, weight: .semibold))
|
||||
.foregroundStyle(.white)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 13)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.fill(eventDraft.isValid && !isSavingEvent ? VelocityTheme.accent : VelocityTheme.surface3)
|
||||
)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.disabled(!eventDraft.isValid || isSavingEvent)
|
||||
}
|
||||
}
|
||||
.padding(24)
|
||||
.frame(maxWidth: 620)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
.presentationDetents([.height(690)])
|
||||
.presentationDragIndicator(.visible)
|
||||
}
|
||||
|
||||
private var filteredAgendaItems: [CalendarAgendaItem] {
|
||||
if let activeDashboardFocus {
|
||||
switch activeDashboardFocus {
|
||||
case .today:
|
||||
let weekday = Self.currentWeekdayName().lowercased()
|
||||
return agendaItems.filter {
|
||||
$0.slot.lowercased().contains(weekday) && !isInactiveAgendaItem($0)
|
||||
}
|
||||
case .pendingTasks:
|
||||
return agendaItems.filter { item in
|
||||
guard let task = item.task else { return false }
|
||||
return ["pending", "snoozed", "confirmed"].contains(task.status.lowercased())
|
||||
}
|
||||
case .urgentTasks:
|
||||
return agendaItems.filter { item in
|
||||
guard let task = item.task else { return false }
|
||||
return ["urgent", "high"].contains(task.priority.lowercased()) && !isInactiveAgendaItem(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
let weekday = selectedDay.lowercased()
|
||||
return agendaItems.filter { $0.slot.lowercased().contains(weekday) }
|
||||
}
|
||||
@@ -514,6 +738,7 @@ struct CalendarView: View {
|
||||
location: event.location ?? "No location",
|
||||
type: eventStatusLabel(event.status),
|
||||
color: color(for: event.status),
|
||||
pendingSync: store.pendingSyncCalendarEventIDs.contains(event.calendarEventId) || event.calendarEventId.hasPrefix("local-"),
|
||||
sortDate: event.startDate,
|
||||
event: event,
|
||||
task: nil
|
||||
@@ -528,6 +753,7 @@ struct CalendarView: View {
|
||||
location: task.clientPhone ?? "Canonical CRM task",
|
||||
type: taskStatusLabel(task),
|
||||
color: taskColor(for: task),
|
||||
pendingSync: store.pendingSyncTaskIDs.contains(task.reminderId),
|
||||
sortDate: task.dueDate,
|
||||
event: nil,
|
||||
task: task
|
||||
@@ -673,6 +899,34 @@ struct CalendarView: View {
|
||||
)
|
||||
}
|
||||
|
||||
private func dashboardFocusBanner(_ focus: VelocityDashboardCalendarFocus) -> some View {
|
||||
HStack(spacing: 10) {
|
||||
Label(calendarFocusLabel(focus), systemImage: "line.3.horizontal.decrease.circle")
|
||||
.font(.system(size: 12, weight: .semibold))
|
||||
.foregroundStyle(VelocityTheme.warning)
|
||||
Text(calendarFocusDescription(focus))
|
||||
.font(.system(size: 12, weight: .medium))
|
||||
.foregroundStyle(VelocityTheme.mutedFg)
|
||||
Spacer()
|
||||
Button("Clear") {
|
||||
withAnimation(.interactiveSpring(response: 0.35, dampingFraction: 0.86)) {
|
||||
activeDashboardFocus = nil
|
||||
}
|
||||
}
|
||||
.font(.system(size: 12, weight: .semibold))
|
||||
.foregroundStyle(VelocityTheme.accent)
|
||||
}
|
||||
.padding(14)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 14)
|
||||
.fill(VelocityTheme.warning.opacity(0.10))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 14)
|
||||
.stroke(VelocityTheme.warning.opacity(0.22), lineWidth: 1)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private func buildMetrics(
|
||||
events: [VelocityCalendarEventDTO],
|
||||
tasks: [VelocityTaskDTO],
|
||||
@@ -884,6 +1138,14 @@ struct CalendarView: View {
|
||||
private func eventActionsMenu(_ event: VelocityCalendarEventDTO) -> some View {
|
||||
Menu {
|
||||
let status = event.status.lowercased()
|
||||
if status != "cancelled" {
|
||||
Button {
|
||||
presentEditEvent(event)
|
||||
} label: {
|
||||
Label("Edit Event", systemImage: "square.and.pencil")
|
||||
}
|
||||
}
|
||||
|
||||
if status == "done" {
|
||||
Button(role: .destructive) {
|
||||
cancelEvent(event, message: "Task removed.", supportsUndo: false)
|
||||
@@ -946,6 +1208,79 @@ struct CalendarView: View {
|
||||
isCreateEventPresented = true
|
||||
}
|
||||
|
||||
private func presentEditEvent(_ event: VelocityCalendarEventDTO) {
|
||||
let startDate = event.startDate ?? CalendarEventDraft.defaultStartDate()
|
||||
var draft = CalendarEventDraft(startDate: startDate)
|
||||
draft.title = event.title
|
||||
draft.description = event.description ?? ""
|
||||
draft.location = event.location ?? ""
|
||||
draft.endDate = event.endDate ?? startDate.addingTimeInterval(60 * 60)
|
||||
draft.allDay = event.allDay
|
||||
draft.status = event.status.lowercased()
|
||||
draft.reminderMinutes = event.reminderMinutes.first ?? 0
|
||||
eventDraft = draft
|
||||
createEventError = nil
|
||||
actionError = nil
|
||||
clearActionMessage()
|
||||
editingEvent = event
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func scheduleSiteVisit(forPersonID personID: String, on weekday: String) async {
|
||||
guard schedulingClientPersonID == nil else {
|
||||
return
|
||||
}
|
||||
guard let contact = store.contacts.first(where: { $0.personId == personID }) else {
|
||||
actionError = "Unable to schedule: this client is not present in the canonical CRM payload."
|
||||
return
|
||||
}
|
||||
guard let leadId = contact.leadId?.trimmedNonEmpty else {
|
||||
actionError = "Unable to schedule \(contact.fullName): no canonical lead is attached."
|
||||
return
|
||||
}
|
||||
|
||||
let startDate = defaultEventStartDate(for: weekday)
|
||||
let endDate = startDate.addingTimeInterval(60 * 60)
|
||||
var metadata = [
|
||||
"created_from": "ipad_calendar_drag_drop",
|
||||
"surface": "velocity_ipad",
|
||||
"person_id": contact.personId,
|
||||
"client_name": contact.fullName,
|
||||
]
|
||||
if let phone = contact.primaryPhone?.trimmedNonEmpty {
|
||||
metadata["client_phone"] = phone
|
||||
}
|
||||
|
||||
schedulingClientPersonID = personID
|
||||
actionError = nil
|
||||
clearActionMessage()
|
||||
|
||||
do {
|
||||
_ = try await store.createCalendarEvent(
|
||||
leadId: leadId,
|
||||
title: "Site visit with \(contact.fullName)",
|
||||
description: contact.primaryInterest.flatMap { "Property focus: \($0)".trimmedNonEmpty },
|
||||
startAt: iso8601Timestamp(startDate),
|
||||
endAt: iso8601Timestamp(endDate),
|
||||
allDay: false,
|
||||
status: "confirmed",
|
||||
reminderMinutes: [60, 15],
|
||||
location: contact.primaryInterest?.trimmedNonEmpty ?? "Project site",
|
||||
metadata: metadata
|
||||
)
|
||||
withAnimation(.interactiveSpring(response: 0.42, dampingFraction: 0.86)) {
|
||||
selectedDay = weekday
|
||||
schedulingClientPersonID = nil
|
||||
targetedDropDay = nil
|
||||
}
|
||||
showActionMessage("Site visit scheduled for \(contact.fullName).")
|
||||
} catch {
|
||||
schedulingClientPersonID = nil
|
||||
targetedDropDay = nil
|
||||
actionError = calendarActionErrorMessage(error)
|
||||
}
|
||||
}
|
||||
|
||||
private func createEvent() {
|
||||
guard eventDraft.isValid else {
|
||||
createEventError = "Add an event title and make sure the end time is after the start time."
|
||||
@@ -1004,6 +1339,50 @@ struct CalendarView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func saveEventEdits(_ event: VelocityCalendarEventDTO) {
|
||||
guard eventDraft.isValid else {
|
||||
createEventError = "Add an event title and make sure the end time is after the start time."
|
||||
return
|
||||
}
|
||||
|
||||
let calendar = Calendar.current
|
||||
let startDate = eventDraft.allDay ? calendar.startOfDay(for: eventDraft.startDate) : eventDraft.startDate
|
||||
let endDate = eventDraft.allDay
|
||||
? (calendar.date(byAdding: .day, value: 1, to: startDate) ?? startDate.addingTimeInterval(24 * 60 * 60))
|
||||
: eventDraft.endDate
|
||||
let reminderMinutes = eventDraft.reminderMinutes > 0 ? [eventDraft.reminderMinutes] : []
|
||||
|
||||
createEventError = nil
|
||||
isSavingEvent = true
|
||||
|
||||
Task {
|
||||
do {
|
||||
_ = try await store.updateCalendarEvent(
|
||||
event,
|
||||
title: eventDraft.title.trimmedNonEmpty ?? event.title,
|
||||
description: eventDraft.description,
|
||||
status: eventDraft.status,
|
||||
startAt: iso8601Timestamp(startDate),
|
||||
endAt: iso8601Timestamp(endDate),
|
||||
reminderMinutes: reminderMinutes,
|
||||
location: eventDraft.location
|
||||
)
|
||||
await MainActor.run {
|
||||
selectedDay = weekdayName(for: startDate)
|
||||
isSavingEvent = false
|
||||
editingEvent = nil
|
||||
showActionMessage("Event updated.")
|
||||
actionError = nil
|
||||
}
|
||||
} catch {
|
||||
await MainActor.run {
|
||||
isSavingEvent = false
|
||||
createEventError = calendarActionErrorMessage(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func mutateEvent(
|
||||
_ event: VelocityCalendarEventDTO,
|
||||
status: String,
|
||||
@@ -1170,6 +1549,41 @@ struct CalendarView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func consumeRequestedCalendarFocus() {
|
||||
guard let focus = store.requestedCalendarFocus else {
|
||||
return
|
||||
}
|
||||
store.requestedCalendarFocus = nil
|
||||
withAnimation(.interactiveSpring(response: 0.35, dampingFraction: 0.86)) {
|
||||
activeDashboardFocus = focus
|
||||
if focus == .today {
|
||||
selectedDay = Self.currentWeekdayName()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func calendarFocusLabel(_ focus: VelocityDashboardCalendarFocus) -> String {
|
||||
switch focus {
|
||||
case .today:
|
||||
return "Today"
|
||||
case .pendingTasks:
|
||||
return "Pending tasks"
|
||||
case .urgentTasks:
|
||||
return "Urgent tasks"
|
||||
}
|
||||
}
|
||||
|
||||
private func calendarFocusDescription(_ focus: VelocityDashboardCalendarFocus) -> String {
|
||||
switch focus {
|
||||
case .today:
|
||||
return "Showing today’s confirmed events and CRM reminders."
|
||||
case .pendingTasks:
|
||||
return "Showing actionable CRM reminders across the week."
|
||||
case .urgentTasks:
|
||||
return "Showing high-priority and urgent CRM reminders."
|
||||
}
|
||||
}
|
||||
|
||||
private func clearActionMessage(clearUndo: Bool = true) {
|
||||
actionMessageDismissTask?.cancel()
|
||||
actionMessageDismissTask = nil
|
||||
@@ -1233,6 +1647,15 @@ struct CalendarView: View {
|
||||
return formatter.string(from: date)
|
||||
}
|
||||
|
||||
private func initials(for name: String) -> String {
|
||||
let pieces = name
|
||||
.split(separator: " ")
|
||||
.prefix(2)
|
||||
.compactMap { $0.first }
|
||||
let initials = String(pieces).uppercased()
|
||||
return initials.isEmpty ? "CL" : initials
|
||||
}
|
||||
|
||||
private func menuIcon(_ systemName: String) -> some View {
|
||||
Image(systemName: systemName)
|
||||
.font(.system(size: 17, weight: .semibold))
|
||||
|
||||
Reference in New Issue
Block a user