Display Planner MyTasks using spfx webpart
Hi,
The requirement was
- To display MyTasks from the planner in Outlook 365.
- By default in the webpart, tasks should be displayed for today that are assigned to me
- In the webpart, if date is selected, my tasks should be displayed as per the selected date.
Below is the source code that has been written to achieve the outcome
//IPlannerSpProps.ts
import { WebPartContext } from "@microsoft/sp-webpart-base";
export interface IPlannerSpProps {
description: string;
isDarkTheme: boolean;
environmentMessage: string;
hasTeamsContext: boolean;
userDisplayName: string;
context:WebPartContext
}
//PlannerSpWebPart.ts
import * as React from 'react';
import * as ReactDom from 'react-dom';
import { Version } from '@microsoft/sp-core-library';
import {
type IPropertyPaneConfiguration,
PropertyPaneTextField
} from '@microsoft/sp-property-pane';
import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base';
import { IReadonlyTheme } from '@microsoft/sp-component-base'
import * as strings from 'PlannerSpWebPartStrings';
import { IPlannerSpProps } from './components/IPlannerSpProps'
import TaskDashboard from './components/PlannerSPTask';
export interface IPlannerSpWebPartProps {
description: string;
}
export default class PlannerSpWebPart extends BaseClientSideWebPart<
private _isDarkTheme: boolean = false;
private _environmentMessage: string = '';
public render(): void {
const element: React.ReactElement<
TaskDashboard,
{
description: this.properties.description,
isDarkTheme: this._isDarkTheme,
context:this.context,
environmentMessage: this._environmentMessage,
hasTeamsContext: !!this.context.sdks.
userDisplayName: this.context.pageContext.user.
}
);
ReactDom.render(element, this.domElement);
}
protected onInit(): Promise<void> {
return this._getEnvironmentMessage().
this._environmentMessage = message;
});
}
private _getEnvironmentMessage(): Promise<string> {
if (!!this.context.sdks.
return this.context.sdks.
.then(context => {
let environmentMessage: string = '';
switch (context.app.host.name) {
case 'Office': // running in Office
environmentMessage = this.context.
break;
case 'Outlook': // running in Outlook
environmentMessage = this.context.
break;
case 'Teams': // running in Teams
case 'TeamsModern':
environmentMessage = this.context.
break;
default:
environmentMessage = strings.UnknownEnvironment;
}
return environmentMessage;
});
}
return Promise.resolve(this.context.
}
protected onThemeChanged(currentTheme: IReadonlyTheme | undefined): void {
if (!currentTheme) {
return;
}
this._isDarkTheme = !!currentTheme.isInverted;
const {
semanticColors
} = currentTheme;
if (semanticColors) {
this.domElement.style.
this.domElement.style.
this.domElement.style.
}
}
protected onDispose(): void {
ReactDom.
}
protected get dataVersion(): Version {
return Version.parse('1.0');
}
protected getPropertyPaneConfiguration()
return {
pages: [
{
header: {
description: strings.
},
groups: [
{
groupName: strings.BasicGroupName,
groupFields: [
PropertyPaneTextField('
label: strings.DescriptionFieldLabel
})
]
}
]
}
]
};
}
}
//PlannerSPTask.tsx
import * as React from 'react';
import { Stack, StackItem } from '@fluentui/react/lib/Stack';
import { Text } from '@fluentui/react/lib/Text';
import { DocumentCard } from '@fluentui/react/lib/
import { Clock, CheckCircle, AlertCircle, BugIcon } from 'lucide-react';
import { IPlannerSpProps } from './IPlannerSpProps';
import styles from './PlannerSp.module.scss';
import { DatePicker, DetailsList, Dialog, DialogFooter, IColumn, IconButton, PrimaryButton, SelectionMode, Spinner } from '@fluentui/react';
import * as MicrosoftGraph from "@microsoft/microsoft-graph-
interface ITaskDashboardState {
showTaskModal: boolean;
selectedTask: IPlannerTask | null;
selectedDate: Date | undefined;
isSortedDescending: boolean;
tasks: IPlannerTask[];
filteredTasks: IPlannerTask[];
buckets: { [key: string]: string };
loading: boolean;
noRecordsFound: boolean;
message: any;
error: string | null;
taskStats: {
notStarted: number;
inProgress: number;
completed: number;
overdue: number;
};
}
interface IPlannerTask {
id: string;
title: string;
bucketId: string;
startDateTime: string | any | null;
percentComplete: number;
createdDateTime: string;
dueDateTime: string | null;
priority: number;
description?: string;
status: 'notStarted' | 'inProgress' | 'completed';
assignments: {
[key: string]: {
assignedDateTime: string;
assignedBy: {
user: {
id: string;
}
}
}
};
checklistItemCount?: number;
activeChecklistItemCount?: number;
checklist?: { // Add checklist property
[key: string]: {
isChecked: boolean;
title: string;
orderHint: string;
lastModifiedDateTime: string;
lastModifiedBy: {
user: {
displayName: string | null;
id: string;
}
}
}
};
}
let tenantId: any
export default class TaskDashboard extends React.Component<
private columns: IColumn[];
constructor(props: IPlannerSpProps) {
super(props);
this.state = {
showTaskModal: false,
selectedTask: null,
selectedDate: new Date(),
isSortedDescending: false,
tasks: [],
filteredTasks: [],
buckets: {},
loading: true,
error: null,
taskStats: {
notStarted: 0,
inProgress: 0,
completed: 0,
overdue: 0,
},
noRecordsFound: false,
message: '',
};
this.columns = this.getColumns();
}
componentDidMount() {
this.fetchPlannerData();
const today = new Date();
this.setState({ selectedDate: today });
this.onDateChange(today);
}
private getColumns(): IColumn[] {
return [
{
key: 'title',
name: 'Task Title',
fieldName: 'title',
minWidth: 150,
maxWidth: 200,
onRender: (item: IPlannerTask) => (
<div>
<IconButton
iconProps={{ iconName: 'view' }}
title="Open in Planner"
onClick={() => this.openTaskInPlanner(item.id
/>
<a href={`#task-${item.id}`}
className={styles.taskLink}
onClick={() => this.onTaskClick(item)}>
{item.title}
</a>
</div>
)
},
{
key: 'bucketName',
name: 'Bucket',
minWidth: 100,
maxWidth: 150,
onRender: (item: IPlannerTask) => this.state.buckets[item.
},
{
key: 'dueDateTime',
name: 'Due Date',
minWidth: 100,
maxWidth: 120,
onRender: (item: IPlannerTask) => item.dueDateTime ? new Date(item.dueDateTime).
},
{
key: 'Progress',
name: 'Progress',
minWidth: 100,
maxWidth: 120,
onRender: (item: IPlannerTask) => this.getStatusDisplay(item)
},
{
key: 'priority',
name: 'Priority',
minWidth: 90,
maxWidth: 100,
onRender: (item: IPlannerTask) => this.getPriorityDisplay(item.
},
{
key: 'percentage',
name: 'percentage',
minWidth: 90,
maxWidth: 100,
onRender: (item: IPlannerTask) => `${item.percentComplete}%`
},
{
key: 'description',
name: 'Description',
minWidth: 200,
maxWidth: 300,
onRender: (item: IPlannerTask) => (
<Text variant="medium"
className={styles.description}
>
{item.description || 'No description'}
</Text>
)
},
// New Column: Checklist
{
key: 'checklist',
name: 'Checklist',
// width: 'max-content',
minWidth: 400, // Minimum width
maxWidth: 800, // Maximum width
onRender: (item: IPlannerTask) => (
<div>
{item.checklist && Object.values(item.checklist).
<div key={index}
className={styles.
>
<input
type="checkbox"
checked={checklistItem.
readOnly
/>
<Text variant="medium">{
</div>
))}
</div>
),
}
];
}
private openTaskInPlanner = (taskId: string) => {
const url = `https://planner.cloud.
window.open(url, '_blank');
};
private getStatusDisplay(task: IPlannerTask): JSX.Element {
const status = this.calculateTaskStatus(task)
const statusColors = {
notStarted: 'gray',
inProgress: 'orange',
completed: 'green',
overdue: 'red'
};
return (
<span style={{ color: statusColors[status] }}>
{status.charAt(0).toUpperCase(
</span>
);
}
private getPriorityDisplay(priority: number): string {
if (priority === 0 || priority === 1) {
return 'Urgent';
} else if (priority >= 2 && priority <= 4) {
return 'Important';
} else if (priority >= 5 && priority <= 7) {
return 'Medium';
} else if (priority >= 8 && priority <= 10) {
return 'Low';
} else {
return 'Normal';
}
}
private calculateTaskStatus(task: IPlannerTask): 'notStarted' | 'inProgress' | 'completed' | 'overdue' {
// Calculate percentage completion based on checklist items
const percentComplete: any = task.percentComplete;
if (percentComplete === 100) return 'completed';
// if (task.dueDateTime && new Date(task.dueDateTime) < new Date(task.startDateTime)) return 'overdue';
if (percentComplete > 0) return 'inProgress';
return 'notStarted';
}
private async fetchPlannerData() {
try {
const client = await this.props.context.
const tenantIdInfo = await client.api('/organization').
tenantId = tenantIdInfo.value[0].id;
const tasksResponse = await client.api('/me/planner/tasks'
.select('id,title,bucketId,
.get();
const tasks: IPlannerTask[] = tasksResponse.value;
// Filter tasks with a due date of today
const today = new Date();
const filteredTasks = tasks.filter(task =>
task.dueDateTime && new Date(task.dueDateTime).
);
// Fetch task details (description and checklist) for each task
const tasksWithDetails = await Promise.all(
filteredTasks.map(async (task) => {
const taskDetails = await this.fetchTaskDetails(task.id)
return {
...task,
description: taskDetails.description,
checklist: taskDetails.checklist,
};
})
);
// Get unique bucket IDs from tasks
const bucketIds = tasksResponse.value
.map((task: IPlannerTask) => task.bucketId)
.filter((id, index, self) => self.indexOf(id) === index);
// Fetch bucket names
const bucketNames: { [key: string]: string } = {};
await Promise.all(bucketIds.map(
try {
const bucketResponse = await client.api(`/planner/buckets/$
.select('id,name')
.get();
bucketNames[bucketId] = bucketResponse.name;
} catch (error) {
console.error(`Error fetching bucket ${bucketId}:`, error);
bucketNames[bucketId] = 'Unknown Bucket';
}
}));
// Calculate task statistics
const taskStats = this.calculateTaskStats(
if (tasksWithDetails.length === 0) {
this.setState({
tasks,
filteredTasks: [],
buckets: bucketNames,
taskStats,
loading: false,
noRecordsFound: true,
selectedDate: new Date(),
message: `No records found for due date ${today.toISOString().split('
});
} else {
this.setState({
tasks,
filteredTasks: tasksWithDetails,
buckets: bucketNames,
taskStats,
loading: false,
noRecordsFound: false,
message: ''
});
}
} catch (error) {
console.error('Error fetching planner data:', error);
this.setState({
loading: false,
error: 'Error loading tasks. Please try again later.'
});
}
}
private async fetchTaskDetails(taskId: string): Promise<{ description?: string; checklist?: any }> {
try {
const client = await this.props.context.
const taskDetailsResponse = await client.api(`/planner/tasks/${
return {
description: taskDetailsResponse.
checklist: taskDetailsResponse.checklist,
};
} catch (error) {
console.error(`Error fetching task details for task ${taskId}:`, error);
return {};
}
}
private calculateTaskStats(tasks: IPlannerTask[]): {
notStarted: number;
inProgress: number;
completed: number;
overdue: number;
} {
return tasks.reduce((stats, task) => {
const status = this.calculateTaskStatus(task)
if (status === 'notStarted') stats.notStarted++;
else if (status === 'inProgress') stats.inProgress++;
else if (status === 'completed') stats.completed++;
else if (status === 'overdue') stats.overdue++;
return stats;
}, { notStarted: 0, inProgress: 0, completed: 0, overdue: 0 });
}
private onDateChange = async (date: Date | null | undefined): Promise<void> => {
const { tasks } = this.state;
if (date) {
const filteredTasks = tasks.filter(task =>
task.dueDateTime && new Date(task.dueDateTime).
);
const tasksWithDetails = await Promise.all(
filteredTasks.map(async (task) => {
const taskDetails = await this.fetchTaskDetails(task.id)
return {
...task,
description: taskDetails.description,
checklist: taskDetails.checklist,
};
})
);
if (tasksWithDetails.length === 0) {
this.setState({
selectedDate: date,
filteredTasks: [],
noRecordsFound: true,
message: `No records found for due date ${date.toISOString().split('T'
});
} else {
this.setState({
selectedDate: date,
filteredTasks: tasksWithDetails,
noRecordsFound: false,
message: ''
});
}
}
else {
const tasksWithDetails = await Promise.all(
tasks.map(async (task) => {
const taskDetails = await this.fetchTaskDetails(task.id)
return {
...task,
description: taskDetails.description,
checklist: taskDetails.checklist,
};
})
);
const taskStats = this.calculateTaskStats(
this.setState({
selectedDate: undefined,
filteredTasks: tasksWithDetails,
noRecordsFound: false,
message: '',
taskStats
});
}
};
private onColumnClick = (ev: React.MouseEvent<HTMLElement>, column: IColumn): void => {
const { filteredTasks, isSortedDescending } = this.state;
const newItems = [...filteredTasks].sort((a, b) => {
const aValue = this.getColumnValue(a, column.key);
const bValue = this.getColumnValue(b, column.key);
return (isSortedDescending ? -1 : 1) * (aValue > bValue ? 1 : -1);
});
this.setState({
filteredTasks: newItems,
isSortedDescending: !isSortedDescending
});
}
private getColumnValue(item: IPlannerTask, columnKey: string): any {
switch (columnKey) {
case 'bucketName':
return this.state.buckets[item.
case 'status':
return this.calculateTaskStatus(item)
default:
return item[columnKey as keyof IPlannerTask];
}
}
private onTaskClick = (task: IPlannerTask): void => {
this.setState({ selectedTask: task, showTaskModal: true });
}
private closeDialog = (): void => {
this.setState({ showTaskModal: false, selectedTask: null });
}
public isdataExist = () => {
const { noRecordsFound } = this.state;
if (noRecordsFound) {
return true;
}
else {
return false;
}
}
render() {
const { filteredTasks, selectedTask, showTaskModal, message, loading, error, taskStats } = this.state;
if (loading) {
return <Spinner label="Loading tasks..." />;
}
if (error) {
return <Text variant="large" className={styles.error}>{
}
const content = this.isdataExist() ? (
<div className={styles.
<Text variant="xLarge" className={styles.taskHeading}
My Tasks</Text>
<Stack horizontal tokens={{ childrenGap: 8 }} className={styles.dateFilter}>
<DatePicker
placeholder="Filter by due date"
// value={this.state.
onSelectDate={this.
value={this.state.
/>
<IconButton
iconProps={{ iconName: 'Clear' }}
title="Reset"
onClick={() => this.onDateChange(undefined)}
/>
</Stack>
<Stack horizontal tokens={{ childrenGap: 16 }} className={styles.
<StackItem grow>
<DocumentCard className={`${styles.card} ${styles.NotStarted}`}>
<div className={styles.cardContent}
<AlertCircle size={24} />
<Text variant="large">Not Started</Text>
<Text variant="xLarge">{taskStats.
</div>
</DocumentCard>
</StackItem>
<StackItem grow>
<DocumentCard className={`${styles.card} ${styles.inProgress}`}>
<div className={styles.cardContent}
<Clock className="cardIcon" size={24} />
<Text variant="large">In Progress</Text>
<Text variant="xLarge">{taskStats.
</div>
</DocumentCard>
</StackItem>
<StackItem grow>
<DocumentCard className={`${styles.card} ${styles.completed}`}>
<div className={styles.cardContent}
<CheckCircle size={24} />
<Text variant="large">Completed</
<Text variant="xLarge">{taskStats.
</div>
</DocumentCard>
</StackItem>
</Stack>
<Text variant="large" className={styles.error}>{
<Dialog
hidden={!showTaskModal}
onDismiss={this.closeDialog}
dialogContentProps={{
title: selectedTask?.title || ''
}}
>
{selectedTask && (
<Stack tokens={{ childrenGap: 10 }}>
<Text>Bucket: {this.state.buckets[
<hr style={{ border: '1px solid #ccc', margin: '10px 0' }} />
<Text>Due Date: {selectedTask.dueDateTime ?
new Date(selectedTask.dueDateTime)
'No due date'}
</Text>
<hr style={{ border: '1px solid #ccc', margin: '10px 0' }} />
<Text>Status: {this.getStatusDisplay(
<hr style={{ border: '1px solid #ccc', margin: '10px 0' }} />
<Text>Priority: {this.getPriorityDisplay(
<hr style={{ border: '1px solid #ccc', margin: '10px 0' }} />
<Text>Progress: {selectedTask.percentComplete}
<hr style={{ border: '1px solid #ccc', margin: '10px 0' }} />
{selectedTask.description && (
<Text>Description: {selectedTask.description}</
)}
<hr style={{ border: '1px solid #ccc', margin: '10px 0' }} />
{selectedTask.checklist && (
<div>
<Text>Checklist:</Text>
{Object.values(selectedTask.
<div key={index} className={styles.
<input
type="checkbox"
checked={checklistItem.
readOnly
/>
<Text variant="medium">{
</div>
))}
<hr style={{ border: '1px solid #ccc', margin: '10px 0' }} />
</div>
)}
</Stack>
)}
<DialogFooter>
<PrimaryButton onClick={this.closeDialog} text="Close" />
</DialogFooter>
</Dialog>
</div>
) : <div className={styles.
<Text variant="xLarge" className={styles.taskHeading}
My Tasks</Text>
<Stack horizontal tokens={{ childrenGap: 8 }} className={styles.dateFilter}>
<DatePicker
placeholder="Filter by due date"
// value={this.state.
onSelectDate={this.
value={this.state.
/>
<IconButton
iconProps={{ iconName: 'Clear' }}
title="Reset"
onClick={() => this.onDateChange(undefined)}
/>
</Stack>
<Stack horizontal tokens={{ childrenGap: 16 }} className={styles.
<StackItem grow>
<DocumentCard className={`${styles.card} ${styles.NotStarted}`}>
<div className={styles.cardContent}
<AlertCircle size={24} />
<Text variant="large">Not Started</Text>
<Text variant="xLarge">{taskStats.
</div>
</DocumentCard>
</StackItem>
<StackItem grow>
<DocumentCard className={`${styles.card} ${styles.inProgress}`}>
<div className={styles.cardContent}
<Clock className="cardIcon" size={24} />
<Text variant="large">In Progress</Text>
<Text variant="xLarge">{taskStats.
</div>
</DocumentCard>
</StackItem>
<StackItem grow>
<DocumentCard className={`${styles.card} ${styles.completed}`}>
<div className={styles.cardContent}
<CheckCircle size={24} />
<Text variant="large">Completed</
<Text variant="xLarge">{taskStats.
</div>
</DocumentCard>
</StackItem>
</Stack>
<DetailsList
items={filteredTasks}
columns={this.columns}
selectionMode={SelectionMode.
onColumnHeaderClick={this.
/>
<Dialog
hidden={!showTaskModal}
onDismiss={this.closeDialog}
dialogContentProps={{
title: selectedTask?.title || ''
}}
>
{selectedTask && (
<Stack tokens={{ childrenGap: 10 }}>
<Text>Bucket: {this.state.buckets[
<hr style={{ border: '1px solid #ccc', margin: '10px 0' }} />
<Text>Due Date: {selectedTask.dueDateTime ?
new Date(selectedTask.dueDateTime)
'No due date'}
</Text>
<hr style={{ border: '1px solid #ccc', margin: '10px 0' }} />
<Text>Status: {this.getStatusDisplay(
<hr style={{ border: '1px solid #ccc', margin: '10px 0' }} />
<Text>Priority: {this.getPriorityDisplay(
<hr style={{ border: '1px solid #ccc', margin: '10px 0' }} />
<Text>Progress: {selectedTask.percentComplete}
<hr style={{ border: '1px solid #ccc', margin: '10px 0' }} />
{selectedTask.description && (
<Text>Description: {selectedTask.description}</
)}
<hr style={{ border: '1px solid #ccc', margin: '10px 0' }} />
{selectedTask.checklist && (
<div>
<Text>Checklist:</Text>
{Object.values(selectedTask.
<div key={index} className={styles.
<input
type="checkbox"
checked={checklistItem.
readOnly
/>
<Text variant="medium">{
</div>
))}
<hr style={{ border: '1px solid #ccc', margin: '10px 0' }} />
</div>
)}
</Stack>
)}
<DialogFooter>
<PrimaryButton onClick={this.closeDialog} text="Close" />
</DialogFooter>
</Dialog>
</div>
return (
content
)
}
}
//serve.json
{
"$schema": "https://developer.microsoft.
"port": 4321,
"https": true,
"initialPage": "https://sitecollectionurl/_
}
Below are the commands to deploy the webpart
gulp clean
gulp build
gulp bundle --ship
gulp package-solution –ship
Outcome
Comments
Post a Comment