Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add transfer poc #1049

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
1 change: 1 addition & 0 deletions .github/workflows/manual-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ on:
- punch
- scopechangerequest
- workorder
- transfer-poc
- '*'
permissions:
actions: read
Expand Down
19 changes: 19 additions & 0 deletions apps/transfer/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"name": "transfer-poc",
"displayName": "Transfer",
"version": "0.0.1",
"main": "/src/main.tsx",
"private": true,
"type": "module",
"scripts": {
"dev": "fusion-framework-cli app dev",
"dev:local": "fusion-framework-cli app dev",
"build": "tsc -b -f",
"pr:deploy": "tsx ../../github-action/src/releasePr.ts release",
"fprd:deploy": "tsx ../../github-action/src/releaseMain.ts release"
},
"dependencies": {
"@cc-components/shared": "workspace:^"
}
}

16 changes: 16 additions & 0 deletions apps/transfer/src/Accordion.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Accordion } from '@equinor/eds-core-react'

export type AccordionSectionProps = {
header: string;
description: string;
}
export const AccordionSection = ({ header, description }: AccordionSectionProps) => (
<Accordion.Item>
<Accordion.Header>
<Accordion.HeaderTitle>
{header}
</Accordion.HeaderTitle>
</Accordion.Header>
<Accordion.Panel>{description}</Accordion.Panel>
</Accordion.Item>
)
33 changes: 33 additions & 0 deletions apps/transfer/src/MaintenanceHistory.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import React from "react"
import { StateProps } from "./main"
import { Accordion, Button, Icon, Typography } from "@equinor/eds-core-react"
import { AccordionSection } from "./Accordion"
import { tokens } from "@equinor/eds-tokens"

type MaintenanceHistoryProps = {
} & StateProps

export function MaintenanceHistory(props: MaintenanceHistoryProps) {

return (
<div style={{ height: "100%", width: "100%", border: "2px solid grey", display: "flex", alignItems: "center", flexDirection: "column", justifyContent: "space-between" }}>
<Typography variant="h1_bold"><>{props.isCompleted && <Icon color={tokens.colors.interactive.primary__resting.hex} name="check_circle_outlined" />}</>Maintenance History</Typography>
<Accordion>
<AccordionSection header='ATEX inspection dates' description='ATEX inspection dates on a tag indicate the schedule for mandatory safety checks of equipment used in explosive atmospheres. These dates ensure compliance with the ATEX directive by verifying that the equipment maintains its integrity and protective features. Regular adherence to these dates is essential for the prevention of accidents in hazardous zones.' />
<AccordionSection header='Insulation Resistance (IR) test results' description='Insulation Resistance (IR) test results on a tag provide a snapshot of the electrical insulations integrity between conductive parts. These values, measured in megohms, indicate the effectiveness of the insulation in preventing leakage currents and potential equipment failures. Regular IR testing is crucial for predictive maintenance and ensuring electrical safety standards are met.' />
<AccordionSection header='Resistance Test results' description='Resistance Test results on a tag reflect the measured electrical resistance of components, such as grounding connections or continuity paths, ensuring they meet specified parameters. These results are critical for verifying that the electrical system can safely conduct current and maintain proper function. Regular testing helps to identify potential issues before they lead to equipment malfunction or safety hazards.' />
<AccordionSection header='Pressure safety valve calibration' description='Pressure Safety Valve (PSV) calibration results on a tag confirm the valves set pressure and blowdown characteristics, ensuring it operates correctly to prevent overpressure conditions. These calibration records are vital for maintaining system safety and compliance with regulatory standards. Regular checks and recalibration are necessary to guarantee the valves reliability and protective performance.' />
<AccordionSection header='SIF/SIL shutdown open/close times' description='SIF/SIL shutdown open/close times on a tag document the response times for Safety Instrumented Functions/Systems to reach a safe state, either by opening or closing. These timings are critical for validating that the system meets the Safety Integrity Level requirements, ensuring rapid protective actions during hazardous events. Regular verification of these times is essential for maintaining operational safety and system effectiveness.' />
</Accordion>
<Typography style={{margin: "10px"}} variant="body_long">
Please confirm that the maintenance history for the tags has been handed over to operations.
</Typography>
<Button disabled={!props.isActive} onClick={() => {
props.next()
}}>Continue</Button>
</div>

)
}


