From 45c816616f32d2dae7ad579be5893f1fad1adb7d Mon Sep 17 00:00:00 2001 From: Yinyin Liu Date: Tue, 28 Apr 2026 08:57:03 +0200 Subject: [PATCH] add substep to upload delivery confirmation document and sync in document page --- csharp/App/Backend/Controller.cs | 11 +- .../DataTypes/ChecklistStepDefinitions.cs | 7 +- csharp/App/Backend/DataTypes/Document.cs | 1 + csharp/App/Backend/Database/Db.cs | 6 + csharp/App/Backend/Database/Read.cs | 7 + .../src/components/FileUploadButton.tsx | 4 +- .../dashboards/Checklist/ChecklistStepRow.tsx | 44 ++++--- .../Checklist/InstallationChecklistTab.tsx | 1 + .../Checklist/SubtaskDocumentUpload.tsx | 122 ++++++++++++++++++ .../Installations/FlatInstallationView.tsx | 2 +- .../FlatInstallationView.tsx | 11 +- .../src/interfaces/ChecklistTypes.tsx | 4 + typescript/frontend-marios2/src/lang/de.json | 2 + typescript/frontend-marios2/src/lang/en.json | 2 + typescript/frontend-marios2/src/lang/fr.json | 2 + typescript/frontend-marios2/src/lang/it.json | 2 + 16 files changed, 202 insertions(+), 26 deletions(-) create mode 100644 typescript/frontend-marios2/src/content/dashboards/Checklist/SubtaskDocumentUpload.tsx diff --git a/csharp/App/Backend/Controller.cs b/csharp/App/Backend/Controller.cs index c6ebfbe72..8c65f89c3 100644 --- a/csharp/App/Backend/Controller.cs +++ b/csharp/App/Backend/Controller.cs @@ -2598,6 +2598,7 @@ public class Controller : ControllerBase [FromQuery] Int64? ticketId, [FromQuery] Int64? ticketCommentId, [FromQuery] Int64? installationId, + [FromQuery] Int64? checklistItemId, [FromQuery] Token authToken) { var user = Db.GetSession(authToken)?.User; @@ -2676,6 +2677,7 @@ public class Controller : ControllerBase TicketId = ticketId, TicketCommentId = ticketCommentId, InstallationId = installationId, + ChecklistItemId = checklistItemId, Scope = scope, S3Key = s3Key, OriginalName = safeFileName, @@ -2725,6 +2727,7 @@ public class Controller : ControllerBase [FromQuery] Int64? ticketId, [FromQuery] Int64? ticketCommentId, [FromQuery] Int64? installationId, + [FromQuery] Int64? checklistItemId, [FromQuery] Token authToken) { var user = Db.GetSession(authToken)?.User; @@ -2736,6 +2739,12 @@ public class Controller : ControllerBase if (ticketCommentId.HasValue) return Ok(Db.GetDocumentsForComment(ticketCommentId.Value)); + if (checklistItemId.HasValue) + { + if (user.UserType != 2) return Unauthorized(); + return Ok(Db.GetDocumentsForChecklistItem(checklistItemId.Value)); + } + if (installationId.HasValue) { // Access control: admin can list all; others need installation access @@ -2747,7 +2756,7 @@ public class Controller : ControllerBase return Ok(Db.GetDocumentsForInstallation(installationId.Value)); } - return BadRequest("Provide ticketId, ticketCommentId, or installationId."); + return BadRequest("Provide ticketId, ticketCommentId, installationId, or checklistItemId."); } [HttpDelete(nameof(DeleteDocument))] diff --git a/csharp/App/Backend/DataTypes/ChecklistStepDefinitions.cs b/csharp/App/Backend/DataTypes/ChecklistStepDefinitions.cs index 794ef0481..326205270 100644 --- a/csharp/App/Backend/DataTypes/ChecklistStepDefinitions.cs +++ b/csharp/App/Backend/DataTypes/ChecklistStepDefinitions.cs @@ -41,7 +41,12 @@ public static class ChecklistStepDefinitions {"text":"checklistStep7Sub4","checked":false} ] """), - new( 8, "Installation delivered to customer site", NoSubtasks), + new( 8, "Installation delivered to customer site", + """ + [ + {"text":"checklistStep8Sub1","checked":false} + ] + """), new( 9, "Installation connected to grid", NoSubtasks), new(10, "Hardware verified on site", NoSubtasks), new(11, "Software verified on site", NoSubtasks), diff --git a/csharp/App/Backend/DataTypes/Document.cs b/csharp/App/Backend/DataTypes/Document.cs index 798742cd9..cb266adb8 100644 --- a/csharp/App/Backend/DataTypes/Document.cs +++ b/csharp/App/Backend/DataTypes/Document.cs @@ -15,6 +15,7 @@ public class Document [Indexed] public Int64? TicketId { get; set; } [Indexed] public Int64? TicketCommentId { get; set; } [Indexed] public Int64? InstallationId { get; set; } + [Indexed] public Int64? ChecklistItemId { get; set; } public Int32 Scope { get; set; } = (Int32)DocumentScope.TicketAttachment; public String S3Key { get; set; } = ""; diff --git a/csharp/App/Backend/Database/Db.cs b/csharp/App/Backend/Database/Db.cs index 2271d074e..36ad9dfcb 100644 --- a/csharp/App/Backend/Database/Db.cs +++ b/csharp/App/Backend/Database/Db.cs @@ -136,6 +136,12 @@ public static partial class Db $"\"{oldText}\"", $"\"{key}\"", $"%\"{oldText}\"%"); } + // One-time backfill: step 8 originally had no subtasks; add the delivery-receipt subtask + // to existing rows so already-seeded installations pick up the new subtask after deploy. + Connection.Execute( + "UPDATE ChecklistItem SET Subtasks = ? WHERE StepNumber = 8 AND (Subtasks IS NULL OR Subtasks = '')", + "[{\"text\":\"checklistStep8Sub1\",\"checked\":false}]"); + //UpdateKeys(); CleanupSessions().SupressAwaitWarning(); DeleteSnapshots().SupressAwaitWarning(); diff --git a/csharp/App/Backend/Database/Read.cs b/csharp/App/Backend/Database/Read.cs index 9d1813439..3843cadbd 100644 --- a/csharp/App/Backend/Database/Read.cs +++ b/csharp/App/Backend/Database/Read.cs @@ -234,6 +234,13 @@ public static partial class Db .OrderBy(d => d.CreatedAt) .ToList(); + public static List GetDocumentsForChecklistItem(Int64 checklistItemId) + => Documents + .Where(d => d.ChecklistItemId == checklistItemId + && d.Scope == (Int32)DocumentScope.InstallationDocument) + .OrderBy(d => d.CreatedAt) + .ToList(); + // ── Checklist Queries ─────────────────────────────────────────────── public static List GetChecklistForInstallation(Int64 installationId) diff --git a/typescript/frontend-marios2/src/components/FileUploadButton.tsx b/typescript/frontend-marios2/src/components/FileUploadButton.tsx index 6886e8c52..218804b70 100644 --- a/typescript/frontend-marios2/src/components/FileUploadButton.tsx +++ b/typescript/frontend-marios2/src/components/FileUploadButton.tsx @@ -32,6 +32,7 @@ interface FileUploadButtonProps { ticketId?: number; ticketCommentId?: number; installationId?: number; + checklistItemId?: number; onUploaded?: (doc: UploadedDocument) => void; disabled?: boolean; } @@ -41,6 +42,7 @@ function FileUploadButton({ ticketId, ticketCommentId, installationId, + checklistItemId, onUploaded, disabled = false }: FileUploadButtonProps) { @@ -87,7 +89,7 @@ function FileUploadButton({ try { const res = await axiosConfig.post('/UploadDocument', formData, { - params: { scope, ticketId, ticketCommentId, installationId }, + params: { scope, ticketId, ticketCommentId, installationId, checklistItemId }, headers: { 'Content-Type': 'multipart/form-data' }, onUploadProgress: (e) => { if (e.total) setProgress(Math.round((e.loaded * 100) / e.total)); diff --git a/typescript/frontend-marios2/src/content/dashboards/Checklist/ChecklistStepRow.tsx b/typescript/frontend-marios2/src/content/dashboards/Checklist/ChecklistStepRow.tsx index 08620c719..baf585590 100644 --- a/typescript/frontend-marios2/src/content/dashboards/Checklist/ChecklistStepRow.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/Checklist/ChecklistStepRow.tsx @@ -26,14 +26,17 @@ import { ChecklistStatus, ChecklistSubtask, parseSubtasks, - serializeSubtasks + serializeSubtasks, + UPLOADABLE_SUBTASK_KEYS } from 'src/interfaces/ChecklistTypes'; import { AdminUser } from 'src/interfaces/TicketTypes'; +import SubtaskDocumentUpload from './SubtaskDocumentUpload'; type EmailIconState = 'idle' | 'loading' | 'success' | 'error'; interface Props { item: ChecklistItem; + installationId: number; adminUsers: AdminUser[]; onUpdate: ( id: number, @@ -54,7 +57,7 @@ const statusColors: Record = { [ChecklistStatus.Done]: '#2e7d32' }; -function ChecklistStepRow({ item, adminUsers, onUpdate, onNotify }: Props) { +function ChecklistStepRow({ item, installationId, adminUsers, onUpdate, onNotify }: Props) { const intl = useIntl(); const [comments, setComments] = useState(item.comments ?? ''); const [emailState, setEmailState] = useState('idle'); @@ -282,22 +285,29 @@ function ChecklistStepRow({ item, adminUsers, onUpdate, onNotify }: Props) { {subtasks.map((s, i) => ( - handleSubtaskToggle(i)} + + handleSubtaskToggle(i)} + /> + } + label={ + + {intl.formatMessage({ id: s.text, defaultMessage: s.text })} + + } + sx={{ ml: 0 }} + /> + {UPLOADABLE_SUBTASK_KEYS.has(s.text) && ( + - } - label={ - - {intl.formatMessage({ id: s.text, defaultMessage: s.text })} - - } - sx={{ ml: 0 }} - /> + )} + ))} diff --git a/typescript/frontend-marios2/src/content/dashboards/Checklist/InstallationChecklistTab.tsx b/typescript/frontend-marios2/src/content/dashboards/Checklist/InstallationChecklistTab.tsx index 8637acee7..16002f0b8 100644 --- a/typescript/frontend-marios2/src/content/dashboards/Checklist/InstallationChecklistTab.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/Checklist/InstallationChecklistTab.tsx @@ -235,6 +235,7 @@ function InstallationChecklistTab({ installationId }: Props) { ; + return ; +} + +function SubtaskDocumentUpload({ installationId, checklistItemId }: Props) { + const [docs, setDocs] = useState([]); + const [refreshKey, setRefreshKey] = useState(0); + + const fetchDocs = useCallback(() => { + axiosConfig + .get('/GetDocuments', { params: { checklistItemId } }) + .then((res) => { + if (Array.isArray(res.data)) setDocs(res.data); + }) + .catch(() => setDocs([])); + }, [checklistItemId]); + + useEffect(() => { + fetchDocs(); + }, [fetchDocs, refreshKey]); + + const handleDownload = (doc: DocumentItem) => { + axiosConfig + .get('/DownloadDocument', { + params: { id: doc.id }, + responseType: 'blob' + }) + .then((res) => { + const url = window.URL.createObjectURL(new Blob([res.data])); + const link = document.createElement('a'); + link.href = url; + link.setAttribute('download', doc.originalName); + document.body.appendChild(link); + link.click(); + link.remove(); + window.URL.revokeObjectURL(url); + }) + .catch(() => {}); + }; + + const handleUploaded = (_doc: UploadedDocument) => { + setRefreshKey((k) => k + 1); + }; + + return ( + + {docs.length > 0 && ( + + {docs.map((d) => ( + + {fileIcon(d.contentType)} + + {d.originalName} + + handleDownload(d)} + aria-label="download" + > + + + + ))} + + )} + + + + {docs.length === 0 && ( + + + + )} + + ); +} + +export default SubtaskDocumentUpload; diff --git a/typescript/frontend-marios2/src/content/dashboards/Installations/FlatInstallationView.tsx b/typescript/frontend-marios2/src/content/dashboards/Installations/FlatInstallationView.tsx index c31a40c23..d7ec91891 100644 --- a/typescript/frontend-marios2/src/content/dashboards/Installations/FlatInstallationView.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/Installations/FlatInstallationView.tsx @@ -75,7 +75,7 @@ const FlatInstallationView = (props: FlatInstallationViewProps) => { setProgressMap(map); }) .catch(() => setProgressMap(new Map())); - }, [showChecklistColumn]); + }, [showChecklistColumn, currentLocation.pathname]); const HoverableTableRow = styled(TableRow)(({ theme }) => ({ cursor: 'pointer', diff --git a/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/FlatInstallationView.tsx b/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/FlatInstallationView.tsx index 14b831484..761142b5c 100644 --- a/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/FlatInstallationView.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/FlatInstallationView.tsx @@ -44,12 +44,15 @@ const FlatInstallationView = (props: FlatInstallationViewProps) => { const showChecklistColumn = currentUser?.userType === UserType.admin && CHECKLIST_ENABLED_PRODUCTS.has(props.product ?? -1); + const isListViewPath = + currentLocation.pathname === baseRoute + 'list' || + currentLocation.pathname === baseRoute + routes.list; const [progressMap, setProgressMap] = useState>( new Map() ); useEffect(() => { - if (!showChecklistColumn) return; + if (!showChecklistColumn || !isListViewPath) return; axiosConfig .get('/GetChecklistSummary') .then((res) => { @@ -61,7 +64,7 @@ const FlatInstallationView = (props: FlatInstallationViewProps) => { setProgressMap(map); }) .catch(() => setProgressMap(new Map())); - }, [showChecklistColumn]); + }, [showChecklistColumn, isListViewPath]); const sortedInstallations = useMemo(() => { return [...props.installations].sort((a, b) => { @@ -114,9 +117,7 @@ const FlatInstallationView = (props: FlatInstallationViewProps) => { } })); - const isListView = - currentLocation.pathname === baseRoute + 'list' || - currentLocation.pathname === baseRoute + routes.list; + const isListView = isListViewPath; return ( diff --git a/typescript/frontend-marios2/src/interfaces/ChecklistTypes.tsx b/typescript/frontend-marios2/src/interfaces/ChecklistTypes.tsx index a6c10246b..61b54cceb 100644 --- a/typescript/frontend-marios2/src/interfaces/ChecklistTypes.tsx +++ b/typescript/frontend-marios2/src/interfaces/ChecklistTypes.tsx @@ -25,6 +25,10 @@ export type ChecklistItem = { export const CHECKLIST_ENABLED_PRODUCTS: ReadonlySet = new Set([2, 4, 5]); +export const UPLOADABLE_SUBTASK_KEYS: ReadonlySet = new Set([ + 'checklistStep8Sub1' +]); + export type ChecklistSummary = { installationId: number; done: number; diff --git a/typescript/frontend-marios2/src/lang/de.json b/typescript/frontend-marios2/src/lang/de.json index 9877e583e..9b25cf16e 100644 --- a/typescript/frontend-marios2/src/lang/de.json +++ b/typescript/frontend-marios2/src/lang/de.json @@ -743,6 +743,8 @@ "checklistStep7Sub2": "Produkt-ID in config.json konfiguriert", "checklistStep7Sub3": "USB-ID in config.json konfiguriert", "checklistStep7Sub4": "Datenlesung vom Wechselrichter getestet", + "checklistStep8Sub1": "Lieferschein mit Kundenunterschrift erhalten und hochgeladen", + "checklistNoAttachments": "Noch keine Datei angehängt.", "setupProgress": "Setup-Fortschritt", "checklistPhaseEmpty": "Nicht gestartet", "checklistPhasePreparation": "Vorbereitung", diff --git a/typescript/frontend-marios2/src/lang/en.json b/typescript/frontend-marios2/src/lang/en.json index b15816902..8ff092980 100644 --- a/typescript/frontend-marios2/src/lang/en.json +++ b/typescript/frontend-marios2/src/lang/en.json @@ -491,6 +491,8 @@ "checklistStep7Sub2": "Product ID configured in config.json", "checklistStep7Sub3": "USB ID configured in config.json", "checklistStep7Sub4": "Inverter data reading from inverter tested", + "checklistStep8Sub1": "Delivery receipt with customer signature received and uploaded", + "checklistNoAttachments": "No file attached yet.", "setupProgress": "Setup Progress", "checklistPhaseEmpty": "Not started", "checklistPhasePreparation": "Preparation", diff --git a/typescript/frontend-marios2/src/lang/fr.json b/typescript/frontend-marios2/src/lang/fr.json index a8e3c19d3..ede82ad71 100644 --- a/typescript/frontend-marios2/src/lang/fr.json +++ b/typescript/frontend-marios2/src/lang/fr.json @@ -743,6 +743,8 @@ "checklistStep7Sub2": "ID produit configuré dans config.json", "checklistStep7Sub3": "ID USB configuré dans config.json", "checklistStep7Sub4": "Lecture des données de l'onduleur testée", + "checklistStep8Sub1": "Bon de livraison signé par le client reçu et téléversé", + "checklistNoAttachments": "Aucun fichier joint pour le moment.", "setupProgress": "Progression installation", "checklistPhaseEmpty": "Non commencé", "checklistPhasePreparation": "Préparation", diff --git a/typescript/frontend-marios2/src/lang/it.json b/typescript/frontend-marios2/src/lang/it.json index 9f44df140..999b8e60d 100644 --- a/typescript/frontend-marios2/src/lang/it.json +++ b/typescript/frontend-marios2/src/lang/it.json @@ -743,6 +743,8 @@ "checklistStep7Sub2": "ID prodotto configurato in config.json", "checklistStep7Sub3": "ID USB configurato in config.json", "checklistStep7Sub4": "Lettura dati inverter testata", + "checklistStep8Sub1": "Bolla di consegna con firma del cliente ricevuta e caricata", + "checklistNoAttachments": "Nessun file allegato.", "setupProgress": "Avanzamento installazione", "checklistPhaseEmpty": "Non avviato", "checklistPhasePreparation": "Preparazione",