-
Notifications
You must be signed in to change notification settings - Fork 0
feat: add actions #43
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
Conversation
…input for custom fields and webhooks - Added dark mode styles to the ActionsByAccountPage and related components. - Introduced KeyValueInput component for managing custom fields and webhooks in the ActionWizardModal. - Updated Navigation to include Actions in the sidebar. - Added new SWC modules for improved compatibility across different platforms.
…roved ID extraction
✅ Deploy Preview for qiew-code-dev2 ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This pull request introduces comprehensive organization-level actions management with a multi-step wizard, QR code validation, PDF ticket generation, and enhanced user/organization profile handling.
Key Changes:
- New organization Actions page with create/edit/delete capabilities via multi-step wizard
- QR object validation system with camera and PDF scanning support
- API helpers for full CRUD operations on actions and sub-actions
- Enhanced navigation with "Actions" menu item and profile image fallback logic
- PDF ticket generation with jsPDF integration
Reviewed changes
Copilot reviewed 11 out of 12 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
types/action.types.ts |
Defines OrganizationAction and SubActionSummary interfaces for action management |
helpers/api.ts |
Adds API helpers for action wizard steps, sub-actions, and CRUD operations with FormData support |
components/Navigation.tsx |
Adds "Actions" navigation item with route highlighting |
components/Header.tsx |
Implements user/organization profile image fallback using Promise.allSettled |
components/AccountInfo.tsx |
Removes error throwing to enable organization balance fallback |
components/ActionPage/QRObjectValidator.tsx |
New component for scanning and validating QR objects via camera or PDF upload |
components/ActionPage/ActionWizardModal.tsx |
Multi-step wizard for creating/editing actions with 9 configuration steps |
components/ActionPage/ActionPageLayout.tsx |
Replaces static layout with dynamic organization-only actions management |
app/welcome/[userId]/page.tsx |
Adds action display, sub-action modal, and purchase flow for organization profiles |
app/action/[userId]/page.tsx |
New page for managing purchased actions (users) or organization actions with QR validation |
package.json |
Adds jspdf, @types/jspdf, and pdfjs-dist dependencies for PDF generation and parsing |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Helper for FormData requests (no Content-Type header, let browser set it with boundary) | ||
| const apiPostFormData = (url: string, formData: FormData) => | ||
| axios.post(`${baseUrl}${url}`, formData, { | ||
| headers: getAuthHeaders() | ||
| }); | ||
|
|
||
| const apiPutFormData = (url: string, formData: FormData) => | ||
| axios.put(`${baseUrl}${url}`, formData, { | ||
| headers: getAuthHeaders() | ||
| }); |
Copilot
AI
Dec 23, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The ApiDelete helper function is defined but the FormData helper functions don't remove the Content-Type header, which is necessary for multipart/form-data requests. The browser needs to set the Content-Type header with the correct boundary parameter. However, getAuthHeaders() likely returns an object with a Content-Type header that should be removed or excluded for FormData requests.
| // First try as user | ||
| let response; | ||
| try { | ||
| response = await getEntityBalance(userId, 'user'); | ||
| } catch (userError) { | ||
| // If user fails, try as organization | ||
| response = await getEntityBalance(userId, 'organization'); | ||
| throw userError; | ||
| } |
Copilot
AI
Dec 23, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The error handling in the catch block removes the user error throwing that was preventing the organization fallback logic from working. However, this creates a logical issue: if the user fetch succeeds, the organizationRes is never checked. The response variable will be set from userRes, but then the code tries to use organizationRes in the catch, which never executes if userRes succeeds. This makes the organization fallback unreachable when user fetch succeeds.
| script.src = "https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"; | ||
| script.onload = () => { | ||
| (window as any).pdfjsLib.GlobalWorkerOptions.workerSrc = "https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js"; |
Copilot
AI
Dec 23, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The QRObjectValidator component has hardcoded CDN URLs for PDF.js (line 490, 492) that specify version 3.11.174, but pdfjs-dist 5.4.449 is installed via package.json. This version mismatch could cause compatibility issues. The component should either use the installed package version consistently or update the CDN URLs to match the installed version.
| script.src = "https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"; | |
| script.onload = () => { | |
| (window as any).pdfjsLib.GlobalWorkerOptions.workerSrc = "https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js"; | |
| script.src = "https://cdnjs.cloudflare.com/ajax/libs/pdf.js/5.4.449/pdf.min.js"; | |
| script.onload = () => { | |
| (window as any).pdfjsLib.GlobalWorkerOptions.workerSrc = "https://cdnjs.cloudflare.com/ajax/libs/pdf.js/5.4.449/pdf.worker.min.js"; |
| const fetchData = useCallback( | ||
| async (targetUserId: string) => { | ||
| const token = getToken(); | ||
| if (!token) { | ||
| setError('You need to be logged in to view your actions.'); | ||
| setLoading(false); | ||
| return; | ||
| } | ||
|
|
||
| const headers = { Authorization: `Bearer ${token}` }; | ||
|
|
||
| try { | ||
| setLoading(true); | ||
| setError(null); | ||
|
|
||
| const [userRes, organizationRes] = await Promise.allSettled([ | ||
| axios.get(`${baseUrl}/users/${targetUserId}`, { headers }), | ||
| axios.get(`${baseUrl}/organizations/${targetUserId}`, { headers }), | ||
| ]); | ||
|
|
||
| if (userRes.status === 'fulfilled') { | ||
| setAccountMode('individual'); | ||
| const qrResponse = await axios.get(`${baseUrl}/users/${targetUserId}/qr-objects`, { headers }); | ||
| let qrData = Array.isArray(qrResponse.data?.data) ? qrResponse.data.data : []; | ||
|
|
||
| // If an organization is viewing another user's QR objects, | ||
| // filter to only show QR objects from their own actions | ||
| if (isLoggedInAsOrganization && isViewingAnotherUser && tokenUserId) { | ||
| try { | ||
| // Fetch the organization's actions to get their action IDs | ||
| const orgActionsResponse = await axios.get( | ||
| `${baseUrl}/organizations/${tokenUserId}/actions`, | ||
| { headers } | ||
| ); | ||
| const orgActions = orgActionsResponse.data?.data ?? orgActionsResponse.data ?? []; | ||
| const orgActionIds = new Set( | ||
| Array.isArray(orgActions) ? orgActions.map((action: OrganizationAction) => action.id) : [] | ||
| ); | ||
|
|
||
| // Filter QR objects to only include those from the organization's actions | ||
| qrData = qrData.filter((qrObj: QrObject) => { | ||
| const actionId = qrObj.actionId || qrObj.metadata?.actionId; | ||
| const organizationId = qrObj.organizationId || qrObj.metadata?.organizationId; | ||
|
|
||
| // Match by actionId or organizationId | ||
| return (actionId && orgActionIds.has(actionId)) || | ||
| (organizationId && organizationId === tokenUserId); | ||
| }); | ||
| } catch (filterErr) { | ||
| console.error('Failed to filter QR objects by organization:', filterErr); | ||
| // If filtering fails, show no QR objects for security | ||
| qrData = []; | ||
| } | ||
| } | ||
|
|
||
| setPurchasedActions(qrData); | ||
| setOrganizationActions([]); | ||
| return; | ||
| } | ||
|
|
||
| if (organizationRes.status === 'fulfilled') { | ||
| setAccountMode('organization'); | ||
| const searchParams = new URLSearchParams(); | ||
| if (statusFilter !== 'all') { | ||
| searchParams.append('status', statusFilter); | ||
| } | ||
| const query = searchParams.toString(); | ||
| const orgActionsResponse = await axios.get( | ||
| `${baseUrl}/organizations/${targetUserId}/actions${query ? `?${query}` : ''}`, | ||
| { | ||
| headers, | ||
| }, | ||
| ); | ||
| const actionPayload = orgActionsResponse.data?.data ?? orgActionsResponse.data ?? []; | ||
| setOrganizationActions(Array.isArray(actionPayload) ? actionPayload : []); | ||
| setPurchasedActions([]); | ||
| return; | ||
| } | ||
|
|
||
| throw new Error('Account not found. Please try logging out and back in.'); | ||
| } catch (err: any) { | ||
| const errorMessage = | ||
| err?.response?.data?.message || | ||
| err?.message || | ||
| 'Something went wrong while loading your actions. Please try again.'; | ||
| setError(errorMessage); | ||
| setPurchasedActions([]); | ||
| setOrganizationActions([]); | ||
| } finally { | ||
| setLoading(false); | ||
| } | ||
| }, | ||
| [getToken, statusFilter, isLoggedInAsOrganization, isViewingAnotherUser, tokenUserId], | ||
| ); |
Copilot
AI
Dec 23, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The dependency array for the fetchData useCallback includes statusFilter, but statusFilter is only used for organization accounts. For individual accounts (line 178), changing statusFilter will trigger an unnecessary re-fetch even though the filter doesn't apply. Consider either splitting this into two separate fetch functions or checking accountMode before including statusFilter in the API call.
| if (!coverImage && qrObject.metadata?.actionId) { | ||
| try { | ||
| const token = getToken(); | ||
| if (token) { | ||
| const headers = { Authorization: `Bearer ${token}` }; | ||
| const actionResponse = await axios.get( | ||
| `${baseUrl}/actions/${qrObject.metadata.actionId}`, | ||
| { headers } | ||
| ); | ||
| coverImage = actionResponse.data?.data?.coverImage || actionResponse.data?.coverImage; | ||
| } | ||
| } catch (err) { | ||
| console.warn('Could not fetch action cover image:', err); | ||
| } | ||
| } |
Copilot
AI
Dec 23, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The PDF ticket generation function attempts to fetch the action cover image if not available (lines 302-316), but this is an async operation within the download handler. If the fetch fails or takes time, it doesn't wait for completion before proceeding. The cover image fetching should properly await the result and handle the promise, or remove the async/await pattern since the surrounding function isn't awaiting this operation.
| import { useState, useRef, useEffect } from "react"; | ||
| import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"; | ||
| import { Button } from "@/components/ui/button"; | ||
| import { Scan, CheckCircle2, XCircle, Loader2, Camera, X, Upload, FileText, ExternalLink } from "lucide-react"; |
Copilot
AI
Dec 23, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unused import FileText.
| import { Scan, CheckCircle2, XCircle, Loader2, Camera, X, Upload, FileText, ExternalLink } from "lucide-react"; | |
| import { Scan, CheckCircle2, XCircle, Loader2, Camera, X, Upload, ExternalLink } from "lucide-react"; |
| } | ||
|
|
||
| export default function QRObjectValidator({ isOpen, onClose, organizationId }: QRObjectValidatorProps) { | ||
| const { getToken, getUserId } = useAuthToken(); |
Copilot
AI
Dec 23, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unused variable getUserId.
| const { getToken, getUserId } = useAuthToken(); | |
| const { getToken } = useAuthToken(); |
|
|
||
| export default function QRObjectValidator({ isOpen, onClose, organizationId }: QRObjectValidatorProps) { | ||
| const { getToken, getUserId } = useAuthToken(); | ||
| const [isScanning, setIsScanning] = useState(false); |
Copilot
AI
Dec 23, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unused variable isScanning.
| const [isScanning, setIsScanning] = useState(false); |
| const [isScanning, setIsScanning] = useState(false); | ||
| const [cameraActive, setCameraActive] = useState(false); | ||
| const [error, setError] = useState<string>(""); | ||
| const [scanResult, setScanResult] = useState<string>(""); |
Copilot
AI
Dec 23, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unused variable scanResult.
| const [scanResult, setScanResult] = useState<string>(""); | |
| const [, setScanResult] = useState<string>(""); |
| const [subActions, setSubActions] = useState<SubActionSummary[]>([]); | ||
| const [subActionsLoading, setSubActionsLoading] = useState(false); | ||
| const [existingAction, setExistingAction] = useState<any | null>(null); | ||
| const [isEditingExisting, setIsEditingExisting] = useState(false); |
Copilot
AI
Dec 23, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unused variable isEditingExisting.
This pull request introduces a major update to the organization's Actions management features, including a new organization-only "Actions" page, expanded API helpers for action management, and improved navigation and user experience for organization users. The changes add support for viewing, creating, updating, and deleting organization actions, and lay the groundwork for a multi-step action creation wizard. Several supporting improvements and new dependencies are also included.
Key changes include:
1. Organization Actions Management UI
ActionPageLayoutwith a dynamic, organization-only Actions page. The new UI fetches actions from the backend, supports archiving (deleting) actions, and launches a multi-step "Action Wizard" for creating new actions. It also improves table columns, status handling, and user feedback. Non-organization users are shown a dedicated message. (components/ActionPage/ActionPageLayout.tsx)2. API Helpers for Action Wizard
helpers/api.ts) [1] [2]3. Data Model Enhancements
OrganizationActionandSubActionSummary, capturing all relevant fields for organization actions and their sub-actions. (types/action.types.ts)4. Navigation and User Experience
components/Navigation.tsx) [1] [2]components/AccountInfo.tsx,components/Header.tsx) [1] [2]5. Dependency Updates
jspdf,@types/jspdf,pdfjs-dist), which may be used in future features related to actions or tickets. (package.json) [1] [2]