111 changes: 111 additions & 0 deletions apps/transfer/src/Scoping.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { ClientGrid } from '@equinor/workspace-ag-grid';
import React, { useEffect, useState } from 'react';
import { Autocomplete, Button, CircularProgress, Icon, Typography } from '@equinor/eds-core-react'
import { commpkgQuery, tagQuery } from './famqueries';
import { FamCommPkg, Famtag } from './types';
import { useFamQuery } from './useFamQuery';
import { tagsDef } from './tagcolumns'
import { StateProps } from './main';
import { tokens } from '@equinor/eds-tokens';
import { useMutation } from '@tanstack/react-query';

const mccr_status_map = {
0: "OS",
1: "PA",
2: "PB",
3: "OK"
}

function useCommissioningPackages(facility: string) {
return useFamQuery<FamCommPkg[]>(["commpkgs"], commpkgQuery(facility))
}

export type ScopingProps = {
tags: Famtag[] | null;
setTags: (tags: Famtag[]) => void;
} & StateProps

export function Scoping(props: ScopingProps) {
const [commpkg, setCommPkg] = useState<FamCommPkg | null>(null)

const { isLoading: tagsLoading, data: tagsData } = useFamQuery<Famtag[]>(["tags", commpkg?.commissioningPackageNo ?? ""], tagQuery(commpkg?.commissioningPackageNo ?? "", props.facility), !!commpkg)
const tags = tagsData?.map(s => ({ ...s, mccrStatus: mccr_status_map[s.worstStatus] })) as Famtag[]
const [reports, setReports] = useState<string[]>([])


const { isPending, mutateAsync } = useMutation({
mutationFn: async () => {
const promise = Promise.all([
new Promise((res) => setTimeout(() => {
setReports(s => [...s, "MC21 - Commissioning package for certificate"]);
res("")
}, 500)),
new Promise((res) => setTimeout(() => {
setReports(s => [...s, "DCP01 - Dynamic Commissioning Procedure"]);
res("")
}, 1000)),
new Promise((res) => setTimeout(() => {
setReports(s => [...s, "MC22 - CPCL/RL content by comm.pkg"]);
res("")
}, 1500)),
new Promise((res) => setTimeout(() => {
setReports(s => [...s, "MC84 - Punchlist report for certificate"]);
res("")
}, 2000)),

]);
return await promise
}
})

useEffect(() => {
props.setTags(tags)
}, [commpkg?.commissioningPackageNo, tagsData])

const { isLoading, data, error } = useCommissioningPackages("JCA")

const isAllTagsValid = tags?.some(s => s.worstStatus < 2)

return (
<div style={{ height: "100%", border: "2px solid grey", alignItems: "center", display: "flex", flexDirection: "column", justifyContent: "space-between" }}>
<Typography variant='h1_bold'><>{props.isCompleted && <Icon color={tokens.colors.interactive.primary__resting.hex} name="check_circle_outlined" />}</>Scoping</Typography>
<div style={{ width: "200px", display: "flex", alignItems: "center" }}>
<Autocomplete disabled={!props.isActive} autoFocus onOptionsChange={(a) => {
setCommPkg(a.selectedItems[0] ?? null)
setReports([])
mutateAsync()
}} optionLabel={s => s.commissioningPackageNo} selectedOptions={[]} multiple={false} options={data ?? []} label={"Search commpkg for transfer"} />
{isLoading && (<CircularProgress size={16} />)}
</div>
<Typography style={{ display: "flex", alignItems: "center" }} variant='h3'>Tags {tagsLoading && (<CircularProgress size={16} />)}</Typography>
<div style={{ width: "500px" }}>
<ClientGrid height={250} rowData={tags ?? []} colDefs={tagsDef} />
</div>

<>{commpkg && (
<>
<Typography variant='h3'>Reports</Typography>
{isPending && (<div><CircularProgress size={16} /><Typography>Generating reports....</Typography></div>)}
<Reports reports={reports} />
</>
)}</>

<Button disabled={isPending || !tags || !props.isActive} onClick={() => {
props.next()
}}>Initiate RFOC certificate</Button>

</div>

)
}

