Web App Documentation

Verification
Tags
Last edited
Last edited time
OwnerRidgeway
Person

1. Business Requirement

2. Environment Setup and Running the App

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 namepurpose
ClientCreate.tsxcreation of client
ClientEdit.tsxupdation of client
ClientLIst.tsxlisting of clients
ClientShow.tsxViewing client details
index.tsxGrouping 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

FileDescription
dateFormatterParser.tsdate parsing functions
FileController.tsUsed in file uploads
renewAccessToken.tsAccess token renewal utility
exporterUtility functions for exporting certain resources as excel sheets
validateprovides validations functions

3. General

FileDescription
authProvider.tsHandles authentication of each api call. Handles validity of jwt token
aws.config.jsHolds aws s3 bucket information
firebas.config.jsHolds firebase credentials for push notifications
firebase.jsFirebase setup file
routes.jsWhere all the react routes are defined
sentry.config.jssentry logging config file
themeReducer.jsweb app theme configuration
types.tstypescript type definitions
/src/interfacesthis directory contains all the typescript types and interfaces
i18ninternationalisation configs
/src/dataproviderthis 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.

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

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.

  1. yarn build or will build the app.

    npm build

  1. Push the contents of build folder to AWS server.
  1. Changes will automatically take place.