diff --git a/.github/workflows/manual-deploy.yml b/.github/workflows/manual-deploy.yml index bec5927a5..1860b4643 100644 --- a/.github/workflows/manual-deploy.yml +++ b/.github/workflows/manual-deploy.yml @@ -17,6 +17,7 @@ on: - punch - scopechangerequest - workorder + - transfer-poc - '*' permissions: actions: read diff --git a/apps/transfer/package.json b/apps/transfer/package.json new file mode 100644 index 000000000..93fe1e9af --- /dev/null +++ b/apps/transfer/package.json @@ -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:^" + } +} + diff --git a/apps/transfer/src/Accordion.tsx b/apps/transfer/src/Accordion.tsx new file mode 100644 index 000000000..c0f602940 --- /dev/null +++ b/apps/transfer/src/Accordion.tsx @@ -0,0 +1,16 @@ +import { Accordion } from '@equinor/eds-core-react' + +export type AccordionSectionProps = { + header: string; + description: string; +} +export const AccordionSection = ({ header, description }: AccordionSectionProps) => ( + + + + {header} + + + {description} + +) diff --git a/apps/transfer/src/MaintenanceHistory.tsx b/apps/transfer/src/MaintenanceHistory.tsx new file mode 100644 index 000000000..da070636f --- /dev/null +++ b/apps/transfer/src/MaintenanceHistory.tsx @@ -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 ( +
+ <>{props.isCompleted && }Maintenance History + + + + + + + + + Please confirm that the maintenance history for the tags has been handed over to operations. + + +
+ + ) +} + + diff --git a/apps/transfer/src/Scoping.tsx b/apps/transfer/src/Scoping.tsx new file mode 100644 index 000000000..51a623e64 --- /dev/null +++ b/apps/transfer/src/Scoping.tsx @@ -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(["commpkgs"], commpkgQuery(facility)) +} + +export type ScopingProps = { + tags: Famtag[] | null; + setTags: (tags: Famtag[]) => void; +} & StateProps + +export function Scoping(props: ScopingProps) { + const [commpkg, setCommPkg] = useState(null) + + const { isLoading: tagsLoading, data: tagsData } = useFamQuery(["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([]) + + + 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 ( +
+ <>{props.isCompleted && }Scoping +
+ { + setCommPkg(a.selectedItems[0] ?? null) + setReports([]) + mutateAsync() + }} optionLabel={s => s.commissioningPackageNo} selectedOptions={[]} multiple={false} options={data ?? []} label={"Search commpkg for transfer"} /> + {isLoading && ()} +
+ Tags {tagsLoading && ()} +
+ +
+ + <>{commpkg && ( + <> + Reports + {isPending && (
Generating reports....
)} + + + )} + + + +
+ + ) +} + +type ReportsProps = { + reports: string[] +} +const Reports = (props: ReportsProps) => { + return ( +
+ {props.reports.map(s =>
{s}
)} +
+ ) +} diff --git a/apps/transfer/src/Signing.tsx b/apps/transfer/src/Signing.tsx new file mode 100644 index 000000000..31426591c --- /dev/null +++ b/apps/transfer/src/Signing.tsx @@ -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 ( +
+ <>{props.isCompleted && }Signing +
+ {signers.map((s, i) => <> + {s} {i < signIndex ? : })} +
+ {isPending && ( +
+ + Setting tag status to asbuilt in STID +
+ )} +
+ {isSuccess && ( +
+ + Status updated to asbuilt in STID +
+ )} + +
+ +
+ ) +} diff --git a/apps/transfer/src/famqueries.ts b/apps/transfer/src/famqueries.ts new file mode 100644 index 000000000..c917e4f22 --- /dev/null +++ b/apps/transfer/src/famqueries.ts @@ -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}'`; + diff --git a/apps/transfer/src/framework-config.ts b/apps/transfer/src/framework-config.ts new file mode 100644 index 000000000..50c3200ef --- /dev/null +++ b/apps/transfer/src/framework-config.ts @@ -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"], + }); + +}; + diff --git a/apps/transfer/src/main.tsx b/apps/transfer/src/main.tsx new file mode 100644 index 000000000..b8869a5f9 --- /dev/null +++ b/apps/transfer/src/main.tsx @@ -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(null); + + return ( +
+
+ setState("SIGNING")} facility={facility} /> + setState("MAINTENANCE HISTORY")} facility={facility} /> + setState("ARCHIVED")} facility={facility} /> +
+
+ ) +} + +export type StateProps = { + next: () => void; + isActive: boolean; + isCompleted: boolean; + facility: string; +} + +type ArchivedProps = { +} & Omit + +function Archived(props: ArchivedProps) { + + return ( +
+ Archived +
+ + ) +} + +const queryclient = new QueryClient() +const TransferApp = () => { + return ( + + + + ); +}; + + +export const render = createRender(TransferApp, configure, 'Transfer'); + +export default render; + diff --git a/apps/transfer/src/tagcolumns.tsx b/apps/transfer/src/tagcolumns.tsx new file mode 100644 index 000000000..8f20fd42a --- /dev/null +++ b/apps/transfer/src/tagcolumns.tsx @@ -0,0 +1,26 @@ +import { ColDef, ICellRendererProps } from "@equinor/workspace-fusion/dist/lib/integrations/grid"; +import { Famtag } from "./types"; +import { BaseStatus, StatusCell, statusColorMap } from "@cc-components/shared"; + +export const tagsDef: ColDef[] = [ + { + headerName: "TagNo", + valueGetter: (s) => s.data?.tagNo + }, + { + headerName: "MCCR status", + valueGetter: (s) => s.data?.mccrStatus, + cellRenderer: (props: ICellRendererProps) => { + if (!props.value) return null; + const statusColor = statusColorMap[(props.value as BaseStatus) ?? 'OS']; + return ( + ({ + style: { backgroundColor: statusColor }, + })} + /> + ); + }, + } +] diff --git a/apps/transfer/src/types.ts b/apps/transfer/src/types.ts new file mode 100644 index 000000000..c7df95bc4 --- /dev/null +++ b/apps/transfer/src/types.ts @@ -0,0 +1,32 @@ +export interface FamCommPkg { + messageTimestamp: string + famBehaviorTime: string + sourceName: string + sourceIdentity: string + facility: string + project: string + location: string + isVoided: boolean + commissioningPackageNo: string + description: string + descriptionOfWork: string + remark: string + commissioningPhase: string + progress: any + demolition: boolean + priority1: string + priority2: string + priority3: string + identifier: string + status: string + dynamicCommissioningStatus: string + responsible: string + createdDate: string + updatedDate: string + urlId: string +} +export interface Famtag { + tagNo: string + worstStatus: 0 | 1 | 2 | 3 + mccrStatus: "OS" | "OK" | "PA" | "PB" +} diff --git a/apps/transfer/src/useFamQuery.ts b/apps/transfer/src/useFamQuery.ts new file mode 100644 index 000000000..04c4bea04 --- /dev/null +++ b/apps/transfer/src/useFamQuery.ts @@ -0,0 +1,24 @@ +import { useHttpClient } from "@equinor/fusion-framework-react/http" +import { useQuery } from "@tanstack/react-query" + +export const useFamQuery = (queryKey: string[], query: string, enabled: boolean = true) => { + const client = useHttpClient("cc-api") + return useQuery({ + enabled, + queryKey, + queryFn: async () => { + const res = await client.fetch("/v2/dynamic", { + method: "POST", + headers: { + ["content-type"]: "application/json" + }, + body: JSON.stringify({ + pagination: null, + options: null, + query: query + }) + }) + return (await res.json()).data as T + } + }) +} diff --git a/apps/transfer/tsconfig.json b/apps/transfer/tsconfig.json new file mode 100644 index 000000000..cc3d460ea --- /dev/null +++ b/apps/transfer/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "jsx": "react-jsx", + "allowJs": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "files": [], + "include": [], + "references": [] +} + diff --git a/apps/transfer/vite.config.ts b/apps/transfer/vite.config.ts new file mode 100644 index 000000000..2195c8418 --- /dev/null +++ b/apps/transfer/vite.config.ts @@ -0,0 +1,27 @@ +import { defineConfig } from 'vite'; +import EnvironmentPlugin from 'vite-plugin-environment'; +import { InjectProcessPlugin } from '../../patches/3d-patch.ts'; + +export default defineConfig({ + plugins: [ + EnvironmentPlugin({ + NODE_ENV: 'production', + }), + ], + appType: 'custom', + build: { + emptyOutDir: true, + rollupOptions: { + plugins: [InjectProcessPlugin], + output: { + inlineDynamicImports: true, + }, + }, + lib: { + entry: './src/main.tsx', + fileName: 'app-bundle', + formats: ['es'], + }, + }, +}); + diff --git a/github-action/src/utils/uploadBundle.ts b/github-action/src/utils/uploadBundle.ts index 40392a070..d95391a48 100644 --- a/github-action/src/utils/uploadBundle.ts +++ b/github-action/src/utils/uploadBundle.ts @@ -34,6 +34,7 @@ export async function uploadBundle( notice(`bundle uploaded with status code ${r.message.statusCode}`); if (r.message.statusCode !== 200) { logInfo(`Failed to upload ${appKey}, code: ${r.message.statusCode}`, 'Red'); + console.log(r.message.read().toString()) throw new Error('Bundle failed to upload, fatal error'); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 196fd9261..e8a5ac942 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -56,7 +56,7 @@ importers: version: 8.3.0(@types/react@18.3.2)(react@18.2.0) '@equinor/workspace-fusion': specifier: 9.0.8 - version: 9.0.8(@ag-grid-enterprise/core@31.2.1)(@babel/core@7.24.5)(@types/react@18.3.2)(@types/sortablejs@1.15.8)(immer@9.0.21)(react-is@18.3.1) + version: 9.0.8(@ag-grid-community/styles@31.2.0)(@ag-grid-enterprise/core@31.2.1)(@babel/core@7.24.5)(@types/react@18.3.2)(@types/sortablejs@1.15.8)(immer@9.0.21)(react-is@18.3.1) '@microsoft/applicationinsights-web': specifier: ^3.2.0 version: 3.2.0(tslib@2.6.2) @@ -272,6 +272,12 @@ importers: specifier: workspace:^ version: link:../../libs/swcrapp + apps/transfer: + dependencies: + '@cc-components/shared': + specifier: workspace:^ + version: link:../../libs/shared + apps/workorder: dependencies: '@cc-components/shared': @@ -2205,6 +2211,7 @@ packages: '@humanwhocodes/config-array@0.11.14': resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==} engines: {node: '>=10.10.0'} + deprecated: Use @eslint/config-array instead '@humanwhocodes/module-importer@1.0.1': resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} @@ -2212,6 +2219,7 @@ packages: '@humanwhocodes/object-schema@2.0.3': resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} + deprecated: Use @eslint/object-schema instead '@icons/material@0.2.4': resolution: {integrity: sha512-QPcGmICAPbGLGb6F/yNf/KzKqvFx8z5qx3D1yFqVAjoFmXK35EgyW+cJ57Te3CNsmzblwtzakLGFqHPqrfb4Tw==} @@ -10377,7 +10385,7 @@ snapshots: - '@types/sortablejs' - supports-color - '@equinor/workspace-fusion@9.0.8(@ag-grid-enterprise/core@31.2.1)(@babel/core@7.24.5)(@types/react@18.3.2)(@types/sortablejs@1.15.8)(immer@9.0.21)(react-is@18.3.1)': + '@equinor/workspace-fusion@9.0.8(@ag-grid-community/styles@31.2.0)(@ag-grid-enterprise/core@31.2.1)(@babel/core@7.24.5)(@types/react@18.3.2)(@types/sortablejs@1.15.8)(immer@9.0.21)(react-is@18.3.1)': dependencies: '@equinor/eds-core-react': 0.37.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(styled-components@6.1.8(react-dom@18.2.0(react@18.2.0))(react@18.2.0)) '@equinor/eds-icons': 0.21.0