type ReportsProps = {
reports: string[]
}
const Reports = (props: ReportsProps) => {
return (
<div>
{props.reports.map(s => <div style={{ display: "flex", alignItems: "center", whiteSpace: "nowrap", textOverflow: "ellipsis", maxWidth: "500px", overflow: "hidden" }}><Icon name="library_pdf" color={tokens.colors.interactive.primary__resting.hex} />{s}</div>)}
</div>
)
}
67 changes: 67 additions & 0 deletions apps/transfer/src/Signing.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import React, { useState } from "react"
import { StateProps } from "./main"
import { Button, CircularProgress, Icon, Typography } from "@equinor/eds-core-react"
import { tokens } from "@equinor/eds-tokens"
import { useMutation } from "@tanstack/react-query"

export type SigningProps = {
} & StateProps


const signers = [
"Ola Nordmann",
"Jørn Olafsen",
"Børje Larsen",
"Cathrine Iversen",
]

export function Signing(props: SigningProps) {
const [signIndex, setsignIndex] = useState(0)
const [signPending, setSignPending] = useState(false);

const initiateSign = async () => {
setSignPending(true)
await new Promise((res) => {
setTimeout(() => {
setsignIndex(s => s + 1)
res(false)
}, 500)
})
setSignPending(false)
}

const { isPending, isSuccess, mutateAsync } = useMutation({
mutationFn: async () => {
return new Promise((res) => setTimeout(() => res(true), 5000))
}
})

return (
<div style={{ height: "100%", display: "flex", flexDirection: "column", alignItems: "center", border: "2px solid grey", width: "100%" }}>
<Typography variant="h1_bold"><>{props.isCompleted && <Icon color={tokens.colors.interactive.primary__resting.hex} name="check_circle_outlined" />}</>Signing</Typography>
<div style={{ height: "100%", width: "200px", alignItems: "center", display: "grid", alignContent: "center", justifyContent: "center", gridTemplateColumns: "1fr auto", gap: "5px", gridTemplateRows: "40px 40px 40px 40px" }}>
{signers.map((s, i) => <>
<Typography>{s}</Typography> {i < signIndex ? <Icon style={{width: "48px"}} name="check_circle_outlined" color={tokens.colors.interactive.primary__resting.hex} /> : <Button onClick={() => initiateSign()} disabled={!props.isActive || i > signIndex}>{(signPending && i == signIndex) ? <CircularProgress size={16} /> : "Sign"}</Button>}</>)}
</div>
{isPending && (
<div style={{ display: "flex", flexDirection: "row", gap: "5px", justifyContent: "center", alignItems: "center" }}>
<CircularProgress size={32} />
<Typography>Setting tag status to asbuilt in STID</Typography>
</div>
)}
<div style={{ display: "flex", flexDirection: "column", justifyContent: "center", alignItems: "center" }}>
{isSuccess && (
<div style={{ display: "flex", "alignItems": "center" }}>
<Icon name="check_circle_outlined" color={tokens.colors.interactive.primary__resting.hex} />
<Typography>Status updated to asbuilt in STID </Typography>
</div>
)}
<Button disabled={!props.isActive || isPending || signIndex !== signers.length} onClick={async () => {
await mutateAsync()
props.next()
}}>Continue </Button>
</div>

</div>
)
}
21 changes: 21 additions & 0 deletions apps/transfer/src/famqueries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
export const tagQuery = (commpkgNo: string, facility: string) => `
SELECT tag.tagNo,min(c.tagStatus) as worstStatus
FROM Completion.CompletionTag_v3 as tag
JOIN (
SELECT
sourceIdentity,
tagId,
CASE
WHEN status='OS' THEN 0
WHEN status='PA' THEN 1
WHEN status='PB' THEN 2
ELSE 3
END as tagStatus
FROM Completion.CompletionChecklist_v2
) as c ON c.tagId = tag.sourceIdentity
WHERE tag.commIssioningPackageNo = '${commpkgNo}' and tag.facility = '${facility}'
GROUP BY tag.tagNo
`

