Web App Documentation
| Verification | |
|---|---|
| Tags | |
| Last edited | |
| Last edited time | |
| Owner | |
| Person |
1. Business Requirement
- Web application will be used by admin.
- Features of web appllication are
- Admin can perform CRUD on officers, clients and their respective details.
- Can create shifts for officers and check their status.
- Review officer reports(Daily report, Incident report and Courtesy report).
- Review officer leaves.
- Admin can create list of hollidays.
- Officer have access to their shifts and other details from mobile app.
- Officers create reports from client locations which will contain media files. Admin can review them and also manually email them to clients.
- Using Scheduler Admin can create bulk shifts for a particular officer or for many officers. And can also copy shifts from a day, week, 14 days or a month to next day, week, 14 days or a month.
2. Environment Setup and Running the App
- After pulling the code from remote repo. Run command This will install all the npm dependencies.
npm install.
- Run to start the project.
npm start
3. Code Structure
1. Resources
Each resource container will have a create, edit, list and show files.
For example lets take /src/clients resource as example
| File name | purpose |
| ClientCreate.tsx | creation of client |
| ClientEdit.tsx | updation of client |
| ClientLIst.tsx | listing of clients |
| ClientShow.tsx | Viewing client details |
| index.tsx | Grouping the exports |
2. Utilities
1. Constants
/src/constants/index.js - Holds all the config info related to remote api, jwt, paginations etc
2. Utilities
This directory contains all the uritility functions
| File | Description |
| dateFormatterParser.ts | date parsing functions |
| FileController.ts | Used in file uploads |
| renewAccessToken.ts | Access token renewal utility |
| exporter | Utility functions for exporting certain resources as excel sheets |
| validate | provides validations functions |
3. General
| File | Description |
| authProvider.ts | Handles authentication of each api call. Handles validity of jwt token |
| aws.config.js | Holds aws s3 bucket information |
| firebas.config.js | Holds firebase credentials for push notifications |
| firebase.js | Firebase setup file |
| routes.js | Where all the react routes are defined |
| sentry.config.js | sentry logging config file |
| themeReducer.js | web app theme configuration |
| types.ts | typescript type definitions |
| /src/interfaces | this directory contains all the typescript types and interfaces |
| i18n | internationalisation configs |
| /src/dataprovider | this directory contains, react admin |
4. Implementations
1. Schedule
Package used: devextreme , devextreme-react
Package configuration: Setup of devextreme , Setup of devextreme-react
Implementation :
src/shifts/devexScheduler/Scheduler/index.tsx
...
<Scheduler
dataSource={this.state.formattedShifts}
defaultCurrentView={this.state.schedulerView}
defaultCurrentDate={this.state.currentDate}
useDropDownViewSwitcher={true}
// height={
// heightOfScheduler > 105 * this.state.officers.items.length
// ? heightOfScheduler
// : 105 * this.state.officers.items.length
// }
// height={!!this.state.officers.items.length ? 100 * (this.state.officers.items.length) + 120 : 400}
// height={500}
groups={groups}
firstDayOfWeek={firstDayOfWeek}
startDayHour={startDayHour}
endDayHour={48}
showCurrentTimeIndicator={false}
dataCellRender={this.renderDataCell}
dateCellRender={this.renderDateCell}
appointmentComponent={Appointment}
ref={this.schedulerRef}
crossScrollingEnabled={true}
onAppointmentFormOpening={this.onAppointmentFormOpening}
onAppointmentAdding={this.onAppointmentAdding}
onAppointmentUpdating={this.onAppointmentUpdating}
appointmentTooltipRender={this.renderAppointmentTooltip}
onOptionChanged={this.handleDateChange}
onContentReady={this.onContentReady}
onCellContextMenu={this.handleCellContextMenu}
>
<Editing allowDeleting={true} allowDragging={true} />
<View
name="Day"
type="timelineDay"
maxAppointmentsPerCell={1}
resourceCellRender={this.renderResourceCell}
/>
<View
name="7 Days View"
type="timelineWeek"
timeCellTemplate={() => {}}
cellDuration={1440 * 60}
maxAppointmentsPerCell={1}
resourceCellRender={this.renderResourceCell}
/>
<View
name="14 Days View"
type="timelineWeek"
timeCellTemplate={() => {}}
intervalCount="2"
cellDuration={1440 * 60}
maxAppointmentsPerCell={1}
resourceCellRender={this.renderResourceCell}
/>
<View
name="Month"
type="timelineMonth"
maxAppointmentsPerCell={1}
resourceCellRender={this.renderResourceCell}
/>
<Resource
fieldExpr="officerId"
dataSource={this.state.officers.items}
label="Officer"
/>
<Resource
fieldExpr="clientId"
allowMultiple={false}
dataSource={this.state.clients.items}
label="Client"
/>
<Resource
fieldExpr="locationId"
allowMultiple={false}
dataSource={this.state.locations.items}
label="Location"
/>
</Scheduler>
2. Data Provider [API call setup]
As we are using react-admin through out the project, so we need to pass a dataProvider to <Admin /> component which is responsible for making all type of requests for any resource. Reference
Note : Data Provider’s job is to turn these method calls into HTTP requests, and transform the HTTP responses to the data format expected by react-admin. In technical terms, a Data Provider is an adapter for an API.
Sample Code :
const dataProvider = {
getList: (resource, params) => Promise,
getOne: (resource, params) => Promise,
getMany: (resource, params) => Promise,
getManyReference: (resource, params) => Promise,
create: (resource, params) => Promise,
update: (resource, params) => Promise,
updateMany: (resource, params) => Promise,
delete: (resource, params) => Promise,
deleteMany: (resource, params) => Promise,
}src/dataProvider/springbootRest.js
Please, visit above file for detailed implementation.
3. Auth Provider [Authentication]
An authProvider is an object that handles authentication and authorization logic. It exposes methods that react-admin calls when needed, and that you can call manually through specialized hooks. The authProvider methods must return a Promise. Reference
src/authProvider.ts
const authProvider = {
// authentication
login: params => Promise.resolve(),
checkError: error => Promise.resolve(),
checkAuth: params => Promise.resolve(),
logout: () => Promise.resolve(),
getIdentity: () => Promise.resolve(),
// authorization
getPermissions: params => Promise.resolve(),
};4. Push Notifications
For notifications we are using FCM : Firebase Cloud Messaging is a cross-platform messaging solution that lets you reliably send messages at no cost.
Package used: firebase
Package configuration: Setup of this package
Implementation :
public/firebase-messaging-sw.js
importScripts('https://www.gstatic.com/firebasejs/8.1.2/firebase-app.js');
importScripts('https://www.gstatic.com/firebasejs/8.1.2/firebase-messaging.js');
const config = {
apiKey: apiKey,
authDomain: authDomain,
databaseURL: databaseURL,
projectId: projectId,
storageBucket: storageBucket,
messagingSenderId: messagingSenderId,
appId: appId
}
// isSupported will check either the environment is supporting FCM or not
const messaging = firebase.messaging.isSupported() ? firebase.messaging() : null;Note : Firebase is supported on localhost and https client not in http client. In Public folder, firebase-messaging-sw.js is mandatory. It registers a firebase service worker for web client which enables the client to get FCM token and to receive the notification.
src/firebase.js
import firebase from 'firebase';
import 'firebase/messaging';
const config = {
apiKey: apiKey,
authDomain: authDomain,
databaseURL: databaseURL,
projectId: projectId,
storageBucket: storageBucket,
messagingSenderId: messagingSenderId,
appId: appId
}
firebase.initilizeApp(config)
export const FIREBASE_MESSAGING = firebase.messaging.isSupported() ? firebase.messaging() : null;
// Rest of the functions ...Note : You can get whole config object after creating a project on Firebase in project setting .
src/lauout/AppBAr.tsx
import { FIREBASE_MESSAGING } from '../firebase'
//Getting FCM Token and storing to database
subscribeToNotification();
//handling forground notification
FIREBASE_MESSAGING.onMessage((payload: any) => {
handleForegroundNotification(payload);
});
//handling token refresh
FIREBASE_MESSAGING.onTokenRefresh(handleTokenRefresh);5. Geo fencing
A Geo fence is created around the perimeters of the client location while creating client location. This is used on the mobile end to verify whether the officer is present withing the client geo fence or not. Along with Fence co-ordinates a central single co-ordinate is also picked to measure the ditance of the officer from center point of cllient location.
Package used: react-google-maps
Requirement: We need a google maps api key. Refer here for generating one.
- Drawing polygonWe draw polygon around the perimeters of the client location. Polygon is an array of (latitude,longitude). Using these co-ordinates geo fence is verified at the mobile end.
const { Polygon, } = require("react-google-maps"); <Polygon ref={props.onPolygonMounted} options={{ fillColor: `#2196F3`, strokeColor: `#2196F3`, fillOpacity: 0.5, strokeWeight: 2, }} // Make the Polygon editable / draggable editable draggable path={props.fenceBounds} // Event used when manipulating and adding points onMouseUp={(e) => props.getPolygonBoundaries(e)} // // Event used when dragging the whole Polygon // onDragEnd={(e) => props.getBoundaries(e)} // onLoad={(e) => props.onPolygonLoad} // onUnmount={onUnmount} />
- Displaying created polygonOnce a fence is created, it can be edited. To display already created polygon we use DrawingManager component from react-google-maps. Fence co-ordinates are passed as props to MapWithASearchBox.jsx component.
{props.fenceBounds.length === 0 ? ( <DrawingManager defaultDrawingMode={null} defaultOptions={{ drawingControl: true, drawingControlOptions: { // position: window.google.maps.ControlPosition.TOP_CENTER, position: window.google.maps.ControlPosition.TOP_LEFT, drawingModes: [ null, window.google.maps.drawing.OverlayType.POLYGON, ], }, }} polygonOptions={{ editable: true, draggable: true }} onPolygonComplete={(e) => props.getBoundaries(e)} /> ) :()
- Picking single co-ordina
const { Marker } = require("react-google-maps"); {props.propertyPosition && props.propertyPosition.lat ? ( <Marker position={props.propertyPosition} draggable={true} onDragEnd={props.onMarkerDragEnd} // onClick={(e) => console.log(e)} /> ) : ( props.markers.map((marker, index) => ( <Marker key={index} position={marker.position} draggable={true} // onDragend={props.onMarkerDragEnd} onDragEnd={props.onMarkerDragEnd} // onClick={(e) => console.log(e)} /> )) )}
6. Media handling
src/utils/FileController.ts
import { fetchUtils } from 'react-admin';
import { FILE_UPLOAD_URI, FILE_DETAIL_UPLOAD_URI, ACCESS_TOKEN, LOGGER } from '../constants';
import { FileDetail } from '../interfaces';
const httpClient = fetchUtils.fetchJson;
export class FileController {
public upload(fileDetail: FileDetail[]): FileDetail[] {
// const formdata = new FormData();
// formdata.append("file", file);
return httpClient(FILE_DETAIL_UPLOAD_URI, {
method: 'POST',
credentials: 'include',
headers: new Headers({
"Authorization": `Bearer ${localStorage.getItem(ACCESS_TOKEN)}`
}),
body: JSON.stringify(fileDetail),
redirect: 'follow'
})
.then((response: any) => response.json)
.then((fileResponse: FileDetail) => {
return fileResponse;
}).catch((error: Error) => {
LOGGER && console.log(`%c FileDetailUpload FileController : %c ${error}`, 'color:blue', 'color:red');
throw new Error("Error in uploading fileDetail")
});
}
}ssrc/documents/DocumentDialog.tsx
const fc = new FileController();
//TODO: Uploading file to AWS s3 bucket
// const FileWithNewName = new FormData();
// FileWithNewName.append('file', values.fileDetail?.rawFile, "test.test")
// LOGGER && console.log(`%c FileWithNewName`, 'color:blue', FileWithNewName)
const newFile = renameFile(values.fileDetail?.rawFile, `${uuidv4()}.${values.fileDetail?.rawFile.name.split('.').pop()}`)
LOGGER && console.log(`%c DocumentDialog : newFile`, 'color:blue', newFile)
const fileDetail = values.fileDetail?.rawFile ? await ReactS3.uploadFile(newFile, AWSConfig) : null
LOGGER && console.log(`%c DocumentDialog : fileDetail`, 'color:blue', fileDetail)
const fileDetailToUpload = [{
name: fileDetail.key,
url: fileDetail.location,
format: values.fileDetail?.rawFile.type,
size: values.fileDetail?.rawFile.size
}]
LOGGER && console.log(`%c fileDetailToUpload`, 'color:blue', fileDetailToUpload)
//TODO: Uploading fileDetail to database
const fileResArray: any = await fc.upload(fileDetailToUpload);
LOGGER && console.log(`%c AddAvatar : fileRes`, 'color:blue', fileResArray)
const fileRes = fileResArray[0];
LOGGER && console.log("values", values, "fileRes", fileRes, "resourceData", resourceData)7. AWS media upload
Package used: @sentry/browser
Package configuration: Setup of this package
Implementation :
//TODO: Uploading file to AWS s3 bucket
const newFile = renameFile(values.fileDetail?.rawFile, `${uuidv4()}.${values.fileDetail?.rawFile.name.split('.').pop()}`)
LOGGER && console.log(`%c DocumentDialog : newFile`, 'color:blue', newFile)
const fileDetail = values.fileDetail?.rawFile ? await ReactS3.uploadFile(newFile, AWSConfig) : null
LOGGER && console.log(`%c DocumentDialog : fileDetail`, 'color:blue', fileDetail)8. Sentry logging
We are using sentry for error tracking.
Package used: @sentry/browser
Package configuration: Setup of this package
Implementation :
sentry.config.js
export default 'https://a1d908334bc44c82886bd0e611c6d670@o573364.ingest.sentry.io/5723839';src/index.tsx
...
import * as Sentry from "@sentry/browser";
import SentryURL from './sentry.config'
//TODO: Sentry Integration for Logging
Sentry.init({
dsn: process.env.NODE_ENV === 'production' ? SentryURL : '',
});
Sentry.configureScope(scope => {
scope.setExtra('battery', 0.7);
scope.setTag('user_mode', 'admin');
scope.setUser({ id: '1377', timezone: new Date().toString() })
});
...src/dataProvider/springbootRest.js
...
return httpClient(url, options).then((response) => convertHTTPResponse(response, type, resource, params))
.catch((err) => {
Sentry.captureException(new Error(err?.body?.detail ? `${err?.body?.detail}, API Path : ${err?.body?.path}` : err?.message || NOTIFICATIONS.serverCommunicationError));
LOGGER && console.log(`%c SpringBootRest Error Response :`, 'color:blue', err?.body, err?.message)
return Promise.reject(new Error(err?.body?.detail || NOTIFICATIONS.serverCommunicationError));
});
...9. Filtering, Sorting and pagination
- Available sorting and filtering fields for all resourcessorting and filtering fieldsResourceFiltersSortings
officersemailId, firstname, lastname, statusemailid, firstname, lastnameclientsemailId, propertyname, clientname, statusemailId, clientName, propertyNamecontactsN/AN/Alocationslocation, propertynameclientPropertyName, name, streetdocumentsN/AN/Aincidentreportsofficer, propertyname, location, status, fromDate, toDateshiftOfficerFirstName, shiftClientPropertyName, shiftClientLocationName, startDateTime, statusdailyreportsofficer, propertyname, location, status, fromDate, toDateshiftOfficerFirstName, shiftClientPropertyName, shiftClientLocationName, startDateTime, statuscourtesyreportsofficer, propertyname, location, status, fromDate, toDateshiftOfficerFirstName, shiftClientPropertyName, shiftClientLocationName, startDateTime, statustimesheetsofficer, propertyname, location, status, fromDate, toDateshiftOfficerFirstName, shiftClientPropertyName, shiftClientLocationName, startDateTime, statusdailyreportinfosN/AN/Acourtesyfieldsstatus, namestatus, namecourtesytemplatesstatus, name, propertynameclientPropertyName, clientLocationName, statusleavesfromDate, toDate, officername , statusstartDateTime,endDateTime, status, officerFirstNameholidaysstartDateTime,endDateTime, status, namestartDateTime,endDateTime, status, nameshiftsofficerId, client, name, propertyname, location, status, fromDate, toDateN/A
We can filter, sort and paginate each resource’s data with in the <List /> component.
For example : src/clients/ClientList.tsx
<List
{...props}
bulkActionButtons={false}
filters={<ClientFilter />} // filtering
title="Clients"
sort={SORT_BY_ID} // sorting
perPage={PER_PAGE}
exporter={clientExporter}
pagination={<MyPagination />} // Pagination
>
<Datagrid
rowClick="expand" // row works as accordian
expand={<ClientShow />}
exporter={clientExporter} // exports the data in a excel sheet
>
...
<TextField
source="number"
label="resources.clients.fields.phone"
sortable={false} // can disable sorting on particular field
/>
...
</Datagrid>
</List>10. Routing
In react admin app, routing is taken care by <Resource /> component. We can also add custom routes to react-admin app by passing a prop called customRoutes to <Admin /> component. Reference
<Admin
title="Dashboard"
dataProvider={dataProvider}
customReducers={{ theme: themeReducer }}
customRoutes={customRoutes}
authProvider={authProvider}
dashboard={Dashboard}
loginPage={Login}
layout={Layout}
i18nProvider={i18nProvider}
>
<Resource name="officers" {...officers} />
<Resource name="clients" {...clients} />
<Resource name="locations" {...locations} />
<Resource name="documents" {...documents} />
<Resource name="contacts" {...contacts} />
<Resource name="leaves" {...leaves} />
<Resource name="holidays" {...holidays} />
<Resource name="incidentreports" {...incidentReports} />
<Resource name="incidentreportdocuments" {...documents} />
<Resource name="courtesyfields" {...courtesyFields} />
<Resource name="courtesytemplates" {...courtesyTemplates} />
<Resource name="courtesyreports" {...courtesyReports} />
<Resource name="dailyreports" {...dailyReports} />
<Resource name="dailyreportinfos" {...dailyReportInfos} />
<Resource name="dailyreportdocuments" {...documents} />
<Resource name="shifts" />
<Resource name="timesheets" {...timesheets} />
<Resource name="locationaccuracy" {...mlogs} />
</Admin>We can create a custom route by using <Route /> component.
src/routes.js
export default [
<Route
exact
path="/configuration"
render={() => (
<Authenticated> {/* Enforce authentication */}
<Configuration />
</Authenticated>
)}
/>,
<Route exact path="/register" component={Register} noLayout />,
<Route exact path="/forgotpassword" component={Forgot} noLayout />,
<Route exact path="/success" component={Success} noLayout />,
<Route exact path="/resetpassword/:accessToken" component={Reset} noLayout />,
<Route exact path="/privacy" component={PrivacyPolicy} noLayout />,
<Route exact path="/termsandservices" component={TermsAndServices} noLayout />,
<Route exact path="/dailyreport/:id" component={DailyReport} noLayout />,
<Route exact path="/incidentreport/:id" component={IncidentReport} noLayout />,
<Route exact path="/courtesyreport/:id" component={CourtesyReport} noLayout />,
<Route
exact
path="/scheduler"
render={() => (
<Authenticated>
<SchedulerView />
</Authenticated>
)}
/>
];11. i18nProvider
Just like for data fetching and authentication, react-admin relies on a simple object for translations. It’s called the i18nProvider, and it manages translation and language change using two methods:
And just like for the dataProvider and the authProvider, you can inject the i18nProvider to your react-admin app using the <Admin> component:
import i18nProvider from './i18n/i18nProvider';
const App = () => (
<Admin
dataProvider={dataProvider}
authProvider={authProvider}
i18nProvider={i18nProvider}
>
<Resource name="posts" list={/* ... */}>
//src/i18n/en.ts
eg : It is all the messages and fieldName for officer resource.
{
resources: {
officers: {
name: 'Officer |||| Officers',
menuName: 'Officer |||| Officers',
fields: {
search: 'Search First Name',
commands: 'Orders',
firstName: 'First Name',
groups: 'Segments',
lastName: 'Last Name',
last_seen_gte: 'Visited Since',
name: 'Name',
total_spent: 'Total spent',
password: 'Password',
confirm_password: 'Confirm Password',
dob: 'Date Of Birth',
phone: "Phone",
status: "Status",
email: 'Email',
payRateType: 'Pay Rate Type',
annualRate: "Annual Rate",
annualHourlyRate: 'Hourly Rate',
hourlyRate: 'Hourly Rate',
weekendRate: 'Weekend Rate',
holidayRate: 'Holiday Rate',
overtimeRate: 'Over Time Rate'
},
fieldGroups: {
identity: 'Identity',
document: 'Document',
address: 'Address',
stats: 'Stats',
history: 'History',
password: 'Password',
change_password: 'Change Password',
},
page: {
delete: 'Delete Officer',
},
errors: {
password_mismatch: 'Must be matched to password',
email_valid: 'Must be valid email',
age_valid: 'Age should be greater than 18 years.',
profileDeleteSuccess: 'Profile picture removed successfully',
profileDeleteError: 'Error occured in profile picture deleteing',
serverCommunicationError: 'Server Communication Error',
proceedForPayrate: 'Please proceed to update pay rate',
proceedForContacts: 'Please proceed to update contacts',
proceedForDocuments: 'Please proceed to update documents',
proceedForLocations: 'Please proceed to update locations',
profileImageUpdateSuccess: 'Profile Image Updated Successfully',
officerUpdateSuccess: 'Officer updated successfully',
profileImageAddSuccess: 'Profile Image Added Successfully',
},
},
}
}How it is being used inside components :
src/officers/OfficerList.tsx
//here label will be provided by i18nprovider
<List
{...props}
>
<Datagrid optimized rowClick="expand" expand={<OfficerShow />}>
<TextField
source="firstName"
label={translate("resources.officers.fields.firstName", {
smart_count: 1,
})}
/>
<TextField
source="lastName"
label="resources.officers.fields.lastName"
/>
<TextField source="emailId" label="resources.officers.fields.email" />
</Datagrid>
</List>src/officers/Officer.tsx
//Here message passed in notify() will be provided by i18nprovider
//.then(({ data }: any) => {
// if (data) {
notify(`resources.officers.errors.profileImageAddSuccess`, 'info');
//redirect(`/officers/${avatarData.userId}/2`);
//}
//})5. Build and Publish
Application is deployed on AWS. We build the app first and push the build to aws EC2 instance.
- yarn build or will build the app.
npm build
- Push the contents of build folder to AWS server.
- Changes will automatically take place.