wip
This commit is contained in:
@@ -74,10 +74,18 @@ data class TaskDetail(
|
|||||||
@Serializable
|
@Serializable
|
||||||
data class TasksByResidenceResponse(
|
data class TasksByResidenceResponse(
|
||||||
@SerialName("residence_id") val residenceId: String,
|
@SerialName("residence_id") val residenceId: String,
|
||||||
val summary: TaskSummary,
|
@SerialName("days_threshold") val daysThreshold: Int,
|
||||||
val tasks: List<TaskDetail>,
|
val summary: CategorizedTaskSummary,
|
||||||
@SerialName("completed_tasks") val completedTasks: List<TaskDetail> = emptyList(),
|
@SerialName("upcoming_tasks") val upcomingTasks: List<TaskDetail>,
|
||||||
@SerialName("cancelled_tasks") val cancelledTasks: List<TaskDetail> = emptyList()
|
@SerialName("in_progress_tasks") val inProgressTasks: List<TaskDetail>,
|
||||||
|
@SerialName("done_tasks") val doneTasks: List<TaskDetail>
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class CategorizedTaskSummary(
|
||||||
|
val upcoming: Int,
|
||||||
|
@SerialName("in_progress") val inProgress: Int,
|
||||||
|
val done: Int
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
|||||||
@@ -94,10 +94,15 @@ class TaskApi(private val client: HttpClient = ApiClient.httpClient) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getTasksByResidence(token: String, residenceId: Int): ApiResult<TasksByResidenceResponse> {
|
suspend fun getTasksByResidence(
|
||||||
|
token: String,
|
||||||
|
residenceId: Int,
|
||||||
|
days: Int = 30
|
||||||
|
): ApiResult<TasksByResidenceResponse> {
|
||||||
return try {
|
return try {
|
||||||
val response = client.get("$baseUrl/tasks/by-residence/$residenceId/") {
|
val response = client.get("$baseUrl/tasks/by-residence/$residenceId/") {
|
||||||
header("Authorization", "Token $token")
|
header("Authorization", "Token $token")
|
||||||
|
parameter("days", days)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.status.isSuccess()) {
|
if (response.status.isSuccess()) {
|
||||||
|
|||||||
@@ -48,8 +48,8 @@ fun ResidenceDetailScreen(
|
|||||||
var showCompleteDialog by remember { mutableStateOf(false) }
|
var showCompleteDialog by remember { mutableStateOf(false) }
|
||||||
var selectedTask by remember { mutableStateOf<TaskDetail?>(null) }
|
var selectedTask by remember { mutableStateOf<TaskDetail?>(null) }
|
||||||
var showNewTaskDialog by remember { mutableStateOf(false) }
|
var showNewTaskDialog by remember { mutableStateOf(false) }
|
||||||
var showCompletedTasks by remember { mutableStateOf(false) }
|
var showInProgressTasks by remember { mutableStateOf(false) }
|
||||||
var showCancelledTasks by remember { mutableStateOf(false) }
|
var showDoneTasks by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
LaunchedEffect(residenceId) {
|
LaunchedEffect(residenceId) {
|
||||||
residenceViewModel.getResidence(residenceId) { result ->
|
residenceViewModel.getResidence(residenceId) { result ->
|
||||||
@@ -394,7 +394,7 @@ fun ResidenceDetailScreen(
|
|||||||
}
|
}
|
||||||
is ApiResult.Success -> {
|
is ApiResult.Success -> {
|
||||||
val taskData = (tasksState as ApiResult.Success).data
|
val taskData = (tasksState as ApiResult.Success).data
|
||||||
if (taskData.tasks.isEmpty() && taskData.completedTasks.isEmpty() && taskData.cancelledTasks.isEmpty()) {
|
if (taskData.upcomingTasks.isEmpty() && taskData.inProgressTasks.isEmpty() && taskData.doneTasks.isEmpty()) {
|
||||||
item {
|
item {
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
@@ -427,7 +427,8 @@ fun ResidenceDetailScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
items(taskData.tasks) { task ->
|
// Upcoming tasks section
|
||||||
|
items(taskData.upcomingTasks) { task ->
|
||||||
TaskCard(
|
TaskCard(
|
||||||
task = task,
|
task = task,
|
||||||
onCompleteClick = {
|
onCompleteClick = {
|
||||||
@@ -444,14 +445,69 @@ fun ResidenceDetailScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Completed tasks section
|
// In Progress tasks section
|
||||||
if (taskData.completedTasks.isNotEmpty()) {
|
if (taskData.inProgressTasks.isNotEmpty()) {
|
||||||
item {
|
item {
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clickable { showCompletedTasks = !showCompletedTasks }
|
.clickable { showInProgressTasks = !showInProgressTasks }
|
||||||
|
.padding(vertical = 8.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.PlayCircle,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.tertiary,
|
||||||
|
modifier = Modifier.size(28.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text(
|
||||||
|
text = "In Progress (${taskData.inProgressTasks.size})",
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = MaterialTheme.colorScheme.tertiary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Icon(
|
||||||
|
if (showInProgressTasks) Icons.Default.KeyboardArrowUp else Icons.Default.KeyboardArrowDown,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showInProgressTasks) {
|
||||||
|
items(taskData.inProgressTasks) { task ->
|
||||||
|
TaskCard(
|
||||||
|
task = task,
|
||||||
|
onCompleteClick = {
|
||||||
|
selectedTask = task
|
||||||
|
showCompleteDialog = true
|
||||||
|
},
|
||||||
|
onEditClick = {
|
||||||
|
onNavigateToEditTask(task)
|
||||||
|
},
|
||||||
|
onCancelClick = {
|
||||||
|
residenceViewModel.cancelTask(task.id)
|
||||||
|
},
|
||||||
|
onUncancelClick = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Done tasks section
|
||||||
|
if (taskData.doneTasks.isNotEmpty()) {
|
||||||
|
item {
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable { showDoneTasks = !showDoneTasks }
|
||||||
.padding(vertical = 8.dp),
|
.padding(vertical = 8.dp),
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
@@ -460,77 +516,27 @@ fun ResidenceDetailScreen(
|
|||||||
Icon(
|
Icon(
|
||||||
Icons.Default.CheckCircle,
|
Icons.Default.CheckCircle,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint = MaterialTheme.colorScheme.tertiary,
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
modifier = Modifier.size(28.dp)
|
modifier = Modifier.size(28.dp)
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
Text(
|
Text(
|
||||||
text = "Completed Tasks (${taskData.completedTasks.size})",
|
text = "Done (${taskData.doneTasks.size})",
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
color = MaterialTheme.colorScheme.tertiary
|
color = MaterialTheme.colorScheme.primary
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Icon(
|
Icon(
|
||||||
if (showCompletedTasks) Icons.Default.KeyboardArrowUp else Icons.Default.KeyboardArrowDown,
|
if (showDoneTasks) Icons.Default.KeyboardArrowUp else Icons.Default.KeyboardArrowDown,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showCompletedTasks) {
|
if (showDoneTasks) {
|
||||||
items(taskData.completedTasks) { task ->
|
items(taskData.doneTasks) { task ->
|
||||||
TaskCard(
|
|
||||||
task = task,
|
|
||||||
onCompleteClick = null,
|
|
||||||
onEditClick = {
|
|
||||||
onNavigateToEditTask(task)
|
|
||||||
},
|
|
||||||
onCancelClick = null,
|
|
||||||
onUncancelClick = null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cancelled tasks section
|
|
||||||
if (taskData.cancelledTasks.isNotEmpty()) {
|
|
||||||
item {
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clickable { showCancelledTasks = !showCancelledTasks }
|
|
||||||
.padding(vertical = 8.dp),
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
|
||||||
Icon(
|
|
||||||
Icons.Default.Cancel,
|
|
||||||
contentDescription = null,
|
|
||||||
tint = MaterialTheme.colorScheme.error,
|
|
||||||
modifier = Modifier.size(28.dp)
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
|
||||||
Text(
|
|
||||||
text = "Cancelled Tasks (${taskData.cancelledTasks.size})",
|
|
||||||
style = MaterialTheme.typography.titleMedium,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
color = MaterialTheme.colorScheme.error
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Icon(
|
|
||||||
if (showCancelledTasks) Icons.Default.KeyboardArrowUp else Icons.Default.KeyboardArrowDown,
|
|
||||||
contentDescription = null,
|
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showCancelledTasks) {
|
|
||||||
items(taskData.cancelledTasks) { task ->
|
|
||||||
TaskCard(
|
TaskCard(
|
||||||
task = task,
|
task = task,
|
||||||
onCompleteClick = null,
|
onCompleteClick = null,
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ struct ResidenceDetailView: View {
|
|||||||
@State private var showEditResidence = false
|
@State private var showEditResidence = false
|
||||||
@State private var showEditTask = false
|
@State private var showEditTask = false
|
||||||
@State private var selectedTaskForEdit: TaskDetail?
|
@State private var selectedTaskForEdit: TaskDetail?
|
||||||
@State private var showCompletedTasks = false
|
@State private var showInProgressTasks = false
|
||||||
@State private var showCancelledTasks = false
|
@State private var showDoneTasks = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
@@ -38,8 +38,8 @@ struct ResidenceDetailView: View {
|
|||||||
if let tasksResponse = tasksResponse {
|
if let tasksResponse = tasksResponse {
|
||||||
TasksSection(
|
TasksSection(
|
||||||
tasksResponse: tasksResponse,
|
tasksResponse: tasksResponse,
|
||||||
showCompletedTasks: $showCompletedTasks,
|
showInProgressTasks: $showInProgressTasks,
|
||||||
showCancelledTasks: $showCancelledTasks,
|
showDoneTasks: $showDoneTasks,
|
||||||
onEditTask: { task in
|
onEditTask: { task in
|
||||||
selectedTaskForEdit = task
|
selectedTaskForEdit = task
|
||||||
showEditTask = true
|
showEditTask = true
|
||||||
@@ -134,7 +134,7 @@ struct ResidenceDetailView: View {
|
|||||||
tasksError = nil
|
tasksError = nil
|
||||||
|
|
||||||
let taskApi = TaskApi(client: ApiClient_iosKt.createHttpClient())
|
let taskApi = TaskApi(client: ApiClient_iosKt.createHttpClient())
|
||||||
taskApi.getTasksByResidence(token: token, residenceId: residenceId) { result, error in
|
taskApi.getTasksByResidence(token: token, residenceId: residenceId, days: 30) { result, error in
|
||||||
if let successResult = result as? ApiResultSuccess<TasksByResidenceResponse> {
|
if let successResult = result as? ApiResultSuccess<TasksByResidenceResponse> {
|
||||||
self.tasksResponse = successResult.data
|
self.tasksResponse = successResult.data
|
||||||
self.isLoadingTasks = false
|
self.isLoadingTasks = false
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import ComposeApp
|
|||||||
|
|
||||||
struct TasksSection: View {
|
struct TasksSection: View {
|
||||||
let tasksResponse: TasksByResidenceResponse
|
let tasksResponse: TasksByResidenceResponse
|
||||||
@Binding var showCompletedTasks: Bool
|
@Binding var showInProgressTasks: Bool
|
||||||
@Binding var showCancelledTasks: Bool
|
@Binding var showDoneTasks: Bool
|
||||||
let onEditTask: (TaskDetail) -> Void
|
let onEditTask: (TaskDetail) -> Void
|
||||||
let onCancelTask: (TaskDetail) -> Void
|
let onCancelTask: (TaskDetail) -> Void
|
||||||
let onUncancelTask: (TaskDetail) -> Void
|
let onUncancelTask: (TaskDetail) -> Void
|
||||||
@@ -19,16 +19,17 @@ struct TasksSection: View {
|
|||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
HStack(spacing: 8) {
|
HStack(spacing: 8) {
|
||||||
TaskPill(count: tasksResponse.summary.total, label: "Total", color: .blue)
|
TaskPill(count: Int32(tasksResponse.summary.upcoming), label: "Upcoming", color: .blue)
|
||||||
TaskPill(count: tasksResponse.summary.pending, label: "Pending", color: .orange)
|
TaskPill(count: Int32(tasksResponse.summary.inProgress), label: "In Progress", color: .orange)
|
||||||
TaskPill(count: tasksResponse.summary.completed, label: "Done", color: .green)
|
TaskPill(count: Int32(tasksResponse.summary.done), label: "Done", color: .green)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if tasksResponse.tasks.isEmpty && tasksResponse.completedTasks.isEmpty && tasksResponse.cancelledTasks.isEmpty {
|
if tasksResponse.upcomingTasks.isEmpty && tasksResponse.inProgressTasks.isEmpty && tasksResponse.doneTasks.isEmpty {
|
||||||
EmptyTasksView()
|
EmptyTasksView()
|
||||||
} else {
|
} else {
|
||||||
ForEach(tasksResponse.tasks, id: \.id) { task in
|
// Upcoming tasks
|
||||||
|
ForEach(tasksResponse.upcomingTasks, id: \.id) { task in
|
||||||
TaskCard(
|
TaskCard(
|
||||||
task: task,
|
task: task,
|
||||||
onEdit: { onEditTask(task) },
|
onEdit: { onEditTask(task) },
|
||||||
@@ -37,31 +38,32 @@ struct TasksSection: View {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !tasksResponse.completedTasks.isEmpty {
|
// In Progress tasks section
|
||||||
|
if !tasksResponse.inProgressTasks.isEmpty {
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
HStack {
|
HStack {
|
||||||
Label("Completed Tasks (\(tasksResponse.completedTasks.count))", systemImage: "checkmark.circle")
|
Label("In Progress (\(tasksResponse.inProgressTasks.count))", systemImage: "play.circle")
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
.foregroundColor(.green)
|
.foregroundColor(.orange)
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
Image(systemName: showCompletedTasks ? "chevron.up" : "chevron.down")
|
Image(systemName: showInProgressTasks ? "chevron.up" : "chevron.down")
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
}
|
}
|
||||||
.padding(.top, 8)
|
.padding(.top, 8)
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
showCompletedTasks.toggle()
|
showInProgressTasks.toggle()
|
||||||
}
|
}
|
||||||
|
|
||||||
if showCompletedTasks {
|
if showInProgressTasks {
|
||||||
ForEach(tasksResponse.completedTasks, id: \.id) { task in
|
ForEach(tasksResponse.inProgressTasks, id: \.id) { task in
|
||||||
TaskCard(
|
TaskCard(
|
||||||
task: task,
|
task: task,
|
||||||
onEdit: { onEditTask(task) },
|
onEdit: { onEditTask(task) },
|
||||||
onCancel: nil,
|
onCancel: { onCancelTask(task) },
|
||||||
onUncancel: nil
|
onUncancel: nil
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -69,32 +71,33 @@ struct TasksSection: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !tasksResponse.cancelledTasks.isEmpty {
|
// Done tasks section
|
||||||
|
if !tasksResponse.doneTasks.isEmpty {
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
HStack {
|
HStack {
|
||||||
Label("Cancelled Tasks (\(tasksResponse.cancelledTasks.count))", systemImage: "xmark.circle")
|
Label("Done (\(tasksResponse.doneTasks.count))", systemImage: "checkmark.circle")
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
.foregroundColor(.red)
|
.foregroundColor(.green)
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
Image(systemName: showCancelledTasks ? "chevron.up" : "chevron.down")
|
Image(systemName: showDoneTasks ? "chevron.up" : "chevron.down")
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
}
|
}
|
||||||
.padding(.top, 8)
|
.padding(.top, 8)
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
showCancelledTasks.toggle()
|
showDoneTasks.toggle()
|
||||||
}
|
}
|
||||||
|
|
||||||
if showCancelledTasks {
|
if showDoneTasks {
|
||||||
ForEach(tasksResponse.cancelledTasks, id: \.id) { task in
|
ForEach(tasksResponse.doneTasks, id: \.id) { task in
|
||||||
TaskCard(
|
TaskCard(
|
||||||
task: task,
|
task: task,
|
||||||
onEdit: { onEditTask(task) },
|
onEdit: { onEditTask(task) },
|
||||||
onCancel: nil,
|
onCancel: nil,
|
||||||
onUncancel: { onUncancelTask(task) }
|
onUncancel: nil
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -109,14 +112,13 @@ struct TasksSection: View {
|
|||||||
TasksSection(
|
TasksSection(
|
||||||
tasksResponse: TasksByResidenceResponse(
|
tasksResponse: TasksByResidenceResponse(
|
||||||
residenceId: "1",
|
residenceId: "1",
|
||||||
summary: TaskSummary(
|
daysThreshold: 30,
|
||||||
total: 3,
|
summary: CategorizedTaskSummary(
|
||||||
completed: 1,
|
upcoming: 3,
|
||||||
pending: 2,
|
inProgress: 1,
|
||||||
inProgress: 0,
|
done: 2
|
||||||
overdue: 1
|
|
||||||
),
|
),
|
||||||
tasks: [
|
upcomingTasks: [
|
||||||
TaskDetail(
|
TaskDetail(
|
||||||
id: 1,
|
id: 1,
|
||||||
residence: 1,
|
residence: 1,
|
||||||
@@ -137,7 +139,8 @@ struct TasksSection: View {
|
|||||||
completions: []
|
completions: []
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
completedTasks: [
|
inProgressTasks: [],
|
||||||
|
doneTasks: [
|
||||||
TaskDetail(
|
TaskDetail(
|
||||||
id: 2,
|
id: 2,
|
||||||
residence: 1,
|
residence: 1,
|
||||||
@@ -157,11 +160,10 @@ struct TasksSection: View {
|
|||||||
showCompletedButton: false,
|
showCompletedButton: false,
|
||||||
completions: []
|
completions: []
|
||||||
)
|
)
|
||||||
],
|
]
|
||||||
cancelledTasks: []
|
|
||||||
),
|
),
|
||||||
showCompletedTasks: .constant(true),
|
showInProgressTasks: .constant(true),
|
||||||
showCancelledTasks: .constant(true),
|
showDoneTasks: .constant(true),
|
||||||
onEditTask: { _ in },
|
onEditTask: { _ in },
|
||||||
onCancelTask: { _ in },
|
onCancelTask: { _ in },
|
||||||
onUncancelTask: { _ in }
|
onUncancelTask: { _ in }
|
||||||
|
|||||||
Reference in New Issue
Block a user