export const commpkgQuery = (facility: string) => `SELECT * FROM Completion.CommissioningPackage_v3 where facility = '${facility}'`;

29 changes: 29 additions & 0 deletions apps/transfer/src/framework-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import {
ComponentRenderArgs,
IAppConfigurator,
} from '@equinor/fusion-framework-react-app';
import { enableContext } from '@equinor/fusion-framework-react-module-context';
import buildQuery from 'odata-query';

export const configure = async (config: IAppConfigurator, c: ComponentRenderArgs) => {
enableContext(config, async (builder) => {
builder.setContextType(['ProjectMaster', 'Facility']);
builder.setContextParameterFn(({ search, type }) => {
return buildQuery({
search,
filter: {
type: {
in: type,
},
},
});
});
});

config.configureHttpClient('cc-api', {
baseUri: "https://famapi.equinor.com",
defaultScopes: ["api://958bef40-48c3-496e-bc0b-0fe5783f196b/access_as_user"],
});

};

66 changes: 66 additions & 0 deletions apps/transfer/src/main.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { createRender } from '@cc-components/shared';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import React, { useState } from 'react';
import { Famtag } from './types';
import { configure } from './framework-config';
import { check_circle_outlined, library_pdf } from '@equinor/eds-icons';
import { Signing } from './Signing';
import { MaintenanceHistory } from './MaintenanceHistory';
import { Scoping } from './Scoping';
import { Icon, Typography } from '@equinor/eds-core-react';

Icon.add({
library_pdf,
check_circle_outlined
})

function Transfer() {
const [facility] = useState("JCA")
const [state, setState] = useState<"SCOPING" | "SIGNING" | "MAINTENANCE HISTORY" | "ARCHIVED">("SCOPING");
const [tags, setTags] = useState<Famtag[] | null>(null);

return (
<div style={{ width: "100%", height: "99%", justifyContent: "center", alignItems: "center", display: "flex" }}>
<div style={{ height: "100%", width: "100%", display: "flex", justifyContent: "center", alignItems: "center" }}>
<Scoping isCompleted={state !== "SCOPING"} isActive={state == "SCOPING"} tags={tags} setTags={setTags} next={() => setState("SIGNING")} facility={facility} />
<Signing isCompleted={state !== "SCOPING" && state !== "SIGNING"} isActive={state == "SIGNING"} next={() => setState("MAINTENANCE HISTORY")} facility={facility} />
<MaintenanceHistory isCompleted={state == "ARCHIVED"} isActive={state == "MAINTENANCE HISTORY"} next={() => setState("ARCHIVED")} facility={facility} />
</div>
</div>
)
}

export type StateProps = {
next: () => void;
isActive: boolean;
isCompleted: boolean;
facility: string;
}

type ArchivedProps = {
} & Omit<StateProps, "next">

function Archived(props: ArchivedProps) {

return (
<div style={{ height: "100%", border: "2px solid grey", display: "grid", alignContent: "center", justifyItems: "center" }}>
<Typography variant='h1_bold'>Archived</Typography>
</div>

)
}

const queryclient = new QueryClient()
const TransferApp = () => {
return (
<QueryClientProvider client={queryclient}>
<Transfer />
</QueryClientProvider>
);
};


export const render = createRender(TransferApp, configure, 'Transfer');

export default render;

Loading
Loading