",
44 | "license": "ISC",
45 | "files": [
46 | "README.md",
47 | "dist",
48 | "src",
49 | "!*/__tests__"
50 | ],
51 | "dependencies": {
52 | "@loopback/boot": "^3.4.1",
53 | "@loopback/core": "^2.16.1",
54 | "@loopback/repository": "^3.7.0",
55 | "@loopback/rest": "^9.3.1",
56 | "@loopback/rest-explorer": "^3.3.1",
57 | "@loopback/service-proxy": "^3.2.1",
58 | "axios": "^0.21.1",
59 | "loopback-connector-mongodb": "^5.2.3",
60 | "tslib": "^2.0.0"
61 | },
62 | "devDependencies": {
63 | "@loopback/build": "^6.4.1",
64 | "@loopback/eslint-config": "^10.2.1",
65 | "@loopback/testlab": "^3.4.1",
66 | "@types/node": "^10.17.60",
67 | "eslint": "^7.28.0",
68 | "source-map-support": "^0.5.19",
69 | "typescript": "~4.3.2"
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/portal/components/react-admin/layout/customUserMenu.js:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { Typography, useMediaQuery, makeStyles } from "@material-ui/core";
3 | import Avatar from "@material-ui/core/Avatar";
4 | import Button from "@material-ui/core/Button";
5 | import KeyboardArrowDownIcon from "@material-ui/icons/KeyboardArrowDown";
6 | import Popover from "@material-ui/core/Popover";
7 | import { useSession } from "next-auth/client";
8 | import CustomLogoutButton from "./logoutButton";
9 |
10 | const UserMenu = ({ logout }) => {
11 | const [session, loading] = useSession();
12 | if (session) {
13 | return ;
14 | }
15 | return <>>;
16 | };
17 |
18 | const useStyles = makeStyles((theme) => ({
19 | userFullName: {
20 | fontSize: "0.8rem",
21 | color: theme.palette.heading.light,
22 | marginLeft: "1rem",
23 | },
24 | downArrow: {
25 | marginLeft: "0.5rem",
26 | fontSize: "1.2rem",
27 | transform: "translateY(-10%)",
28 | fontWeight: "400",
29 | },
30 | }));
31 |
32 | const UserMenuComponent = ({ user, logout }) => {
33 | const [userMenu, setUserMenu] = React.useState(null);
34 | const isSmall = useMediaQuery((theme) => theme.breakpoints.down("sm"));
35 | const classes = useStyles();
36 |
37 | const userMenuClick = (event) => {
38 | setUserMenu(event.currentTarget);
39 | };
40 |
41 | const userMenuClose = () => {
42 | setUserMenu(null);
43 | };
44 | return (
45 |
46 |
62 |
63 |
79 |
80 |
81 |
82 | );
83 | };
84 |
85 | export default UserMenu;
86 |
--------------------------------------------------------------------------------
/portal/components/react-admin/layout/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { connect, useDispatch } from "react-redux";
3 | import {
4 | getResources,
5 | Notification,
6 | setSidebarVisibility,
7 | Sidebar,
8 | } from "react-admin";
9 | import { makeStyles, useMediaQuery } from "@material-ui/core";
10 | import CustomSidebar from "./customSidebar";
11 | import AppBar from "./customAppBar";
12 |
13 | const useStyles = makeStyles((theme) => ({
14 | wrapper: {
15 | height: "100vh",
16 | display: "grid",
17 | gridTemplateRows: "8vh auto",
18 | [theme.breakpoints.down("sm")]: {
19 | gridTemplateColumns: "1fr",
20 | },
21 | [theme.breakpoints.up("sm")]: {
22 | gridTemplateColumns: "18vw auto",
23 | },
24 | },
25 | sidebar: {
26 | gridColumn: "1 / 2", //Start End
27 | gridRow: "1 / 3",
28 | inset: "unset!important",
29 | "& > div": {
30 | display: "grid",
31 | gridTemplateColumns: "1fr",
32 | gridTemplateRows: "8vh auto",
33 | [theme.breakpoints.down("sm")]: {
34 | width: "70vw",
35 | },
36 | [theme.breakpoints.up("sm")]: {
37 | width: "18vw",
38 | },
39 | backgroundColor: theme.palette.grey.main,
40 | },
41 | },
42 | container: {
43 | display: "grid",
44 | gridTemplateRows: "9vh auto",
45 | gridTemplateColumns: "auto",
46 | "& > header": {
47 | position: "inherit",
48 | },
49 | [theme.breakpoints.down("sm")]: {
50 | gridColumn: "1 / 3",
51 | },
52 | [theme.breakpoints.up("sm")]: {
53 | gridColumn: "2 / 3",
54 | },
55 | gridRow: "1 / 3",
56 | },
57 | }));
58 |
59 | const CustomLayout = (props) => {
60 | const dispatch = useDispatch();
61 | const classes = useStyles();
62 | const { children, logout, open, title } = props;
63 | return (
64 |
65 |
68 | {
70 | if (props.sidebarOpen) {
71 | dispatch(setSidebarVisibility(false));
72 | }
73 | }}
74 | className={classes.container}
75 | >
76 |
77 |
{children}
78 |
79 |
80 |
81 | );
82 | };
83 |
84 | const mapStateToProps = (state) => ({
85 | isLoading: state.admin.loading > 0,
86 | resources: getResources(state),
87 | sidebarOpen: state.admin.ui.sidebarOpen,
88 | });
89 | export default connect(mapStateToProps, { setSidebarVisibility })(CustomLayout);
90 |
--------------------------------------------------------------------------------
/portal/styles/globals.css:
--------------------------------------------------------------------------------
1 | html,
2 | body {
3 | padding: 0;
4 | margin: 0;
5 | font-family: "Martel Sans", -apple-system, BlinkMacSystemFont, Segoe UI,
6 | Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue,
7 | sans-serif;
8 | text-rendering: optimizeLegibility;
9 | }
10 |
11 | a {
12 | color: inherit;
13 | text-decoration: none;
14 | }
15 |
16 | * {
17 | box-sizing: border-box;
18 | }
19 |
20 | /*
21 | * FONT FACE
22 | */
23 | @font-face {
24 | font-family: "Bahnschrift";
25 | src: url("/Bahnschrift.otf");
26 | font-style: normal;
27 | font-weight: 400;
28 | font-display: swap;
29 | }
30 |
31 | /**
32 | * =============
33 | * CARD STYLES
34 | */
35 |
36 | .card {
37 | margin: 1rem;
38 | padding: 1.5rem;
39 | text-align: left;
40 | color: inherit;
41 | text-decoration: none;
42 | border: 1px solid #eaeaea;
43 | border-radius: 10px;
44 | transition: color 0.15s ease, border-color 0.15s ease;
45 | }
46 |
47 | .logo-card {
48 | display: grid;
49 | grid-template-columns: 10% 90%;
50 | }
51 |
52 | @media screen and (min-width: 768px) {
53 | .card-center:last-child {
54 | transform: translateX(50%);
55 | }
56 | }
57 |
58 | .card:hover,
59 | .card:focus,
60 | .card:active {
61 | color: #0070f3;
62 | border-color: #0070f3;
63 | }
64 |
65 | .card h2 {
66 | margin: 0 0 1rem 0;
67 | font-size: 1rem;
68 | }
69 |
70 | .card p {
71 | margin: 0;
72 | font-size: 1.05rem;
73 | line-height: 1.5;
74 | }
75 |
76 | /**
77 | * UTIL CLASSES
78 | */
79 |
80 | .center {
81 | transform: translate(50%);
82 | }
83 |
84 | .text-center {
85 | text-align: center;
86 | }
87 |
88 | .text-bold {
89 | font-weight: 700;
90 | }
91 |
92 | .capitalize {
93 | text-transform: capitalize;
94 | }
95 |
96 | .pointer:hover {
97 | cursor: pointer;
98 | }
99 |
100 | /**
101 | * =============
102 | * REACT-ADMIN OVERRIDES
103 | */
104 |
105 | .edit-page {
106 | margin: 1rem 2rem;
107 | }
108 |
109 | .edit-page > div {
110 | margin-top: 0;
111 | }
112 |
113 | .simple-form > div:last-child {
114 | height: 0rem;
115 | }
116 |
117 | /**
118 | * =============
119 | * MEDIA QUERIES
120 | */
121 |
122 | @media (max-width: 600px) {
123 | html {
124 | font-size: 75%;
125 | }
126 |
127 | .card > p {
128 | font-size: 1rem;
129 | }
130 |
131 | .card h2 {
132 | font-size: 1.4rem;
133 | }
134 |
135 | /* React Admin Override */
136 | .edit-page {
137 | height: 100vh;
138 | overflow: scroll;
139 | }
140 |
141 | /* .grid {
142 | width: 100%;
143 | flex-direction: column;
144 | } */
145 | }
146 |
--------------------------------------------------------------------------------
/portal/components/react-admin/theme.js:
--------------------------------------------------------------------------------
1 | import { defaultTheme } from "react-admin";
2 | import merge from "lodash/merge";
3 | import red from "@material-ui/core/colors/red";
4 |
5 | const primaryColour = {
6 | main: "#303765",
7 | };
8 |
9 | const lavender = {
10 | main: "#E6E6FA",
11 | };
12 |
13 | const darkGrey = {
14 | main: "#343A40",
15 | light: "#AEAEAE",
16 | hover: "#4C4C4C",
17 | darker: "#2D2D2D",
18 | };
19 |
20 | const heading = {
21 | main: "#FAFAFAEE",
22 | light: "#FAFAFAAA",
23 | };
24 |
25 | const customTheme = merge({}, defaultTheme, {
26 | palette: {
27 | primary: primaryColour,
28 | heading: heading,
29 | secondary: lavender,
30 | error: red,
31 | grey: darkGrey,
32 | contrastThreshold: 3,
33 | tonalOffset: 0.2,
34 | },
35 | typography: {
36 | // Use the system font instead of the default Roboto font.
37 | fontFamily: [
38 | "Bahnschrift",
39 | "-apple-system",
40 | "BlinkMacSystemFont",
41 | '"Segoe UI"',
42 | "Arial",
43 | "sans-serif",
44 | ].join(","),
45 | body1: {
46 | fontSize: "1.4rem",
47 | },
48 | h6: {
49 | fontSize: "1.45rem",
50 | },
51 | h5: {
52 | fontSize: "1.65rem",
53 | },
54 | },
55 | overrides: {
56 | MuiButton: {
57 | // override the styles of all instances of this component
58 | root: {
59 | // Name of the rule
60 | color: "white", // Some CSS
61 | },
62 | },
63 | MuiTableCell: {
64 | // Replace the font of the list items
65 | root: {
66 | fontFamily: "Bahnschrift",
67 | },
68 | head: {
69 | // Add styling for the heading row in lists
70 | color: "#00000058",
71 | fontSize: "0.8rem",
72 | fontWeight: "bold",
73 | borderBottom: "1px solid lavender",
74 | textTransform: "uppercase",
75 | backgroundColor: "#F8FAFC",
76 | },
77 | },
78 | MuiToolbar: {
79 | regular: {
80 | minHeight: "10vh",
81 | },
82 | },
83 | MuiFilledInput: {
84 | root: {
85 | backgroundColor: "#F8FAFC",
86 | },
87 | input: {
88 | fontSize: "0.9rem",
89 | },
90 | },
91 | MuiMenuItem: {
92 | root: {
93 | fontSize: "0.9rem",
94 | },
95 | },
96 | MuiTableBody: {
97 | root: {
98 | "& > tr:nth-child(odd)": {
99 | backgroundColor: lavender.main,
100 | },
101 | },
102 | },
103 | MuiList: {
104 | root: {
105 | "& > a:nth-child(odd) > div": {
106 | backgroundColor: lavender.main,
107 | },
108 | },
109 | },
110 | },
111 | });
112 |
113 | export default customTheme;
114 |
--------------------------------------------------------------------------------
/portal/components/layout.js:
--------------------------------------------------------------------------------
1 | import Head from "next/head";
2 | import Image from "next/image";
3 | import { useState, useEffect } from "react";
4 | import styles from "../styles/layout.module.css";
5 |
6 | const Layout = ({ children, home }) => {
7 | const transitionStages = {
8 | FADE_OUT: "fadeOut",
9 | FADE_IN: "fadeIn",
10 | };
11 |
12 | const [activeChildren, setActiveChildren] = useState(children);
13 | const [transitionStage, setTransitionStage] = useState(
14 | transitionStages.FADE_OUT
15 | );
16 |
17 | const compareElem = (a, b) => {
18 | return a.type.name === b.type.name;
19 | };
20 |
21 | const transitionEnd = () => {
22 | if (transitionStage === transitionStages.FADE_OUT) {
23 | setActiveChildren(children);
24 | setTransitionStage(transitionStages.FADE_IN);
25 | }
26 | };
27 |
28 | useEffect(() => {
29 | setTransitionStage(transitionStages.FADE_IN);
30 | }, [transitionStages.FADE_IN]);
31 |
32 | useEffect(() => {
33 | if (!compareElem(children, activeChildren))
34 | setTransitionStage(transitionStages.FADE_OUT);
35 | }, [transitionStages.FADE_OUT, children, activeChildren]);
36 |
37 | return (
38 | <>
39 |
40 | समर्थ हिमाचल
41 |
42 |
43 |
44 |
45 | Rozgar Saathi
46 | {/* Bacchon ka sahara, phone humara
47 |
48 | An initiative of the Government of Himachal Pradesh, India
49 |
*/}
50 |
51 |
{
53 | transitionEnd();
54 | }}
55 | className={`${styles.main} ${styles[transitionStage]}`}
56 | >
57 | {activeChildren}
58 |
59 |
60 | For more details, contact 1800-180-8190{" "}
61 |
62 |
83 |
84 | >
85 | );
86 | };
87 |
88 | export default Layout;
89 |
--------------------------------------------------------------------------------
/portal/styles/layout.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | min-height: 100vh;
3 | padding: 0 0.5rem;
4 | align-items: center;
5 | height: 100vh;
6 | display: grid;
7 | grid-template-rows: auto;
8 | }
9 |
10 | .header {
11 | display: flex;
12 | flex-direction: column;
13 | justify-content: center;
14 | align-items: center;
15 | margin-top: 1rem;
16 | }
17 |
18 | .headerLogos {
19 | display: flex;
20 | flex-direction: row;
21 | width: 70vw;
22 | justify-content: space-around;
23 | }
24 |
25 | .main {
26 | opacity: 0;
27 | transition-property: opacity;
28 | transition-timing-function: ease-in;
29 | transition-delay: 0ms;
30 | transition-duration: 500ms;
31 | }
32 |
33 | .footer {
34 | width: 100%;
35 | border-top: 1px solid #eaeaea;
36 | display: flex;
37 | flex-direction: column;
38 | justify-content: center;
39 | align-items: center;
40 | }
41 |
42 | .footer a {
43 | display: flex;
44 | justify-content: center;
45 | align-items: center;
46 | flex-grow: 1;
47 | }
48 |
49 | .title a,
50 | .title span {
51 | color: #0070f3;
52 | text-decoration: none;
53 | }
54 |
55 | .title a:hover,
56 | .title a:focus,
57 | .title a:active {
58 | text-decoration: underline;
59 | }
60 |
61 | .title {
62 | margin: 0;
63 | line-height: 1.15;
64 | font-size: 2.75rem;
65 | color: #303765;
66 | margin-bottom: 0.2rem;
67 | }
68 | .subtitle {
69 | margin: 0;
70 | line-height: 1.15;
71 | font-size: 1.6rem;
72 | color: #000;
73 | font-weight: 400;
74 | margin-bottom: 0.4rem;
75 | }
76 | .subsubtitle {
77 | margin: 0rem;
78 | line-height: 1.15;
79 | font-size: 1rem;
80 | color: #000;
81 | font-weight: 400;
82 | font-style: italic;
83 | }
84 |
85 | .title,
86 | .description {
87 | text-align: center;
88 | }
89 |
90 | .logo {
91 | display: flex;
92 | flex-direction: row;
93 | justify-content: space-between;
94 | }
95 |
96 | .credit {
97 | font-weight: 700;
98 | text-align: center;
99 | font-size: 1rem;
100 | }
101 |
102 | .address {
103 | text-align: center;
104 | font-size: 0.8rem;
105 | opacity: 0.65;
106 | font-weight: 500;
107 | }
108 |
109 | /**
110 | * =============
111 | * TRANSITION STYLES
112 | */
113 |
114 | .fadeIn {
115 | opacity: 1;
116 | }
117 |
118 | .fadeOut {
119 | }
120 | /**
121 | * =============
122 | * MEDIA QUERIES
123 | */
124 |
125 | @media (max-width: 1600px) {
126 | .logo {
127 | width: 60vw;
128 | margin-top: 1rem;
129 | }
130 | .address {
131 | width: 30vw;
132 | }
133 | }
134 |
135 | @media (max-width: 768px) {
136 | .headerLogos {
137 | width: 100%;
138 | justify-content: space-between;
139 | }
140 |
141 | .logo {
142 | width: 100vw;
143 | }
144 |
145 | .face {
146 | transform: scale(0.8);
147 | }
148 |
149 | .logo img {
150 | transform: scale(0.625);
151 | }
152 | .address {
153 | width: 80vw;
154 | font-size: 0.7rem;
155 | transform: translateX(10px);
156 | }
157 | }
158 |
--------------------------------------------------------------------------------
/portal/pages/api/track.js:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 |
3 | const handler = async (req, res) => {
4 | if (req.method === "POST") {
5 | try {
6 | const { captcha, captchaToken } = req.body;
7 | const responseObjectCaptcha = await captchaVerify(captcha, captchaToken);
8 | const { id } = req.body;
9 | const responseObject = await startFetchTrackDevice(id);
10 | if (responseObject?.errors) {
11 | res
12 | .status(500)
13 | .json({ error: responseObject?.errors?.[0]?.message, success: null });
14 | } else if (responseObject?.data) {
15 | if (responseObject?.data?.["device_donation_donor"]?.length) {
16 | res.status(200).json({
17 | error: null,
18 | success: {
19 | data: maskPhoneNumber(
20 | responseObject.data["device_donation_donor"]
21 | ),
22 | },
23 | });
24 | } else
25 | res.status(200).json({
26 | error:
27 | "No device found with this ID/ इस आईडी से कोई फ़ोन नहीं मिला!",
28 | success: null,
29 | });
30 | }
31 | } catch (e) {
32 | res
33 | .status(200)
34 | .json({ error: "Incorect Captcha/ Captcha कोड गलत है!", success: e });
35 | }
36 | }
37 | };
38 |
39 | function maskPhoneNumber(array) {
40 | const obj = array[0];
41 | let { phone_number } = obj;
42 | phone_number = `******${phone_number.slice(6)}`;
43 | obj.phone_number = phone_number;
44 | return obj;
45 | }
46 |
47 | async function captchaVerify(captcha, captchaToken) {
48 | const result = await axios({
49 | method: "POST",
50 | url: `${process.env.NEXT_PUBLIC_CAPTCHA_URL}`,
51 | data: {
52 | captcha: captcha,
53 | token: captchaToken,
54 | },
55 | });
56 |
57 | return result;
58 | }
59 |
60 | async function fetchGraphQL(operationsDoc, operationName, variables) {
61 | const result = await axios({
62 | method: "POST",
63 | headers: {
64 | "x-hasura-admin-secret": "2OWslm5aAjlTARU",
65 | },
66 | url: "http://143.110.186.108:5001/v1/graphql",
67 | data: {
68 | query: operationsDoc,
69 | variables: variables,
70 | operationName: operationName,
71 | },
72 | });
73 |
74 | return await result;
75 | }
76 |
77 | const operationsDoc = `
78 | query trackDevice($trackingKey: String) {
79 | device_donation_donor(where: {device_tracking_key: {_eq: $trackingKey}}) {
80 | delivery_status
81 | phone_number
82 | name
83 | recipient_school {
84 | udise
85 | }
86 | }
87 | }
88 | `;
89 |
90 | function fetchTrackDevice(trackingKey) {
91 | return fetchGraphQL(operationsDoc, "trackDevice", {
92 | trackingKey: trackingKey,
93 | });
94 | }
95 |
96 | async function startFetchTrackDevice(trackingKey) {
97 | const response = await fetchTrackDevice(trackingKey);
98 |
99 | return response.data;
100 | }
101 |
102 | export default handler;
103 |
--------------------------------------------------------------------------------
/portal/components/react-admin/app.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 | import { AdminContext, AdminUI, Resource, useDataProvider } from "react-admin";
3 | import buildHasuraProvider, { buildFields } from "ra-data-hasura";
4 | import { ApolloClient, InMemoryCache } from "@apollo/client";
5 | import { useSession } from "next-auth/client";
6 | import { MuiThemeProvider, createMuiTheme } from "@material-ui/core";
7 | import customTheme from "./theme";
8 | import customLayout from "./layout/";
9 | import customFields from "./customHasura/customFields";
10 | import customVariables from "./customHasura/customVariables";
11 | import { resourceConfig } from "./layout/config";
12 |
13 | const App = () => {
14 | const [dataProvider, setDataProvider] = useState(null);
15 | const [apolloClient, setApolloClient] = useState(null);
16 | const [session] = useSession();
17 |
18 | useEffect(() => {
19 | // console.log("ENtered app.js",process.env.NEXT_PUBLIC_HASURA_URL)
20 | const hasuraHeaders = {};
21 | hasuraHeaders.Authorization = `Bearer ${session.jwt}`;
22 | if (session.role) hasuraHeaders["x-hasura-role"] = session.role;
23 |
24 | let tempClient = new ApolloClient({
25 | uri: process.env.NEXT_PUBLIC_HASURA_URL,
26 | cache: new InMemoryCache(),
27 | headers: hasuraHeaders,
28 | });
29 | // console.log("temp Client:", tempClient)
30 | async function buildDataProvider() {
31 | const hasuraProvider = await buildHasuraProvider(
32 | { client: tempClient },
33 | {
34 | buildFields: customFields,
35 | },
36 | customVariables
37 | );
38 | setDataProvider(() => hasuraProvider);
39 | setApolloClient(tempClient);
40 | }
41 | buildDataProvider();
42 | }, [session]);
43 |
44 | if (!dataProvider || !apolloClient) return null;
45 | return (
46 |
47 |
48 |
49 | );
50 | };
51 | function AsyncResources({ client }) {
52 | let introspectionResultObjects =
53 | client.cache?.data?.data?.ROOT_QUERY?.__schema.types
54 | ?.filter((obj) => obj.kind === "OBJECT")
55 | ?.map((elem) => elem.name);
56 | const resources = resourceConfig;
57 | let filteredResources = resources;
58 | if (introspectionResultObjects) {
59 | filteredResources = resources.filter((elem) =>
60 | introspectionResultObjects.includes(elem.name)
61 | );
62 | console.log("introspectionResultObjects", filteredResources);
63 | }
64 | if (!resources) return null;
65 | return (
66 |
67 |
68 | {filteredResources.map((resource) => (
69 |
77 | ))}
78 |
79 |
80 | );
81 | }
82 |
83 | export default App;
84 |
--------------------------------------------------------------------------------
/portal/components/login/login.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 | import { useToasts } from "react-toast-notifications";
3 | import { signIn } from "next-auth/client";
4 | import { useRouter } from "next/router";
5 | import controls from "./form.config";
6 | import styles from "../../styles/Login.module.css";
7 |
8 | export default function Login(props) {
9 | const { persona } = props;
10 | const [input, setInput] = useState({});
11 | const router = useRouter();
12 | const [inputValidity, setInputValidity] = useState(
13 | controls.map((control) => {
14 | return {
15 | [control.name]: false,
16 | };
17 | })
18 | );
19 | const [formValidity, setFormValidity] = useState(false);
20 |
21 | const handleInput = (e) => {
22 | setInput({ ...input, [e.target.name]: e.target.value });
23 | setInputValidity({
24 | ...inputValidity,
25 | [e.target.name]: e.target.validity.valid,
26 | });
27 | };
28 |
29 | useEffect(() => {
30 | let validity = controls.reduce(
31 | (acc, control) => (acc = acc && inputValidity[control.name]),
32 | true
33 | );
34 | setFormValidity(validity);
35 | }, [inputValidity]);
36 |
37 | const { addToast } = useToasts();
38 |
39 | const signUserIn = async (e) => {
40 | e.preventDefault();
41 | console.log("person:", persona);
42 | console.log(
43 | "Env Variables",
44 | `${process.env.NEXT_PUBLIC_URL}/${persona.redirectUrl}`
45 | );
46 | const { error, url } = await signIn("fusionauth", {
47 | loginId: input.username,
48 | password: input.password,
49 | applicationId: persona.applicationId,
50 | redirect: false,
51 | callbackUrl: `${
52 | persona.redirectUrl.search("http") < 0
53 | ? `${process.env.NEXT_PUBLIC_URL}/${persona.redirectUrl}`
54 | : persona.redirectUrl
55 | }`,
56 | });
57 | if (url) {
58 | router.push(url);
59 | }
60 | if (error) {
61 | addToast(error, { appearance: "error" });
62 | }
63 | };
64 |
65 | return (
66 |
67 |
68 | Log in as {persona.en}/
69 | {persona.hi} लॉग इन
70 |
71 |
72 | {persona.credentials} के यूज़र
73 | नाम/पासवर्ड से लॉग इन कीजिए
74 |
75 |
96 |
97 | );
98 | }
99 |
--------------------------------------------------------------------------------
/portal/components/react-admin/layout/customSidebar.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import { withRouter } from "react-router-dom";
3 | import { makeStyles } from "@material-ui/core/styles";
4 | import { ListSubheader } from "@material-ui/core";
5 | import clsx from "clsx";
6 | import UserSidebarHeader from "./sidebarHeader";
7 | import VerticalCollapse from "./verticalCollapse";
8 | import VerticalItem from "./verticalItem";
9 | import { resourceConfig } from "./config";
10 |
11 | const useStyles = makeStyles((theme) => ({
12 | listTitle: {
13 | fontSize: "0.9rem",
14 | textTransform: "uppercase",
15 | textAlign: "center",
16 | fontWeight: "700",
17 | color: theme.palette.grey[500],
18 | },
19 | sidebarHeader: {
20 | backgroundColor: theme.palette.grey[700],
21 | "& > div": {
22 | marginTop: "1ch;",
23 | },
24 | },
25 | sidebarList: {
26 | display: "flex",
27 | flexDirection: "column",
28 | marginTop: "1rem",
29 | },
30 | }));
31 |
32 | const CustomSidebar = (props) => {
33 | const [activePath, setActivePath] = useState(null);
34 | const { location, resources } = props;
35 |
36 | let filteredResources = resourceConfig;
37 | if (props.resources) {
38 | filteredResources = resourceConfig?.filter(
39 | (configResource) =>
40 | (resources?.some(
41 | (resource) => resource?.name === configResource?.name
42 | ) &&
43 | configResource.label) ||
44 | configResource.title
45 | );
46 | }
47 | useEffect(() => {
48 | const pathname = location.pathname.replace(/\//, "");
49 | if (activePath !== pathname) {
50 | setActivePath(pathname);
51 | }
52 | }, [location, activePath]);
53 |
54 | return (
55 |
59 | );
60 | };
61 |
62 | const SidebarWrapper = React.memo(function SidebarWrapper({
63 | activePath,
64 | filteredResources,
65 | }) {
66 | const classes = useStyles();
67 | return (
68 | <>
69 |
70 |
71 | {filteredResources.map((item, index) => {
72 | if (item.title)
73 | return (
74 |
75 | {item.label}
76 |
77 | );
78 | if (item.children) {
79 | return (
80 |
86 | );
87 | }
88 | return (
89 |
95 | );
96 | })}
97 |
98 | >
99 | );
100 | });
101 |
102 | export default withRouter((props) => );
103 |
--------------------------------------------------------------------------------
/captcha-service/app.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Captcha PNG img generator
3 | * @Author: Chakshu Gautam
4 | * @Email: chaks.gautam@gmail.com
5 | * @Version: 2.0
6 | * @Date: 2020-08-18
7 | * @license http://www.opensource.org/licenses/bsd-license.php BSD License
8 | */
9 | const { v1: uuid } = require("uuid");
10 |
11 | const captchapng = require("./captchapng");
12 | const cors = require("cors");
13 |
14 | const express = require("express");
15 | const app = express();
16 | const port = process.env.PORT || 9000;
17 | const host = process.env.PORT || "0.0.0.0";
18 |
19 | const low = require("lowdb");
20 | const FileSync = require("lowdb/adapters/FileSync");
21 | const adapter = new FileSync("db.json");
22 | const db = low(adapter);
23 | app.use(
24 | cors({
25 | origin: "*",
26 | optionsSuccessStatus: 200,
27 | })
28 | );
29 |
30 | db.defaults({ captchas: [] }).write();
31 |
32 | app.listen(port, host, () => console.log(`listening on port: ${port}`));
33 |
34 | app.get("/", (request, response) => {
35 | const captchaParsed = parseInt(Math.random() * 900000 + 100000);
36 | var p = new captchapng(120, 30, captchaParsed); // width,height,numeric captcha
37 | p.color(0, 0, 0, 0); // First color: background (red, green, blue, alpha)
38 | p.color(80, 80, 80, 255); // Second color: paint (red, green, blue, alpha)
39 |
40 | var img = p.getBase64();
41 | // var imgbase64 = new Buffer(img,'base64');
42 |
43 | const token = uuid();
44 | db.get("captchas")
45 | .push({ token, captchaParsed, timestamp: Math.floor(Date.now() / 1000) })
46 | .write();
47 | console.log({ token });
48 |
49 | response.header({
50 | "Content-Type": "application/json",
51 | token,
52 | "access-control-expose-headers": "token",
53 | });
54 |
55 | // Request token
56 | // response.send(imgbase64);
57 | response.json({ blob: img });
58 | });
59 |
60 | const removeOldCaptchas = () => {
61 | // Delete all captchas that are more than 600 seconds old.
62 | const now = Math.floor(Date.now() / 1000);
63 | const allData = db.get("captchas").value();
64 |
65 | allData
66 | .filter((captcha) => now - captcha.timestamp > 600)
67 | .forEach((filteredCaptcha) => {
68 | db.get("captchas").remove({ token: filteredCaptcha.token }).write();
69 | });
70 | };
71 |
72 | app.get("/verify", (request, response) => {
73 | removeOldCaptchas();
74 |
75 | try {
76 | const userResponse = request.query.captcha;
77 | const token = request.query.token;
78 | const captcha = db.get("captchas").find({ token: token }).value();
79 |
80 | if (!captcha) {
81 | response.status(400).send({ status: "Token Not Found" });
82 | return;
83 | }
84 |
85 | deleteUsedCaptcha(token);
86 | if (parseInt(userResponse) === (captcha && captcha.captchaParsed)) {
87 | response.status(200).send({ status: "Success" });
88 | } else {
89 | response.status(400).send({ status: "Code Incorrect" });
90 | }
91 | } catch (err) {
92 | console.log({ err });
93 | response.status(500).send({ status: "Internal Server Error" });
94 | }
95 | });
96 | function deleteUsedCaptcha(token) {
97 | db.get("captchas").remove({ token: token }).write();
98 | }
99 |
--------------------------------------------------------------------------------
/certificate/certificate-template-final.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
115 |
116 |
117 |
![]()
118 |
119 | Certificate of appreciation
120 |
121 |
![]()
122 |
123 |
124 | is presented to
125 |
126 |
127 | for their contribution to a child’s online learning through the donation
128 | of a smartphone under the ‘Digital Saathi’ program
129 |
130 |
133 |
134 |
135 |
--------------------------------------------------------------------------------
/api/src/controllers/donate-device-graphQL-model.ts:
--------------------------------------------------------------------------------
1 | import pincodeToBlock from '../datasources/pincodeToBlock';
2 | export class DonateDevice {
3 | name?: string | null;
4 | phone_number?: string | null;
5 | created_at?: string | null;
6 | state_ut?: string | null;
7 | district?: string | null;
8 | block?: string | null;
9 | other_district?: null;
10 | address?: string | null;
11 | landmark?: string | null;
12 | pincode?: string | null;
13 | delivery_mode?: string | null;
14 | delivery_mode_outside_HP?: string | null;
15 | declaration_acknowledgement?: string | null;
16 | device_company?: string | null;
17 | device_other_model?: string | null;
18 | device_model?: string | null;
19 | device_size?: string | null;
20 | device_age?: number | null;
21 | device_condition?: string | null;
22 | call_function?: boolean | null;
23 | wa_function?: boolean | null;
24 | yt_function?: boolean | null;
25 | charger_available?: boolean | null;
26 | final_declaration?: string | null;
27 | device_tracking_key?: string | null;
28 | delivery_status?: string | null;
29 |
30 | operationsDoc = `
31 | mutation insertDonor($donor: device_donation_donor_insert_input!) {
32 | insert_device_donation_donor_one(object: $donor) {
33 | id
34 | phone_number
35 | device_tracking_key
36 | }
37 | }
38 | `;
39 | variableName = `donor`;
40 | operationName = `insertDonor`;
41 | databaseOperationName = `insert_device_donation_donor_one`;
42 |
43 | constructor(data: any) {
44 | this.name = data?.name ?? null;
45 | this.phone_number = data.contact ?? null;
46 | this.created_at = data['*meta-submission-date*'] ?? null;
47 | this.state_ut = data.state ?? null;
48 | this.district = data.district ?? null;
49 | this.block = this.fetchBlock(this.convertToString(data.pincode)) ?? null;
50 | this.other_district = data.otherdistrict ?? null;
51 | this.address = data.address ?? null;
52 | this.landmark = data.landmark ?? null;
53 | this.pincode = this.convertToString(data.pincode) ?? null;
54 | this.delivery_mode = data.delivery ?? null;
55 | this.declaration_acknowledgement = data.declaration ?? null;
56 | this.delivery_mode_outside_HP = data.deliverynonhp ?? null;
57 | this.device_company = data.company ?? null;
58 | this.device_other_model = data.companyother ?? null;
59 | this.device_model = data.modelname ?? null;
60 | this.device_size = data.screen ?? null;
61 | this.device_age = data.years ?? null;
62 | this.device_condition = data.condition ?? null;
63 | this.call_function = this.convertToBoolean(data.calls) ?? null;
64 | this.wa_function = this.convertToBoolean(data.wa) ?? null;
65 | this.yt_function = this.convertToBoolean(data.youtube) ?? null;
66 | this.charger_available = this.convertToBoolean(data.charger) ?? null;
67 | this.final_declaration = data.finalDecalaration ?? null;
68 | this.device_tracking_key = data.trackingKey ?? null;
69 | this.delivery_status = 'no-action-taken';
70 | }
71 |
72 | fetchBlock(pincode: string | null): string | null {
73 | if (!pincode) return null;
74 | const pincodeBlockMapping = new pincodeToBlock();
75 | if (!pincodeBlockMapping.mapping[pincode]) return 'OTHER';
76 | return pincodeBlockMapping.mapping[pincode];
77 | }
78 |
79 | convertToBoolean(response: string): boolean {
80 | if (response?.charAt(response.length - 1) === 'y') return true;
81 | else return false;
82 | }
83 |
84 | convertToString(response: number): string | null {
85 | if (response) {
86 | return String(response);
87 | }
88 | return null;
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/certificate/server.js:
--------------------------------------------------------------------------------
1 | const jwt = require("jsonwebtoken");
2 | const fs = require("fs").promises;
3 | const puppeteer = require("puppeteer");
4 | const QRCode = require("qrcode");
5 | const crypto = require("crypto");
6 |
7 | const fastify = require("fastify")({
8 | logger: true,
9 | });
10 |
11 | fastify.listen(process.env.PORT, process.env.HOST, (err, address) => {
12 | if (err) throw err;
13 | // Server is now listening on ${address}
14 | });
15 |
16 | fastify.post("/", async (request, reply) => {
17 | const { body = {} } = request;
18 | const { name = "", trackingKey = "", udise = "" } = body;
19 | const pdf = await printPdf(name, trackingKey, udise);
20 | reply.code(200).send({ base64String: pdf });
21 | });
22 |
23 | async function printPdf(name, trackingKey, udise) {
24 | const template = await fs.readFile(
25 | "./certificate-template-final.html",
26 | "utf-8"
27 | );
28 | const base64logo = await fs.readFile("./assets/cert-logo.png", "base64");
29 | const base64footer = await fs.readFile("./assets/cert-footer.png", "base64");
30 | const base64bahnschrift = await fs.readFile(
31 | "./assets/bahnschrift.ttf",
32 | "base64"
33 | );
34 | const privateKey = await fs.readFile("./jwtRS256_digitalsaathi.key", "utf-8");
35 | const payload = {
36 | sub: `did:tracking-key:${trackingKey}`,
37 | jti: crypto.randomBytes(16).toString("hex"),
38 | iss: "https://hpdigitalsaathi.in",
39 | vc: {
40 | "@context": [
41 | "https://www.w3.org/2018/credentials/v1",
42 | "https://www.w3.org/2018/credentials/examples/v1",
43 | ],
44 | type: ["VerifiableCredential", "DeviceDonorCredential"],
45 | credentialSubject: {
46 | donor: {
47 | trackingKey: trackingKey,
48 | name: name,
49 | recipient: {
50 | udise: udise,
51 | },
52 | },
53 | },
54 | },
55 | };
56 | const token = jwt.sign(payload, privateKey, {
57 | algorithm: "RS256",
58 | expiresIn: "10y",
59 | notBefore: "60",
60 | });
61 | const generateQR = async (text) => {
62 | try {
63 | return await QRCode.toDataURL(text, { scale: 2 });
64 | } catch (err) {
65 | console.error(err);
66 | }
67 | };
68 | const qrcode = await generateQR(token);
69 | const browser = await puppeteer.launch({
70 | headless: "true",
71 | executablePath: "/usr/bin/chromium-browser",
72 | args: [
73 | "--no-sandbox",
74 | "--disable-setuid-sandbox",
75 | "--disable-gpu",
76 | // This will write shared memory files into /tmp instead of /dev/shm,
77 | // because Docker’s default for /dev/shm is 64MB
78 | "--disable-dev-shm-usage",
79 | ],
80 | });
81 | const page = await browser.newPage();
82 | await page.setContent(template);
83 | await page.evaluate(
84 | (name, qrcode, base64logo, base64footer, base64bahnschrift) => {
85 | donor = document.querySelector("#donor");
86 | qr = document.querySelector("#qrcode");
87 | logo = document.querySelector("#logo");
88 | footer = document.querySelector("#footer");
89 |
90 | const style = document.createElement("style");
91 | style.appendChild(
92 | document.createTextNode(`
93 | @font-face {
94 | font-family: Bahnschrift;
95 | src: url('data:font/ttf;base64,${base64bahnschrift}');
96 | }`)
97 | );
98 | document.head.appendChild(style);
99 |
100 | donor.innerHTML = `${name}`;
101 | qr.src = qrcode;
102 | logo.src = `data:image/png;base64,${base64logo}`;
103 | footer.src = `data:image/png;base64,${base64footer}`;
104 | },
105 | name,
106 | qrcode,
107 | base64logo,
108 | base64footer,
109 | base64bahnschrift
110 | );
111 | await page.evaluateHandle("document.fonts.ready");
112 | const buffer = await page.pdf({ printBackground: true });
113 | const base64 = buffer.toString("base64");
114 | await browser.close();
115 | return base64;
116 | }
117 |
--------------------------------------------------------------------------------
/portal/components/react-admin/layout/verticalCollapse.js:
--------------------------------------------------------------------------------
1 | import React, { createElement, useEffect, useState } from "react";
2 | import {
3 | Collapse,
4 | IconButton,
5 | ListItem,
6 | ListItemText,
7 | } from "@material-ui/core";
8 | import { makeStyles } from "@material-ui/core/styles";
9 | import clsx from "clsx";
10 | import VerticalItem from "./verticalItem";
11 | import { KeyboardArrowDownIcon, KeyboardArrowUpIcon } from "@material-ui/icons";
12 |
13 | const useStyles = makeStyles((theme) => ({
14 | root: {
15 | padding: 0,
16 | "&.open": {
17 | backgroundColor:
18 | theme.palette.type === "dark"
19 | ? "rgba(255,255,255,.015)"
20 | : "rgba(0,0,0,.05)",
21 | },
22 | item: (props) => ({
23 | height: 40,
24 | width: "calc(100% - 16px)",
25 | borderRadius: "0 20px 20px 0",
26 | paddingRight: 12,
27 | flex: 1,
28 | paddingLeft: props.itemPadding > 60 ? 60 : props.itemPadding,
29 | color: theme.palette.text.primary,
30 | "&.active > .list-item-text > span": {
31 | fontWeight: 600,
32 | },
33 | "& .list-item-icon": {
34 | marginRight: 16,
35 | },
36 | }),
37 | },
38 | }));
39 |
40 | const isUrlInChildren = (parent, url) => {
41 | if (!parent.children) {
42 | return false;
43 | }
44 |
45 | for (let i = 0; i < parent.children.length; i += 1) {
46 | if (parent.children[i].children) {
47 | if (isUrlInChildren(parent.children[i], url)) {
48 | return true;
49 | }
50 | }
51 |
52 | if (
53 | parent.children[i].url === url ||
54 | url.includes(parent.children[i].url)
55 | ) {
56 | return true;
57 | }
58 | }
59 |
60 | return false;
61 | };
62 |
63 | const needsToBeOpened = (location, item) => {
64 | return location && isUrlInChildren(item, location.pathname);
65 | };
66 |
67 | function VerticalCollapse({ activePath, ...props }) {
68 | const [open, setOpen] = useState(() =>
69 | needsToBeOpened(window.location, props.item)
70 | );
71 | const { item, nestedLevel, publicity } = props;
72 |
73 | const classes = useStyles({
74 | itemPadding: nestedLevel > 0 ? 40 + nestedLevel * 16 : 24,
75 | });
76 |
77 | useEffect(() => {
78 | if (needsToBeOpened(window.location, item)) {
79 | setOpen(true);
80 | }
81 | }, [item]);
82 |
83 | function handleClick() {
84 | setOpen(!open);
85 | }
86 |
87 | return (
88 |
89 |
99 | {/* {item.icon && createElement(CustomIcons[item.icon])} */}
100 |
105 |
106 | ev.preventDefault()}
111 | >
112 | {createElement(!open ? KeyboardArrowDownIcon : KeyboardArrowUpIcon)}
113 |
114 |
115 |
116 | {item.children && (
117 |
122 | {item.children.map((i, index) => {
123 | if (i.children) {
124 | return (
125 |
133 | );
134 | }
135 | return (
136 |
142 | );
143 | })}
144 |
145 | )}
146 |
147 | );
148 | }
149 |
150 | export default VerticalCollapse;
151 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3"
2 |
3 | services:
4 | # postgres:
5 | # image: postgres:11.9-alpine
6 | # environment:
7 | # PGDATA: /var/lib/postgresql/data/pgdata
8 | # POSTGRES_USER: ${POSTGRES_USER}
9 | # POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
10 | # # Un-comment to access the db service directly
11 | # # ports:
12 | # # - 5432:5432
13 | # restart: unless-stopped
14 | # volumes:
15 | # - db_data:/var/lib/postgresql/data
16 |
17 | # fusionauth:
18 | # image: fusionauth/fusionauth-app:latest
19 | # depends_on:
20 | # - postgres
21 | # environment:
22 | # DATABASE_URL: jdbc:postgresql://postgres:5432/fusionauth
23 | # # Prior to version 1.19.0, use this deprecated name
24 | # # DATABASE_ROOT_USER: ${POSTGRES_USER}
25 | # DATABASE_ROOT_USERNAME: ${POSTGRES_USER}
26 | # DATABASE_ROOT_PASSWORD: ${POSTGRES_PASSWORD}
27 | # # Prior to version 1.19.0, use this deprecated name
28 | # # DATABASE_USER: ${DATABASE_USER}
29 | # DATABASE_USERNAME: ${DATABASE_USERNAME}
30 | # DATABASE_PASSWORD: ${DATABASE_PASSWORD}
31 | # # Prior to version 1.19.0, use this deprecated names
32 | # # FUSIONAUTH_MEMORY: ${FUSIONAUTH_MEMORY}
33 | # # FUSIONAUTH_SEARCH_ENGINE_TYPE: database
34 | # # FUSIONAUTH_URL: http://fusionauth:9011
35 | # # FUSIONAUTH_RUNTIME_MODE: development
36 | # FUSIONAUTH_APP_MEMORY: ${FUSIONAUTH_APP_MEMORY}
37 | # FUSIONAUTH_APP_RUNTIME_MODE: development
38 | # FUSIONAUTH_APP_URL: http://fusionauth:9011
39 | # SEARCH_TYPE: database
40 |
41 | # restart: unless-stopped
42 | # ports:
43 | # - 9011:9011
44 | # volumes:
45 | # - fa_config:/usr/local/fusionauth/config
46 |
47 | # graphql-engine:
48 | # image: hasura/graphql-engine:v2.0.0-beta.2
49 | # ports:
50 | # - "8080:8080"
51 | # depends_on:
52 | # - postgres
53 | # restart: always
54 | # environment:
55 | # HASURA_GRAPHQL_DATABASE_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/postgres
56 | # ## enable the console served by server
57 | # HASURA_GRAPHQL_ENABLE_CONSOLE: "true" # set to "false" to disable console
58 | # ## enable debugging mode. It is recommended to disable this in production
59 | # HASURA_GRAPHQL_DEV_MODE: "true"
60 | # HASURA_GRAPHQL_ENABLED_LOG_TYPES: startup, http-log, webhook-log, websocket-log, query-log
61 | # ## uncomment next line to set an admin secret
62 | # HASURA_GRAPHQL_ADMIN_SECRET: S#CREtp@55W0rd
63 | # HASURA_GRAPHQL_JWT_SECRET: '{"type": "RS512", "jwk_url": "http://fusionauth:9011/.well-known/jwks"}'
64 |
65 | mongo:
66 | image: mongo
67 | ports:
68 | - "27017:27017"
69 | environment:
70 | MONGO_INITDB_ROOT_USERNAME: ${MONGO_USER}
71 | MONGO_INITDB_ROOT_PASSWORD: ${MONGO_PASSWORD}
72 |
73 | api:
74 | build:
75 | context: ./api
76 | depends_on:
77 | - mongo
78 | environment:
79 | MONGO_DATASOURCE_USER: ${MONGO_USER}
80 | MONGO_DATASOURCE_PASSWORD: ${MONGO_PASSWORD}
81 | HASURA_URL: ${HASURA_URL}
82 | HASURA_ADMIN_SECRET: ${HASURA_ADMIN_SECRET}
83 | GUPSHUP_USERNAME: ${GUPSHUP_USERNAME}
84 | GUPSHUP_PASSWORD: ${GUPSHUP_PASSWORD}
85 | GUPSHUP_PRINCIPAL_ENTITY_ID: ${GUPSHUP_PRINCIPAL_ENTITY_ID}
86 | SLACK_ADMIN_LOGGER_AUTH_TOKEN: ${SLACK_ADMIN_LOGGER_AUTH_TOKEN}
87 | SLACK_ADMIN_LOGS_CHANNEL_ID: ${SLACK_ADMIN_LOGS_CHANNEL_ID}
88 | ports:
89 | - "3001:3000"
90 |
91 | portal:
92 | build:
93 | context: ./portal
94 | ports:
95 | - "3000:3000"
96 | environment:
97 | HASURA_URL: ${HASURA_URL}
98 | HASURA_ADMIN_SECRET: ${HASURA_ADMIN_SECRET}
99 | FUSIONAUTH_DOMAIN: ${FUSIONAUTH_DOMAIN}
100 | FUSIONAUTH_API_KEY: ${FUSIONAUTH_API_KEY}
101 | GUPSHUP_USERNAME: ${GUPSHUP_USERNAME}
102 | GUPSHUP_PASSWORD: ${GUPSHUP_PASSWORD}
103 | GUPSHUP_PRINCIPAL_ENTITY_ID: ${GUPSHUP_PRINCIPAL_ENTITY_ID}
104 | NEXTAUTH_URL: ${NEXTAUTH_URL}
105 | SLACK_ADMIN_LOGGER_AUTH_TOKEN: ${SLACK_ADMIN_LOGGER_AUTH_TOKEN}
106 | CERTIFICATE_URL: ${CERTIFICATE_URL}
107 | CAPTCHA_URL: ${CAPTCHA_URL}
108 |
109 | certificate:
110 | build:
111 | context: ./certificate
112 |
113 | captcha-service:
114 | build:
115 | context: ./captcha-service
116 | # volumes:
117 | # db_data:
118 | # fa_config:
119 |
--------------------------------------------------------------------------------
/api/src/controllers/donate-device.controller.ts:
--------------------------------------------------------------------------------
1 | import {repository} from '@loopback/repository';
2 | import {getModelSchemaRef, post, requestBody, response} from '@loopback/rest';
3 | import {DonateDevice} from '../models';
4 | import {DonateDeviceRepository} from '../repositories';
5 | import sendSMS from './../utils/sendSMS';
6 | import {DonateDevice as DonateDeviceType} from './donate-device-graphQL-model';
7 | import {graphQLHelper} from './graphQL-helper';
8 |
9 | export class DonateDeviceController {
10 | constructor(
11 | @repository(DonateDeviceRepository)
12 | public donateDeviceRepository: DonateDeviceRepository,
13 | ) {}
14 |
15 | @post('/donate-devices')
16 | @response(200, {
17 | description: 'DonateDevice model instance',
18 | content: {'application/json': {schema: getModelSchemaRef(DonateDevice)}},
19 | })
20 | async create(
21 | @requestBody({
22 | content: {
23 | 'application/json': {
24 | schema: getModelSchemaRef(DonateDevice, {
25 | title: 'NewDonateDevice',
26 | exclude: ['id'],
27 | }),
28 | },
29 | },
30 | })
31 | donateDevice: Omit,
32 | ): Promise {
33 | const instanceID = donateDevice.data[0]?.instanceID;
34 | const trackingKey = instanceID
35 | .split(':')?.[1]
36 | ?.split('-')?.[0]
37 | .toUpperCase();
38 | const data = donateDevice?.data?.[0];
39 | const smsBody = `You have successfully registered for donating your smartphone as part of "Baccho ka Sahara, Phone Humara" campaign. Your tracking ID is ${trackingKey}. You can use this ID to track the status of delivery for your donated device.\n\n- Samagra Shiksha, Himachal Pradesh`;
40 | const contactNumber = data.contact;
41 | const smsDispatchResponse = sendSMS(smsBody, trackingKey, contactNumber);
42 |
43 | data.trackingKey = trackingKey;
44 | const donateDeviceType = new DonateDeviceType(data);
45 | const gQLHelper = new graphQLHelper();
46 | if (donateDeviceType.phone_number) {
47 | const {errors, data: gqlResponse} = await gQLHelper.startExecuteInsert(
48 | donateDeviceType,
49 | );
50 | if (errors) {
51 | console.error(errors);
52 | } else {
53 | console.log(gqlResponse);
54 | }
55 | }
56 | return this.donateDeviceRepository.create(donateDevice);
57 | }
58 |
59 | // @get('/donate-devices/count')
60 | // @response(200, {
61 | // description: 'DonateDevice model count',
62 | // content: { 'application/json': { schema: CountSchema } },
63 | // })
64 | // async count(
65 | // @param.where(DonateDevice) where?: Where,
66 | // ): Promise {
67 | // return this.donateDeviceRepository.count(where);
68 | // }
69 |
70 | // @patch('/donate-devices')
71 | // @response(200, {
72 | // description: 'DonateDevice PATCH success count',
73 | // content: { 'application/json': { schema: CountSchema } },
74 | // })
75 | // async updateAll(
76 | // @requestBody({
77 | // content: {
78 | // 'application/json': {
79 | // schema: getModelSchemaRef(DonateDevice, { partial: true }),
80 | // },
81 | // },
82 | // })
83 | // donateDevice: DonateDevice,
84 | // @param.where(DonateDevice) where?: Where,
85 | // ): Promise {
86 | // return this.donateDeviceRepository.updateAll(donateDevice, where);
87 | // }
88 |
89 | // @get('/donate-devices/{id}')
90 | // @response(200, {
91 | // description: 'DonateDevice model instance',
92 | // content: {
93 | // 'application/json': {
94 | // schema: getModelSchemaRef(DonateDevice, { includeRelations: true }),
95 | // },
96 | // },
97 | // })
98 | // async findById(
99 | // @param.path.string('id') id: string,
100 | // @param.filter(DonateDevice, { exclude: 'where' }) filter?: FilterExcludingWhere
101 | // ): Promise {
102 | // return this.donateDeviceRepository.findById(id, filter);
103 | // }
104 |
105 | // @patch('/donate-devices/{id}')
106 | // @response(204, {
107 | // description: 'DonateDevice PATCH success',
108 | // })
109 | // async updateById(
110 | // @param.path.string('id') id: string,
111 | // @requestBody({
112 | // content: {
113 | // 'application/json': {
114 | // schema: getModelSchemaRef(DonateDevice, { partial: true }),
115 | // },
116 | // },
117 | // })
118 | // donateDevice: DonateDevice,
119 | // ): Promise {
120 | // await this.donateDeviceRepository.updateById(id, donateDevice);
121 | // }
122 |
123 | // @put('/donate-devices/{id}')
124 | // @response(204, {
125 | // description: 'DonateDevice PUT success',
126 | // })
127 | // async replaceById(
128 | // @param.path.string('id') id: string,
129 | // @requestBody() donateDevice: DonateDevice,
130 | // ): Promise {
131 | // await this.donateDeviceRepository.replaceById(id, donateDevice);
132 | // }
133 |
134 | // @del('/donate-devices/{id}')
135 | // @response(204, {
136 | // description: 'DonateDevice DELETE success',
137 | // })
138 | // async deleteById(@param.path.string('id') id: string): Promise {
139 | // await this.donateDeviceRepository.deleteById(id);
140 | // }
141 | }
142 |
--------------------------------------------------------------------------------
/api/src/controllers/request-device.controller.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Count,
3 | CountSchema,
4 | Filter,
5 | FilterExcludingWhere,
6 | repository,
7 | Where,
8 | } from '@loopback/repository';
9 | import {
10 | post,
11 | param,
12 | get,
13 | getModelSchemaRef,
14 | patch,
15 | put,
16 | del,
17 | requestBody,
18 | response,
19 | } from '@loopback/rest';
20 | import {RequestDevice} from '../models';
21 | import {RequestDeviceRepository} from '../repositories';
22 | import {RequestDevice as RequestDeviceType} from './request-device-graphQL-model';
23 | import {graphQLHelper} from './graphQL-helper';
24 |
25 | export class RequestDeviceController {
26 | constructor(
27 | @repository(RequestDeviceRepository)
28 | public requestDeviceRepository: RequestDeviceRepository,
29 | ) {}
30 |
31 | @post('/request-devices')
32 | @response(200, {
33 | description: 'RequestDevice model instance',
34 | content: {'application/json': {schema: getModelSchemaRef(RequestDevice)}},
35 | })
36 | async create(
37 | @requestBody({
38 | content: {
39 | 'application/json': {
40 | schema: getModelSchemaRef(RequestDevice, {
41 | title: 'NewRequestDevice',
42 | exclude: ['id'],
43 | }),
44 | },
45 | },
46 | })
47 | requestDevice: Omit,
48 | ): Promise {
49 | const instanceID = requestDevice.data[0]?.instanceID;
50 | const filter = {where: {'data.instanceID': instanceID}};
51 | const existingRecord = await this.requestDeviceRepository.findOne(filter);
52 | if (!existingRecord) {
53 | const data = requestDevice?.data?.[0];
54 | const requestDeviceType = new RequestDeviceType(data);
55 | const gQLHelper = new graphQLHelper();
56 | const {errors, data: gqlResponse} = await gQLHelper.startExecuteInsert(
57 | requestDeviceType,
58 | );
59 | if (errors) {
60 | console.error(errors);
61 | } else {
62 | console.log(gqlResponse);
63 | }
64 | return this.requestDeviceRepository.create(requestDevice);
65 | } else return existingRecord;
66 | }
67 | /*
68 | @get('/request-devices/count')
69 | @response(200, {
70 | description: 'RequestDevice model count',
71 | content: {'application/json': {schema: CountSchema}},
72 | })
73 | async count(
74 | @param.where(RequestDevice) where?: Where,
75 | ): Promise {
76 | return this.requestDeviceRepository.count(where);
77 | }
78 |
79 | @get('/request-devices')
80 | @response(200, {
81 | description: 'Array of RequestDevice model instances',
82 | content: {
83 | 'application/json': {
84 | schema: {
85 | type: 'array',
86 | items: getModelSchemaRef(RequestDevice, {includeRelations: true}),
87 | },
88 | },
89 | },
90 | })
91 | async find(
92 | @param.filter(RequestDevice) filter?: Filter,
93 | ): Promise {
94 | return this.requestDeviceRepository.find(filter);
95 | }
96 |
97 | @patch('/request-devices')
98 | @response(200, {
99 | description: 'RequestDevice PATCH success count',
100 | content: {'application/json': {schema: CountSchema}},
101 | })
102 | async updateAll(
103 | @requestBody({
104 | content: {
105 | 'application/json': {
106 | schema: getModelSchemaRef(RequestDevice, {partial: true}),
107 | },
108 | },
109 | })
110 | requestDevice: RequestDevice,
111 | @param.where(RequestDevice) where?: Where,
112 | ): Promise {
113 | return this.requestDeviceRepository.updateAll(requestDevice, where);
114 | }
115 |
116 | @get('/request-devices/{id}')
117 | @response(200, {
118 | description: 'RequestDevice model instance',
119 | content: {
120 | 'application/json': {
121 | schema: getModelSchemaRef(RequestDevice, {includeRelations: true}),
122 | },
123 | },
124 | })
125 | async findById(
126 | @param.path.string('id') id: string,
127 | @param.filter(RequestDevice, {exclude: 'where'})
128 | filter?: FilterExcludingWhere,
129 | ): Promise {
130 | return this.requestDeviceRepository.findById(id, filter);
131 | }
132 |
133 | @patch('/request-devices/{id}')
134 | @response(204, {
135 | description: 'RequestDevice PATCH success',
136 | })
137 | async updateById(
138 | @param.path.string('id') id: string,
139 | @requestBody({
140 | content: {
141 | 'application/json': {
142 | schema: getModelSchemaRef(RequestDevice, {partial: true}),
143 | },
144 | },
145 | })
146 | requestDevice: RequestDevice,
147 | ): Promise {
148 | await this.requestDeviceRepository.updateById(id, requestDevice);
149 | }
150 |
151 | @put('/request-devices/{id}')
152 | @response(204, {
153 | description: 'RequestDevice PUT success',
154 | })
155 | async replaceById(
156 | @param.path.string('id') id: string,
157 | @requestBody() requestDevice: RequestDevice,
158 | ): Promise {
159 | await this.requestDeviceRepository.replaceById(id, requestDevice);
160 | }
161 |
162 | @del('/request-devices/{id}')
163 | @response(204, {
164 | description: 'RequestDevice DELETE success',
165 | })
166 | async deleteById(@param.path.string('id') id: string): Promise {
167 | await this.requestDeviceRepository.deleteById(id);
168 | }
169 | */
170 | }
171 |
--------------------------------------------------------------------------------
/portal/components/react-admin/base/resources/request-device.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {
3 | List,
4 | SimpleList,
5 | Datagrid,
6 | TextField,
7 | BooleanField,
8 | FunctionField,
9 | Edit,
10 | SimpleForm,
11 | NullableBooleanInput,
12 | Filter,
13 | SearchInput,
14 | } from "react-admin";
15 |
16 | import { Typography, makeStyles, useMediaQuery } from "@material-ui/core";
17 | import EditNoDeleteToolbar from "../components/EditNoDeleteToolbar";
18 | import BackButton from "../components/BackButton";
19 | import blueGrey from "@material-ui/core/colors/blueGrey";
20 |
21 | const useStyles = makeStyles((theme) => ({
22 | searchBar: {
23 | "& > div": {
24 | fontSize: "1rem",
25 | },
26 | },
27 | smSearchBar: {
28 | "& > div": {
29 | fontSize: "1.2rem",
30 | },
31 | },
32 | smList: {
33 | margin: "1rem 4rem",
34 | "& > div": {
35 | paddingLeft: 0,
36 | backgroundColor: "unset",
37 | "&:first-child > div": {
38 | backgroundColor: "unset",
39 | },
40 | "&:last-child > div": {
41 | backgroundColor: "#FFF",
42 | boxShadow:
43 | "0px 2px 1px -1px rgb(0 0 0 / 20%), 0px 1px 1px 0px rgb(0 0 0 / 14%), 0px 1px 3px 0px rgb(0 0 0 / 12%)",
44 | },
45 | },
46 | },
47 | list: {
48 | margin: "0rem 2rem",
49 | },
50 | filter: {
51 | paddingLeft: 0,
52 | },
53 | grid: {
54 | display: "grid",
55 | width: "100%",
56 | gridTemplateColumns: "1fr 1fr 1fr",
57 | gridRowGap: "1ch",
58 | gridColumnGap: "1ch",
59 | margin: "1rem 0",
60 | "& > td": theme.overrides.MuiTableCell.head,
61 | "& > span": {
62 | fontSize: "1.1rem",
63 | },
64 | },
65 | fullWidthGrid: {
66 | gridTemplateColumns: "1fr",
67 | },
68 | heading: {
69 | fontSize: "1.4rem",
70 | lineHeight: "0.5rem",
71 | fontWeight: 700,
72 | fontVariant: "all-small-caps",
73 | },
74 | select: {
75 | "& > div > div": {
76 | fontSize: "1.1rem",
77 | },
78 | },
79 | warning: {
80 | margin: "0",
81 | padding: "0",
82 | paddingBottom: "1rem",
83 | textAlign: "center",
84 | width: "100%",
85 | fontStyle: "oblique",
86 | },
87 | grey: {
88 | color: blueGrey[300],
89 | },
90 | }));
91 |
92 | /**
93 | * Donate Device Request List
94 | * @param {*} props
95 | */
96 | export const RequestDeviceList = (props) => {
97 | const isSmall = useMediaQuery((theme) => theme.breakpoints.down("sm"));
98 | const classes = useStyles();
99 | return (
100 |
106 | {isSmall ? (
107 | record.name}
109 | secondaryText={(record) => record.district}
110 | tertiaryText={(record) => record.student_count_no_smartphone}
111 | linkType="edit"
112 | />
113 | ) : (
114 |
115 |
116 |
117 | {
120 | if (record) {
121 | return record.district
122 | ? record.district
123 | : record.other_district;
124 | }
125 | }}
126 | />
127 |
128 |
129 |
133 |
134 | )}
135 |
136 | );
137 | };
138 |
139 | export const RequestDeviceEdit = (props) => {
140 | const classes = useStyles();
141 | const Title = ({ record }) => {
142 | return (
143 |
144 | School #{record.udise} details
145 |
146 | );
147 | };
148 | return (
149 |
150 |
}>
151 |
}>
152 |
153 |
School Details
154 |
155 |
Name |
156 | Phone Number |
157 | District |
158 |
159 |
165 |
171 | Block |
172 | Pincode |
173 | School Name |
174 |
180 |
186 |
192 |
193 |
Demand Overview
194 |
195 |
Total Students |
196 | Students without smartphone |
197 | |
198 |
199 |
203 |
204 |
205 |
206 |
207 |
208 | );
209 | };
210 |
--------------------------------------------------------------------------------
/portal/components/config.js:
--------------------------------------------------------------------------------
1 | const resourceConfig = {
2 | personas: [
3 | // {
4 | // consonant: true,
5 | // en: "school head",
6 | // hi: "स्कूल प्रमुख",
7 | // credentials: "e-Samwad",
8 | // applicationId: process.env.NEXT_PUBLIC_FUSIONAUTH_SCHOOL_APP_ID,
9 | // redirectUrl: "school",
10 | // },
11 | {
12 | consonant: false,
13 | en: "official",
14 | hi: "अधिकारी",
15 | credentials: "Shiksha Saathi",
16 | applicationId: process.env.NEXT_PUBLIC_FUSIONAUTH_STATE_APP_ID,
17 | redirectUrl: `admin#/candidate_profile`,
18 | },
19 | ],
20 | homepageCards: [
21 | {
22 | // title: {
23 | // en: "Donate your smartphone",
24 | // hi: "अपना स्मार्टफ़ोन दान करें",
25 | // },
26 | // target: "/donate",
27 | // icon: "volunteer_activism",
28 | // colour: "primary",
29 | // },{
30 | // title: {
31 | // en: "Donate a Smartphone as an Individual Donor",
32 | // hi: "व्यक्तिगत दाता",
33 | // },
34 | // target: process.env.NEXT_PUBLIC_DONATE_DEVICE_INDIV_FORM_URL,
35 | // icon: "volunteer_activism",
36 | // colour: "primary",
37 | // },
38 | // {
39 | // title: {
40 | // en: "Donate a smartphone as a Corporate Donor",
41 | // hi: "कॉर्पोरेट दाता",
42 | // },
43 | // target: process.env.NEXT_PUBLIC_DONATE_DEVICE_CORP_FORM_URL,
44 | // icon: "corporate_fare",
45 | // colour: "primary",
46 | // },
47 | // {
48 | // title: {
49 | // en: "Frequently Asked Questions",
50 | // hi: "जानकारी",
51 | // },
52 | // target: process.env.NEXT_PUBLIC_FAQ_DOCUMENT_URL,
53 | // icon: "quiz",
54 | // colour: "primary",
55 | // },
56 | // {
57 | title: {
58 | en: "Login for state officials",
59 | hi: "राज्य के अधिकारियों के लिए लॉग इन",
60 | },
61 | target: "/login",
62 | icon: "login",
63 | colour: "secondary",
64 | },
65 | // {
66 | // title: {
67 | // en: "Track your smartphone and get your Digi Saathi certificate",
68 | // hi: "अपने स्मार्टफ़ोन को ट्रैक करें और अपना Digi साथी प्रशंसा पत्र लें",
69 | // },
70 | // target: "/track",
71 | // icon: "grading",
72 | // colour: "secondary",
73 | // },
74 | ],
75 | donatePageCards: [
76 | {
77 | title: {
78 | en: "Individual donor",
79 | hi: "व्यक्तिगत दाता ",
80 | },
81 | target: process.env.NEXT_PUBLIC_DONATE_DEVICE_INDIV_FORM_URL,
82 | icon: "volunteer_activism",
83 | colour: "primary",
84 | },
85 | {
86 | title: {
87 | en: "Corporate donor",
88 | hi: "कॉर्पोरेट दाता",
89 | },
90 | target: process.env.NEXT_PUBLIC_DONATE_DEVICE_CORP_FORM_URL,
91 | icon: "corporate_fare",
92 | colour: "primary",
93 | },
94 | ],
95 | schoolPageCards: [
96 | {
97 | title: {
98 | en: "Demand estimation form",
99 | hi: "स्मार्टफ़ोन लागत अनुमान प्रपत्र भरें ",
100 | },
101 | target: process.env.NEXT_PUBLIC_REQUEST_DEVICE_FORM_URL,
102 | icon: "smartphone",
103 | colour: "primary",
104 | },
105 | {
106 | title: {
107 | en: "Update donee data",
108 | hi: "लाभार्थी जानकारी भरें",
109 | },
110 | target: "/admin",
111 | icon: "login",
112 | colour: "secondary",
113 | },
114 | ],
115 | statusChoices: [
116 | {
117 | id: "no-action-taken",
118 | name: "Donation in Progress", //No Action Taken
119 | icon: "warning",
120 | style: "error",
121 | },
122 | {
123 | id: "donor-no-init",
124 | name: "Delivery Not Initiated",
125 | icon: "pending_actions",
126 | style: "error",
127 | },
128 | {
129 | id: "donor-init",
130 | name: "Delivery Initiated",
131 | icon: "inventory",
132 | style: "pending",
133 | },
134 | {
135 | id: "received-state",
136 | name: "Received by state",
137 | icon: "real_estate_agent",
138 | style: "success",
139 | templateId: "1007356590433077522",
140 | template:
141 | "Congratulations! Your donated device with the tracking ID {#var#} has been successfully received by Samagra Shiksha, Himachal Pradesh.\nYou can visit the donation portal to download your DigitalSaathi eCertificate.\n\n\n- Samagra Shiksha, Himachal Pradesh",
142 | variables: ["device_tracking_key"],
143 | },
144 | {
145 | id: "delivered-child",
146 | name: "Delivered to child",
147 | icon: "check_circle",
148 | style: "success",
149 | templateId: "1007267945817846297",
150 | template:
151 | "Congratulations! Your donated device with the tracking ID {#var#} has been successfully donated to a child-in-need from {#var#},({#var#}) . Thank you for your contribution to a student's online learning.\n\n\n- Samagra Shiksha, Himachal Pradesh",
152 | variables: [
153 | "device_tracking_key",
154 | "recipient_school.name",
155 | "recipient_school.location.district",
156 | ],
157 | },
158 | {
159 | id: "cancelled",
160 | name: "Cancelled",
161 | icon: "disabled_by_default",
162 | style: "error",
163 | },
164 | ],
165 | deliveryTypeChoices: [
166 | { id: "hand", name: "Hand Off", filterable: true },
167 | { id: "pickup", name: "Pick Up", filterable: true },
168 | { id: "courier", name: "Courier", filterable: true },
169 | { id: "handnonhp", name: "Hand Off (outside HP)" },
170 | { id: "couriernonhp", name: "Courier (outside HP)" },
171 | ],
172 | gradeChoices: [
173 | {
174 | id: 1,
175 | name: "1",
176 | },
177 | {
178 | id: 2,
179 | name: "2",
180 | },
181 | {
182 | id: 3,
183 | name: "3",
184 | },
185 | {
186 | id: 4,
187 | name: "4",
188 | },
189 | {
190 | id: 5,
191 | name: "5",
192 | },
193 | {
194 | id: 6,
195 | name: "6",
196 | },
197 | {
198 | id: 7,
199 | name: "7",
200 | },
201 | {
202 | id: 8,
203 | name: "8",
204 | },
205 | {
206 | id: 9,
207 | name: "9",
208 | },
209 | {
210 | id: 10,
211 | name: "10",
212 | },
213 | {
214 | id: 11,
215 | name: "11",
216 | },
217 | {
218 | id: 12,
219 | name: "12",
220 | },
221 | ],
222 | };
223 |
224 | export default resourceConfig;
225 |
--------------------------------------------------------------------------------
/portal/components/react-admin/base/resources/candidateProfile/candidate-show.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {
3 | List,
4 | Datagrid,
5 | Pagination,
6 | TextField,
7 | DateField,
8 | FunctionField,
9 | TopToolbar,
10 | sanitizeListRestProps,
11 | UrlField,
12 | BooleanField,
13 | RichTextField,
14 | ArrayField,
15 | SingleFieldList,
16 | FileField,
17 | Show,
18 | Tab,
19 | TabbedShowLayout,
20 | SimpleShowLayout,
21 | Filter,
22 | SelectInput,
23 | SearchInput,
24 | ExportButton,
25 | AutocompleteInput,
26 | useQuery,
27 | downloadCSV,
28 | } from "react-admin";
29 | import { makeStyles, Typography } from "@material-ui/core";
30 |
31 | function getAge({ start, end }) {
32 | var today = end ? new Date(end) : new Date();
33 | var birthDate = new Date(start);
34 | var age = today.getFullYear() - birthDate.getFullYear();
35 | var m = today.getMonth() - birthDate.getMonth();
36 | let roundedDownAge = age;
37 | if (m < 0 || (m === 0 && today.getDate() < birthDate.getDate())) {
38 | roundedDownAge--;
39 | }
40 | if (today < birthDate) {
41 | return { years: "Invalid Date", months: "Invalid Date" };
42 | }
43 | return { years: roundedDownAge, months: age * 12 + m };
44 | }
45 |
46 | const useStyles = makeStyles((theme) => ({
47 | root: {
48 | width: "calc(100% - 0px)",
49 | height: "86vh",
50 | marginTop: theme.spacing.unit * 3,
51 | overflowX: "auto",
52 | overflowY: "scroll",
53 | marginLeft: "1rem",
54 | },
55 | }));
56 |
57 | export const CandidateShow = (props) => {
58 | const CustomFileField = ({ record, ...props }) => {
59 | const url = generateResumeLink(record?.data?.[0]?.resume?.url);
60 | return (
61 |
68 | {url ? (
69 |
70 | Resume
71 |
72 | ) : (
73 |
No Resume uploaded
74 | )}
75 |
76 | );
77 | };
78 | return (
79 |
80 |
81 |
82 |
83 |
84 | {
87 | if (record) {
88 | return getAge({
89 | start: record.DOB,
90 | end: null,
91 | }).years;
92 | }
93 | }}
94 | />
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
107 |
111 |
112 |
113 |
114 |
118 |
122 | {
125 | if (record) {
126 | console.log("Role:", record);
127 | if (record.current_employed_status === 1) {
128 | return `${record.job_role}, ${record.employer_organization_name}`;
129 | } else return "N/A";
130 | }
131 | }}
132 | />
133 |
134 | {
137 | if (record && record.work_experience_details) {
138 | return `${record.work_experience_details.work_experience_choices}`;
139 | } else return "N/A";
140 | }}
141 | />
142 | {
145 | if (record) {
146 | if (record.current_employed_status === 1) {
147 | return `₹${record.monthly_salary_details.salary_range}`;
148 | } else return "N/A";
149 | }
150 | }}
151 | />
152 |
153 |
154 |
158 |
162 |
163 |
167 |
171 |
172 | {
175 | if (record) {
176 | return `${
177 | record.sector_preference_1.sector_preference_name || "None"
178 | }, ${
179 | record.sector_preference_2.sector_preference_name || "None"
180 | }, ${
181 | record.sector_preference_3.sector_preference_name || "None"
182 | }`;
183 | }
184 | }}
185 | />
186 | {
189 | if (record) {
190 | return `${record.skill_1 || "None"}, ${
191 | record.skill_2 || "None"
192 | }, ${record.skill_3 || "None"}, ${record.skill_4 || "None"}`;
193 | }
194 | }}
195 | />
196 |
200 |
201 | {/* */}
202 |
203 |
204 |
205 | );
206 | };
207 |
--------------------------------------------------------------------------------
/portal/components/react-admin/base/resources/candidateProfile/candidate-profile.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {
3 | List,
4 | Datagrid,
5 | Pagination,
6 | FunctionField,
7 | TopToolbar,
8 | sanitizeListRestProps,
9 | Filter,
10 | SearchInput,
11 | ExportButton,
12 | downloadCSV,
13 | } from "react-admin";
14 | import { makeStyles, Typography } from "@material-ui/core";
15 | import jsonExport from "jsonexport/dist";
16 |
17 | const SearchFilter = (props) => {
18 | console.log("Props:", props);
19 | return (
20 |
21 |
27 |
28 | );
29 | };
30 | const exporter = (records) => {
31 | const recordsForExport = records.map((record) => {
32 | let age = getAge({
33 | start: record.DOB,
34 | end: null,
35 | }).years;
36 |
37 | return {
38 | "Candidate Name": record.name ? record.name : "NONE",
39 | "Mobile Number": record.mobile_number ? record.mobile_number : "NONE",
40 | "Whatsapp Number": record.whatsapp_mobile_number
41 | ? record.whatsapp_mobile_number
42 | : "NONE",
43 | District: record.district_name.name ? record.district_name.name : "NONE",
44 | Pincode: record.pincode ? record.pincode : "NONE",
45 | "Max Qualification": record.highest_level_qualification
46 | .highest_level_qualification_name
47 | ? record.highest_level_qualification.highest_level_qualification_name
48 | : "NONE",
49 | Qualification: record.qualification_detail.qualification_name
50 | ? record.qualification_detail.qualification_name
51 | : "NONE",
52 | Marks: record.final_score_highest_qualification
53 | ? record.final_score_highest_qualification
54 | : "NONE",
55 | "Date of Birth": record.DOB,
56 | Age: age,
57 | Gender: record.gender.gender_name,
58 | "Have you ever been employed": record.ever_employment.employment_status
59 | ? record.ever_employment.employment_status
60 | : "NONE",
61 | "Job Role": record.job_role ? record.job_role : "NONE",
62 | "Company Name": record.employer_organization_name
63 | ? record.employer_organization_name
64 | : "NONE",
65 | "Total work experience (months)": record.work_experience_details
66 | ? record.work_experience_details.work_experience_choices
67 | : "NONE",
68 | "Monthly salary (Rs.)": record.monthly_salary_details
69 | ? record.monthly_salary_details.salary_range
70 | : "NONE",
71 | "Driving License": record.driver_license.driver_license_choice,
72 | "Distance willing to travel":
73 | record.district_travel.district_travel_choice,
74 | "PAN Card Availability": record.pan_card.pan_card_choice,
75 | "English speaking competency":
76 | record.english_knowledge_choice.english_choice,
77 | "Computer operating competencies":
78 | record.computer_operator.computer_operator_choices,
79 | // "Preferred Sectors": `${record.sector_preference_1.sector_preference_name},${record.sector_preference_2.sector_preference_name},${record.sector_preference_3.sector_preference_name}`,
80 | "Preferred Skill #1": record.skill_1,
81 | "Preferred Skill #2": record.skill_2,
82 | "Preferred Skill #3": record.skill_3,
83 | "Preferred Skill #4": record.skill_4,
84 | "Expected Salary": record.expected_salary.salary_range,
85 | "Resume (URL)": "",
86 | };
87 | });
88 | jsonExport(recordsForExport, (err, csv) => {
89 | downloadCSV(
90 | csv,
91 | `candidateData_${new Date(Date.now()).toLocaleDateString()}`
92 | );
93 | });
94 | };
95 |
96 | function getAge({ start, end }) {
97 | var today = end ? new Date(end) : new Date();
98 | var birthDate = new Date(start);
99 | var age = today.getFullYear() - birthDate.getFullYear();
100 | var m = today.getMonth() - birthDate.getMonth();
101 | let roundedDownAge = age;
102 | if (m < 0 || (m === 0 && today.getDate() < birthDate.getDate())) {
103 | roundedDownAge--;
104 | }
105 | if (today < birthDate) {
106 | return { years: "Invalid Date", months: "Invalid Date" };
107 | }
108 | return { years: roundedDownAge, months: age * 12 + m };
109 | }
110 |
111 | const CandidateActions = (props) => (
112 |
113 |
114 |
115 | );
116 |
117 | const useStyles = makeStyles((theme) => ({
118 | root: {
119 | width: "calc(100% - 0px)",
120 | height: "86vh",
121 | marginTop: theme.spacing.unit * 3,
122 | overflowX: "auto",
123 | overflowY: "scroll",
124 | marginLeft: "1rem",
125 | },
126 | }));
127 | export const CandidateList = (props) => {
128 | console.log("Entered Candidate");
129 | const classes = useStyles();
130 |
131 | return (
132 |
133 |
}
137 | bulkActionButtons={false}
138 | filters={
}
139 | pagination={
}
140 | exporter={exporter}
141 | >
142 |
143 | `${record.name}`} />
144 | {
147 | if (record && record.DOB) {
148 | console.log(record);
149 | return getAge({
150 | start: record.DOB,
151 | end: null,
152 | }).years;
153 | }
154 | }}
155 | />
156 | {
159 | if (record && record.gender) {
160 | return record.gender.gender_name;
161 | }
162 | }}
163 | />
164 | `${record.whatsapp_mobile_number}`}
167 | />
168 | {
171 | if (record && record.district_name) {
172 | return record.district_name.name;
173 | }
174 | }}
175 | />
176 | `${record.pincode}`}
179 | />
180 | {
183 | if (record && record.highest_level_qualification) {
184 | return record.highest_level_qualification
185 | .highest_level_qualification_name;
186 | }
187 | }}
188 | />
189 | {
192 | if (record && record.qualification_detail) {
193 | return record.qualification_detail.qualification_name;
194 | }
195 | }}
196 | />
197 | `${record.final_score_highest_qualification}`}
200 | />
201 |
202 |
203 |
204 | );
205 | };
206 |
--------------------------------------------------------------------------------
/captcha-service/captchapng.js:
--------------------------------------------------------------------------------
1 | /**
2 | * captchapng
3 | * Captcha PNG generator
4 | * @Author: George Chan
5 | * @Email: gchan@21cn.com
6 | * @Version: 0.0.1
7 | * @Date: 2013-08-18
8 | * @license http://www.opensource.org/licenses/bsd-license.php BSD License
9 | */
10 |
11 | var pnglib = require('pnglib');
12 | this.numMask = [];
13 | this.numMask[0]=[];
14 | this.numMask[0]=loadNumMask0();
15 | this.numMask[1]=loadNumMask1();
16 | myself = this;
17 |
18 | function loadNumMask0() {
19 | var numbmp=[];
20 | numbmp[0]=["0011111000","0111111110","0111111110","1110001111","1110001111","1110001111","1110001111","1110001111","1110001111","1110001111","1110001111","1110001111","1110001111","1110001111","1110001111","1110001111","0111111111"," 111111110","0011111100"];
21 | numbmp[1]=["0000011","0000111","0011111","1111111","1111111","0001111","0001111","0001111","0001111","0001111","0001111","0001111","0001111","0001111","0001111","0001111","0001111","0001111","0001111"];
22 | numbmp[2]=["001111100","011111110","111111111","111001111","111001111","111001111","111001111","000011111","000011110","000111110","000111100","000111100","001111000","001111000","011110000","011110000","111111111","111111111","111111111"];
23 | numbmp[3]=["0011111100","0111111110","1111111111","1111001111","1111001111","1111001111","0000001111","0001111110","0001111100","0001111111","0000001111","1111001111","1111001111","1111001111","1111001111","1111001111","1111111111","0111111110","0011111100"];
24 | numbmp[4]=["00001111110","00001111110","00011111110","00011111110","00011111110","00111011110","00111011110","00111011110","01110011110","01110011110","01110011110","11100011110","11111111111","11111111111","11111111111","11111111111","00000011110","00000011110","00000011110"];
25 | numbmp[5]=["1111111111","1111111111","1111111111","1111000000","1111000000","1111011100","1111111110","1111111111","1111001111","1111001111","0000001111","0000001111","1111001111","1111001111","1111001111","1111001111","1111111111","0111111110","0011111100"];
26 | numbmp[6]=["0011111100","0111111110","0111111111","1111001111","1111001111","1111000000","1111011100","1111111110","1111111111","1111001111","1111001111","1111001111","1111001111","1111001111","1111001111","1111001111","0111111111","0111111110","0011111100"];
27 | numbmp[7]=["11111111","11111111","11111111","00001111","00001111","00001111","00001110","00001110","00011110","00011110","00011110","00011100","00111100","00111100","00111100","00111100","00111000","01111000","01111000"];
28 | numbmp[8]=["0011111100","0111111110","1111111111","1111001111","1111001111","1111001111","1111001111","0111111110","0011111100","0111111110","1111001111","1111001111","1111001111","1111001111","1111001111","1111001111","1111111111","0111111110","0011111100"];
29 | numbmp[9]=["0011111100","0111111110","1111111111","1111001111","1111001111","1111001111","1111001111","1111001111","1111001111","1111001111","1111111111","0111111111","0011101111","0000001111","1111001111","1111001111","1111111110","0111111110","0011111000"];
30 |
31 | return numbmp;
32 | }
33 |
34 | function loadNumMask1() {
35 | var numbmp=[];
36 | numbmp[0] = ["000000001111000","000000111111110","000001110000110","000011000000011","000110000000011","001100000000011","011100000000011","011000000000011","111000000000110","110000000000110","110000000001110","110000000001100","110000000011000","110000000111000","011000011110000","011111111000000","000111110000000"];
37 | numbmp[1] = ["00000111","00001111","00011110","00010110","00001100","00001100","00011000","00011000","00110000","00110000","00110000","01100000","01100000","01100000","11000000","11000000","11000000"];
38 | numbmp[2] = ["00000011111000","00001111111110","00011100000110","00011000000011","00000000000011","00000000000011","00000000000011","00000000000110","00000000001110","00000000011100","00000001110000","00000111100000","00001110000000","00111100000000","01110000000000","11111111110000","11111111111110","00000000011110"];
39 | numbmp[3] = ["000000111111000","000011111111110","000111100000111","000110000000011","000000000000011","000000000000011","000000000001110","000000111111000","000000111111000","000000000011100","000000000001100","000000000001100","110000000001100","111000000011100","111100000111000","001111111110000","000111111000000"];
40 | numbmp[4] = ["00000011000001","00000110000011","00001100000010","00011000000110","00111000000110","00110000001100","01100000001100","01100000001000","11000000011000","11111111111111","11111111111111","00000000110000","00000000110000","00000000100000","00000001100000","00000001100000","00000001100000"];
41 | numbmp[5] = ["0000001111111111","0000011111111111","0000111000000000","0000110000000000","0000110000000000","0001110000000000","0001101111100000","0001111111111000","0001110000011000","0000000000001100","0000000000001100","0000000000001100","1100000000001100","1110000000011000","1111000001111000","0111111111100000","0001111110000000"];
42 | numbmp[6] = ["000000001111100","000000111111110","000011110000111","000111000000011","000110000000000","001100000000000","011001111100000","011111111111000","111110000011000","111000000001100","110000000001100","110000000001100","110000000001100","111000000011000","011100001110000","001111111100000","000111110000000"];
43 | numbmp[7] = ["1111111111111","1111111111111","0000000001110","0000000011100","0000000111000","0000000110000","0000001100000","0000011100000","0000111000000","0000110000000","0001100000000","0011100000000","0011000000000","0111000000000","1110000000000","1100000000000","1100000000000"];
44 | numbmp[8] = ["0000000111110000","0000011111111100","0000011000001110","0000110000000111","0000110000011111","0000110001111000","0000011111100000","0000011110000000","0001111111000000","0011100011100000","0111000001110000","1110000000110000","1100000000110000","1100000001110000","1110000011100000","0111111111000000","0001111100000000"];
45 | numbmp[9] = ["0000011111000","0001111111110","0011100000110","0011000000011","0110000000011","0110000000011","0110000000011","0110000000111","0011000011110","0011111111110","0000111100110","0000000001100","0000000011000","0000000111000","0000011110000","1111111000000","1111110000000"];
46 | return numbmp;
47 | }
48 |
49 |
50 | function captchapng(width,height,dispNumber) {
51 | this.width = width;
52 | this.height = height;
53 | this.depth = 8;
54 | this.dispNumber = ""+dispNumber.toString();
55 | this.widthAverage = parseInt(this.width/this.dispNumber.length);
56 |
57 | var p = new pnglib(this.width,this.height,this.depth);
58 |
59 | for (var numSection=0;numSection=myself.numMask.length?0:font);
65 | //var random_x_offs = 0, random_y_offs = 0;
66 | var random_x_offs = parseInt(Math.random()*(this.widthAverage - myself.numMask[font][dispNum][0].length));
67 | var random_y_offs = parseInt(Math.random()*(this.height - myself.numMask[font][dispNum].length));
68 | random_x_offs = (random_x_offs<0?0:random_x_offs);
69 | random_y_offs = (random_y_offs<0?0:random_y_offs);
70 |
71 | for (var i=0;(i {
18 | console.log("Props:", props);
19 | return (
20 |
21 |
27 |
28 | );
29 | };
30 | const exporter = (records) => {
31 | const recordsForExport = records.map((record) => {
32 | let age = getAge({
33 | start: record.DOB,
34 | end: null,
35 | }).years;
36 |
37 | return {
38 | "Candidate Name": record.name ? record.name : "NONE",
39 | "Mobile Number": record.mobile_number ? record.mobile_number : "NONE",
40 | "Whatsapp Number": record.whatsapp_mobile_number
41 | ? record.whatsapp_mobile_number
42 | : "NONE",
43 | District: record.district_name.name ? record.district_name.name : "NONE",
44 | Pincode: record.pincode ? record.pincode : "NONE",
45 | "Max Qualification": record.highest_level_qualification
46 | .highest_level_qualification_name
47 | ? record.highest_level_qualification.highest_level_qualification_name
48 | : "NONE",
49 | Qualification: record.qualification_detail.qualification_name
50 | ? record.qualification_detail.qualification_name
51 | : "NONE",
52 | Marks: record.final_score_highest_qualification
53 | ? record.final_score_highest_qualification
54 | : "NONE",
55 | "Date of Birth": record.DOB,
56 | Age: age,
57 | Gender: record.gender.gender_name,
58 | "Have you ever been employed": record.ever_employment.employment_status
59 | ? record.ever_employment.employment_status
60 | : "NONE",
61 | "Job Role": record.job_role ? record.job_role : "NONE",
62 | "Company Name": record.employer_organization_name
63 | ? record.employer_organization_name
64 | : "NONE",
65 | "Total work experience (months)": record.work_experience_details
66 | ? record.work_experience_details.work_experience_choices
67 | : "NONE",
68 | "Monthly salary (Rs.)": record.monthly_salary_details
69 | ? record.monthly_salary_details.salary_range
70 | : "NONE",
71 | "Driving License": record.driver_license.driver_license_choice,
72 | "Distance willing to travel":
73 | record.district_travel.district_travel_choice,
74 | "PAN Card Availability": record.pan_card.pan_card_choice,
75 | "English speaking competency":
76 | record.english_knowledge_choice.english_choice,
77 | "Computer operating competencies":
78 | record.computer_operator.computer_operator_choices,
79 | // "Preferred Sectors": `${record.sector_preference_1.sector_preference_name},${record.sector_preference_2.sector_preference_name},${record.sector_preference_3.sector_preference_name}`,
80 | "Preferred Skill #1": record.skill_1,
81 | "Preferred Skill #2": record.skill_2,
82 | "Preferred Skill #3": record.skill_3,
83 | "Preferred Skill #4": record.skill_4,
84 | "Expected Salary": record.expected_salary.salary_range,
85 | "Resume (URL)": "",
86 | };
87 | });
88 | jsonExport(recordsForExport, (err, csv) => {
89 | downloadCSV(
90 | csv,
91 | `candidateData_${new Date(Date.now()).toLocaleDateString()}`
92 | );
93 | });
94 | };
95 |
96 | function getAge({ start, end }) {
97 | var today = end ? new Date(end) : new Date();
98 | var birthDate = new Date(start);
99 | var age = today.getFullYear() - birthDate.getFullYear();
100 | var m = today.getMonth() - birthDate.getMonth();
101 | let roundedDownAge = age;
102 | if (m < 0 || (m === 0 && today.getDate() < birthDate.getDate())) {
103 | roundedDownAge--;
104 | }
105 | if (today < birthDate) {
106 | return { years: "Invalid Date", months: "Invalid Date" };
107 | }
108 | return { years: roundedDownAge, months: age * 12 + m };
109 | }
110 |
111 | const CandidateActions = (props) => (
112 |
113 |
114 |
115 | );
116 |
117 | const useStyles = makeStyles((theme) => ({
118 | root: {
119 | width: "calc(100% - 0px)",
120 | height: "86vh",
121 | marginTop: theme.spacing.unit * 3,
122 | overflowX: "auto",
123 | overflowY: "scroll",
124 | marginLeft: "1rem",
125 | },
126 | }));
127 | export const RecruiterData = (props) => {
128 | console.log("Entered Recruiter");
129 | const classes = useStyles();
130 |
131 | return (
132 |
133 |
}
137 | bulkActionButtons={false}
138 | filters={
}
139 | pagination={
}
140 | exporter={exporter}
141 | >
142 |
143 |
144 |
145 |
146 |
147 |
148 | `${record.name}`} />
149 | {
152 | if (record && record.DOB) {
153 | console.log(record);
154 | return getAge({
155 | start: record.DOB,
156 | end: null,
157 | }).years;
158 | }
159 | }}
160 | />
161 | {
164 | if (record && record.gender) {
165 | return record.gender.gender_name;
166 | }
167 | }}
168 | />
169 | `${record.whatsapp_mobile_number}`}
172 | />
173 | {
176 | if (record && record.district_name) {
177 | return record.district_name.name;
178 | }
179 | }}
180 | />
181 | `${record.pincode}`}
184 | />
185 | {
188 | if (record && record.highest_level_qualification) {
189 | return record.highest_level_qualification
190 | .highest_level_qualification_name;
191 | }
192 | }}
193 | />
194 | {
197 | if (record && record.qualification_detail) {
198 | return record.qualification_detail.qualification_name;
199 | }
200 | }}
201 | />
202 | `${record.final_score_highest_qualification}`}
205 | />
206 |
207 |
208 |
209 | );
210 | };
211 |
--------------------------------------------------------------------------------
/portal/components/track/track.js:
--------------------------------------------------------------------------------
1 | import Image from "next/image";
2 | import { useState, useRef, useEffect } from "react";
3 | import styles from "../../styles/Track.module.css";
4 | import controls from "./track.config";
5 | import axios from "axios";
6 | import { useToasts } from "react-toast-notifications";
7 | import config from "@/components/config";
8 |
9 | const Track = () => {
10 | const [trackingKey, setTrackingKey] = useState(null);
11 | const [captcha, setCaptcha] = useState(null);
12 | const [captchaToken, setCaptchaToken] = useState(null);
13 | const [refreshToken, setRefreshToken] = useState(null);
14 | const [captchaImg, setCaptchaImg] = useState(null);
15 | const [trackingKeyValid, setTrackingKeyValid] = useState(false);
16 | const [trackingResponse, setTrackingResponse] = useState(null);
17 | const [deliveryStatus, setDeliveryStatus] = useState(false);
18 | const [displayCertificate, setDisplayCertificate] = useState(false);
19 | const captchaRef = useRef(null);
20 |
21 | useEffect(() => {
22 | const obj = config.statusChoices.find(
23 | (elem) => elem.id === trackingResponse?.delivery_status
24 | );
25 | if (obj) {
26 | setDeliveryStatus(obj);
27 | if (["received-state", "delivered-child"].includes(obj.id))
28 | setDisplayCertificate(true);
29 | else setDisplayCertificate(false);
30 | }
31 | }, [trackingResponse]);
32 |
33 | const handleInput = (e) => {
34 | setTrackingKey(e.target.value);
35 | setTrackingKeyValid(e.target.value != "" && captcha && captcha != "");
36 | };
37 |
38 | const handleInputCaptcha = (e) => {
39 | setCaptcha(e.target.value);
40 | setTrackingKeyValid(
41 | e.target.value != "" && trackingKey && trackingKey != ""
42 | );
43 | };
44 |
45 | const { addToast } = useToasts();
46 |
47 | useEffect(() => {
48 | const response = axios
49 | .get(process.env.NEXT_PUBLIC_CAPTCHA_URL)
50 | .then((resp) => {
51 | const { blob } = resp.data;
52 | const { token } = resp.headers;
53 | setCaptchaImg(blob);
54 | setCaptchaToken(token);
55 | })
56 | .catch((err) => {
57 | addToast(err.response?.data?.errors || err.message, {
58 | appearance: "error",
59 | });
60 | });
61 | }, [refreshToken]);
62 |
63 | const handleSubmit = async (e) => {
64 | e.preventDefault();
65 | try {
66 | const response = await axios.post(
67 | `${process.env.NEXT_PUBLIC_API_URL}/track`,
68 | {
69 | id: trackingKey,
70 | captcha: captcha,
71 | captchaToken: captchaToken,
72 | }
73 | );
74 | const { error, success } = response.data;
75 |
76 | if (success) setTrackingResponse(success.data);
77 |
78 | if (error) {
79 | addToast(error, { appearance: "error" });
80 | var rightNow = new Date();
81 | setRefreshToken(rightNow.toISOString());
82 | }
83 | } catch (err) {
84 | addToast(JSON.stringify(err), { appearance: "error" });
85 | }
86 | };
87 |
88 | const fetchCertificate = async (e) => {
89 | e.preventDefault();
90 | try {
91 | const response = await axios.post(
92 | `${process.env.NEXT_PUBLIC_API_URL}/certificate`,
93 | {
94 | name: trackingResponse.name,
95 | trackingKey: trackingKey,
96 | udise: trackingResponse.recipient_school?.udise,
97 | }
98 | );
99 | const { error, success } = response.data;
100 | if (error) {
101 | addToast(error, { appearance: "error" });
102 | }
103 | if (success) {
104 | const pdfData = success.base64String;
105 | const byteCharacters = atob(pdfData);
106 | let byteNumbers = new Array(byteCharacters.length);
107 | for (let i = 0; i < byteCharacters.length; i++) {
108 | byteNumbers[i] = byteCharacters.charCodeAt(i);
109 | }
110 | const byteArray = new Uint8Array(byteNumbers);
111 | const file = new Blob([byteArray], { type: "application/pdf;base64" });
112 | const fileURL = URL.createObjectURL(file);
113 | window.open(fileURL);
114 | }
115 | } catch (err) {
116 | addToast(err.message, { appearance: "error" });
117 | }
118 | };
119 |
120 | return (
121 | <>
122 | Track / ट्रैक
123 |
124 |
125 |
126 | Enter tracking ID / ट्रैकिंग आईडी भरें
127 |
128 |
166 |
167 |
172 |
Status / स्थिति
173 |
174 |
175 |
176 | | Donor Mobile No. |
177 | Status |
178 |
179 |
180 |
181 | {trackingResponse ? (
182 | <>
183 | |
184 | {trackingResponse.phone_number}
185 | |
186 |
187 |
192 | {deliveryStatus.icon}
193 |
194 | {deliveryStatus.name}
195 | |
196 | >
197 | ) : (
198 | <>>
199 | )}
200 |
201 |
202 | |
203 | {displayCertificate && (
204 |
216 | )}
217 | |
218 |
219 |
220 |
221 |
222 |
223 | >
224 | );
225 | };
226 |
227 | export default Track;
228 |
--------------------------------------------------------------------------------