@burdenoff/microfe-crewfoundry 2026.1013.1 → 2026.1029.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/crewfoundry/CrewFoundryRoot.d.ts.map +1 -1
- package/dist/crewfoundry/CrewFoundryRoot.js +160 -256
- package/dist/crewfoundry/CrewFoundryRoot.js.map +1 -1
- package/dist/crewfoundry/CrewFoundryRoutes.d.ts.map +1 -1
- package/dist/crewfoundry/CrewFoundryRoutes.js +152 -192
- package/dist/crewfoundry/CrewFoundryRoutes.js.map +1 -1
- package/dist/crewfoundry/components/InlineError.d.ts +5 -0
- package/dist/crewfoundry/components/InlineError.d.ts.map +1 -0
- package/dist/crewfoundry/components/InlineError.js +17 -0
- package/dist/crewfoundry/components/InlineError.js.map +1 -0
- package/dist/crewfoundry/components/forms/FormModal.d.ts +25 -1
- package/dist/crewfoundry/components/forms/FormModal.d.ts.map +1 -1
- package/dist/crewfoundry/components/forms/FormModal.js +151 -136
- package/dist/crewfoundry/components/forms/FormModal.js.map +1 -1
- package/dist/crewfoundry/components/marketplace/BookServiceModal.d.ts +14 -0
- package/dist/crewfoundry/components/marketplace/BookServiceModal.d.ts.map +1 -0
- package/dist/crewfoundry/components/marketplace/BookServiceModal.js +75 -0
- package/dist/crewfoundry/components/marketplace/BookServiceModal.js.map +1 -0
- package/dist/crewfoundry/components/marketplace/MarketplaceTabs.d.ts +24 -0
- package/dist/crewfoundry/components/marketplace/MarketplaceTabs.d.ts.map +1 -0
- package/dist/crewfoundry/components/marketplace/MarketplaceTabs.js +47 -0
- package/dist/crewfoundry/components/marketplace/MarketplaceTabs.js.map +1 -0
- package/dist/crewfoundry/components/marketplace/ServiceListingModal.d.ts +12 -0
- package/dist/crewfoundry/components/marketplace/ServiceListingModal.d.ts.map +1 -0
- package/dist/crewfoundry/components/marketplace/ServiceListingModal.js +123 -0
- package/dist/crewfoundry/components/marketplace/ServiceListingModal.js.map +1 -0
- package/dist/crewfoundry/components/viz/CertificateCard.js +4 -118
- package/dist/crewfoundry/components/viz/PathSteps.js +3 -68
- package/dist/crewfoundry/components/viz/lib/geometry.js +5 -13
- package/dist/crewfoundry/components/viz/lib/geometry.js.map +1 -1
- package/dist/crewfoundry/lib/analytics.d.ts +1 -6
- package/dist/crewfoundry/lib/analytics.d.ts.map +1 -1
- package/dist/crewfoundry/lib/analytics.js +3 -18
- package/dist/crewfoundry/lib/analytics.js.map +1 -1
- package/dist/crewfoundry/lib/companyFinance.d.ts +3 -7
- package/dist/crewfoundry/lib/companyFinance.d.ts.map +1 -1
- package/dist/crewfoundry/lib/companyFinance.js +3 -8
- package/dist/crewfoundry/lib/companyFinance.js.map +1 -1
- package/dist/crewfoundry/lib/learning.d.ts +12 -0
- package/dist/crewfoundry/lib/learning.d.ts.map +1 -0
- package/dist/crewfoundry/lib/learning.js +26 -0
- package/dist/crewfoundry/lib/learning.js.map +1 -0
- package/dist/crewfoundry/lib/marketplace.d.ts +30 -0
- package/dist/crewfoundry/lib/marketplace.d.ts.map +1 -0
- package/dist/crewfoundry/lib/marketplace.js +91 -0
- package/dist/crewfoundry/lib/marketplace.js.map +1 -0
- package/dist/crewfoundry/lib/useEnsureLoaded.d.ts +7 -0
- package/dist/crewfoundry/lib/useEnsureLoaded.d.ts.map +1 -0
- package/dist/crewfoundry/lib/useEnsureLoaded.js +25 -0
- package/dist/crewfoundry/lib/useEnsureLoaded.js.map +1 -0
- package/dist/crewfoundry/pages/AnalyticsFinancialAnalyticsPagePage.d.ts.map +1 -1
- package/dist/crewfoundry/pages/AnalyticsFinancialAnalyticsPagePage.js +12 -25
- package/dist/crewfoundry/pages/AnalyticsFinancialAnalyticsPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/AnalyticsHiringAnalyticsPagePage.d.ts.map +1 -1
- package/dist/crewfoundry/pages/AnalyticsHiringAnalyticsPagePage.js +49 -49
- package/dist/crewfoundry/pages/AnalyticsHiringAnalyticsPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/CommunicationsAnnouncementsPagePage.d.ts.map +1 -1
- package/dist/crewfoundry/pages/CommunicationsAnnouncementsPagePage.js +145 -157
- package/dist/crewfoundry/pages/CommunicationsAnnouncementsPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/CompanyClientManagementPagePage.d.ts.map +1 -1
- package/dist/crewfoundry/pages/CompanyClientManagementPagePage.js +14 -14
- package/dist/crewfoundry/pages/CompanyClientManagementPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/CompanyVendorManagementPagePage.d.ts.map +1 -1
- package/dist/crewfoundry/pages/CompanyVendorManagementPagePage.js +14 -14
- package/dist/crewfoundry/pages/CompanyVendorManagementPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/HelpApiDocumentationPagePage.d.ts.map +1 -1
- package/dist/crewfoundry/pages/HelpApiDocumentationPagePage.js +71 -33
- package/dist/crewfoundry/pages/HelpApiDocumentationPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/HiringCreateJobPagePage.d.ts.map +1 -1
- package/dist/crewfoundry/pages/HiringCreateJobPagePage.js +260 -115
- package/dist/crewfoundry/pages/HiringCreateJobPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/HiringJobDetailsPagePage.d.ts.map +1 -1
- package/dist/crewfoundry/pages/HiringJobDetailsPagePage.js +199 -166
- package/dist/crewfoundry/pages/HiringJobDetailsPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/HiringJobsPagePage.d.ts.map +1 -1
- package/dist/crewfoundry/pages/HiringJobsPagePage.js +118 -104
- package/dist/crewfoundry/pages/HiringJobsPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/LearningCourseDetailsPagePage.d.ts.map +1 -1
- package/dist/crewfoundry/pages/LearningCourseDetailsPagePage.js +247 -173
- package/dist/crewfoundry/pages/LearningCourseDetailsPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/LearningCoursesPagePage.d.ts +12 -0
- package/dist/crewfoundry/pages/LearningCoursesPagePage.d.ts.map +1 -1
- package/dist/crewfoundry/pages/LearningCoursesPagePage.js +307 -293
- package/dist/crewfoundry/pages/LearningCoursesPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/LearningEnrollmentsPagePage.d.ts +2 -0
- package/dist/crewfoundry/pages/LearningEnrollmentsPagePage.d.ts.map +1 -0
- package/dist/crewfoundry/pages/LearningEnrollmentsPagePage.js +223 -0
- package/dist/crewfoundry/pages/LearningEnrollmentsPagePage.js.map +1 -0
- package/dist/crewfoundry/pages/LearningLearningAnalyticsPagePage.d.ts +4 -0
- package/dist/crewfoundry/pages/LearningLearningAnalyticsPagePage.d.ts.map +1 -1
- package/dist/crewfoundry/pages/LearningLearningAnalyticsPagePage.js +115 -160
- package/dist/crewfoundry/pages/LearningLearningAnalyticsPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/MarketplaceBookingManagementPagePage.d.ts.map +1 -1
- package/dist/crewfoundry/pages/MarketplaceBookingManagementPagePage.js +165 -75
- package/dist/crewfoundry/pages/MarketplaceBookingManagementPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/MarketplaceEscrowPagePage.d.ts +7 -0
- package/dist/crewfoundry/pages/MarketplaceEscrowPagePage.d.ts.map +1 -0
- package/dist/crewfoundry/pages/MarketplaceEscrowPagePage.js +175 -0
- package/dist/crewfoundry/pages/MarketplaceEscrowPagePage.js.map +1 -0
- package/dist/crewfoundry/pages/MarketplaceMarketplacePagePage.d.ts.map +1 -1
- package/dist/crewfoundry/pages/MarketplaceMarketplacePagePage.js +182 -223
- package/dist/crewfoundry/pages/MarketplaceMarketplacePagePage.js.map +1 -1
- package/dist/crewfoundry/pages/MarketplaceMyServicesPagePage.d.ts.map +1 -1
- package/dist/crewfoundry/pages/MarketplaceMyServicesPagePage.js +168 -227
- package/dist/crewfoundry/pages/MarketplaceMyServicesPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/MarketplaceServiceDetailsPagePage.d.ts.map +1 -1
- package/dist/crewfoundry/pages/MarketplaceServiceDetailsPagePage.js +127 -290
- package/dist/crewfoundry/pages/MarketplaceServiceDetailsPagePage.js.map +1 -1
- package/dist/crewfoundry/store/crewfoundryStore.d.ts +139 -79
- package/dist/crewfoundry/store/crewfoundryStore.d.ts.map +1 -1
- package/dist/crewfoundry/store/crewfoundryStore.js +323 -244
- package/dist/crewfoundry/store/crewfoundryStore.js.map +1 -1
- package/dist/crewfoundry/utils/dateUtils.d.ts +52 -0
- package/dist/crewfoundry/utils/dateUtils.d.ts.map +1 -0
- package/dist/crewfoundry/utils/dateUtils.js +31 -0
- package/dist/crewfoundry/utils/dateUtils.js.map +1 -0
- package/dist/generated/global-types.d.ts +5145 -1627
- package/dist/generated/global-types.d.ts.map +1 -1
- package/dist/generated/wspace-types.d.ts +100373 -34602
- package/dist/generated/wspace-types.d.ts.map +1 -1
- package/dist/utils/pageContext.d.ts.map +1 -1
- package/dist/utils/pageContext.js +0 -1
- package/dist/utils/pageContext.js.map +1 -1
- package/package.json +1 -1
- package/dist/crewfoundry/components/viz/CertificateCard.js.map +0 -1
- package/dist/crewfoundry/components/viz/PathSteps.js.map +0 -1
- package/dist/crewfoundry/pages/CommunicationsCallsPagePage.d.ts +0 -2
- package/dist/crewfoundry/pages/CommunicationsCallsPagePage.d.ts.map +0 -1
- package/dist/crewfoundry/pages/CommunicationsCallsPagePage.js +0 -53
- package/dist/crewfoundry/pages/CommunicationsCallsPagePage.js.map +0 -1
- package/dist/crewfoundry/pages/CommunicationsMessagesPagePage.d.ts +0 -2
- package/dist/crewfoundry/pages/CommunicationsMessagesPagePage.d.ts.map +0 -1
- package/dist/crewfoundry/pages/CommunicationsMessagesPagePage.js +0 -107
- package/dist/crewfoundry/pages/CommunicationsMessagesPagePage.js.map +0 -1
- package/dist/crewfoundry/pages/CommunicationsVideoCallsPagePage.d.ts +0 -2
- package/dist/crewfoundry/pages/CommunicationsVideoCallsPagePage.d.ts.map +0 -1
- package/dist/crewfoundry/pages/CommunicationsVideoCallsPagePage.js +0 -62
- package/dist/crewfoundry/pages/CommunicationsVideoCallsPagePage.js.map +0 -1
- package/dist/crewfoundry/pages/LearningAssessmentsPagePage.d.ts +0 -2
- package/dist/crewfoundry/pages/LearningAssessmentsPagePage.d.ts.map +0 -1
- package/dist/crewfoundry/pages/LearningAssessmentsPagePage.js +0 -37
- package/dist/crewfoundry/pages/LearningAssessmentsPagePage.js.map +0 -1
- package/dist/crewfoundry/pages/LearningCertificatesPagePage.d.ts +0 -2
- package/dist/crewfoundry/pages/LearningCertificatesPagePage.d.ts.map +0 -1
- package/dist/crewfoundry/pages/LearningCertificatesPagePage.js +0 -111
- package/dist/crewfoundry/pages/LearningCertificatesPagePage.js.map +0 -1
- package/dist/crewfoundry/pages/LearningCourseLearningPagePage.d.ts +0 -2
- package/dist/crewfoundry/pages/LearningCourseLearningPagePage.d.ts.map +0 -1
- package/dist/crewfoundry/pages/LearningCourseLearningPagePage.js +0 -309
- package/dist/crewfoundry/pages/LearningCourseLearningPagePage.js.map +0 -1
- package/dist/crewfoundry/pages/LearningLearningPagePage.d.ts +0 -2
- package/dist/crewfoundry/pages/LearningLearningPagePage.d.ts.map +0 -1
- package/dist/crewfoundry/pages/LearningLearningPagePage.js +0 -200
- package/dist/crewfoundry/pages/LearningLearningPagePage.js.map +0 -1
- package/dist/crewfoundry/pages/LearningLearningPathDetailPagePage.d.ts +0 -2
- package/dist/crewfoundry/pages/LearningLearningPathDetailPagePage.d.ts.map +0 -1
- package/dist/crewfoundry/pages/LearningLearningPathDetailPagePage.js +0 -190
- package/dist/crewfoundry/pages/LearningLearningPathDetailPagePage.js.map +0 -1
- package/dist/crewfoundry/pages/LearningLearningPathsPagePage.d.ts +0 -2
- package/dist/crewfoundry/pages/LearningLearningPathsPagePage.d.ts.map +0 -1
- package/dist/crewfoundry/pages/LearningLearningPathsPagePage.js +0 -230
- package/dist/crewfoundry/pages/LearningLearningPathsPagePage.js.map +0 -1
- package/dist/crewfoundry/pages/LearningMyLearningPagePage.d.ts +0 -2
- package/dist/crewfoundry/pages/LearningMyLearningPagePage.d.ts.map +0 -1
- package/dist/crewfoundry/pages/LearningMyLearningPagePage.js +0 -293
- package/dist/crewfoundry/pages/LearningMyLearningPagePage.js.map +0 -1
- package/dist/crewfoundry/pages/MarketplaceEarningsPagePage.d.ts +0 -2
- package/dist/crewfoundry/pages/MarketplaceEarningsPagePage.d.ts.map +0 -1
- package/dist/crewfoundry/pages/MarketplaceEarningsPagePage.js +0 -163
- package/dist/crewfoundry/pages/MarketplaceEarningsPagePage.js.map +0 -1
- package/dist/crewfoundry/pages/MarketplaceFreelancerProfilePageEnhancedPage.d.ts +0 -2
- package/dist/crewfoundry/pages/MarketplaceFreelancerProfilePageEnhancedPage.d.ts.map +0 -1
- package/dist/crewfoundry/pages/MarketplaceFreelancerProfilePageEnhancedPage.js +0 -365
- package/dist/crewfoundry/pages/MarketplaceFreelancerProfilePageEnhancedPage.js.map +0 -1
- package/dist/crewfoundry/pages/MarketplaceFreelancerProfilePagePage.d.ts +0 -2
- package/dist/crewfoundry/pages/MarketplaceFreelancerProfilePagePage.d.ts.map +0 -1
- package/dist/crewfoundry/pages/MarketplaceFreelancerProfilePagePage.js +0 -224
- package/dist/crewfoundry/pages/MarketplaceFreelancerProfilePagePage.js.map +0 -1
- package/dist/crewfoundry/pages/MarketplaceMarketplaceServicesPagePage.d.ts +0 -2
- package/dist/crewfoundry/pages/MarketplaceMarketplaceServicesPagePage.d.ts.map +0 -1
- package/dist/crewfoundry/pages/MarketplaceMarketplaceServicesPagePage.js +0 -180
- package/dist/crewfoundry/pages/MarketplaceMarketplaceServicesPagePage.js.map +0 -1
- package/dist/crewfoundry/pages/MarketplaceMessagingPagePage.d.ts +0 -2
- package/dist/crewfoundry/pages/MarketplaceMessagingPagePage.d.ts.map +0 -1
- package/dist/crewfoundry/pages/MarketplaceMessagingPagePage.js +0 -38
- package/dist/crewfoundry/pages/MarketplaceMessagingPagePage.js.map +0 -1
- package/dist/crewfoundry/pages/MarketplaceProjectDetailsPagePage.d.ts +0 -2
- package/dist/crewfoundry/pages/MarketplaceProjectDetailsPagePage.d.ts.map +0 -1
- package/dist/crewfoundry/pages/MarketplaceProjectDetailsPagePage.js +0 -204
- package/dist/crewfoundry/pages/MarketplaceProjectDetailsPagePage.js.map +0 -1
- package/dist/crewfoundry/pages/PaymentsEscrowPagePage.d.ts +0 -2
- package/dist/crewfoundry/pages/PaymentsEscrowPagePage.d.ts.map +0 -1
- package/dist/crewfoundry/pages/PaymentsEscrowPagePage.js +0 -55
- package/dist/crewfoundry/pages/PaymentsEscrowPagePage.js.map +0 -1
- package/dist/crewfoundry/pages/PaymentsInvoicesPagePage.d.ts +0 -2
- package/dist/crewfoundry/pages/PaymentsInvoicesPagePage.d.ts.map +0 -1
- package/dist/crewfoundry/pages/PaymentsInvoicesPagePage.js +0 -335
- package/dist/crewfoundry/pages/PaymentsInvoicesPagePage.js.map +0 -1
- package/dist/crewfoundry/pages/PaymentsPaymentDashboardPagePage.d.ts +0 -2
- package/dist/crewfoundry/pages/PaymentsPaymentDashboardPagePage.d.ts.map +0 -1
- package/dist/crewfoundry/pages/PaymentsPaymentDashboardPagePage.js +0 -160
- package/dist/crewfoundry/pages/PaymentsPaymentDashboardPagePage.js.map +0 -1
- package/dist/crewfoundry/pages/PaymentsPaymentMethodsPagePage.d.ts +0 -2
- package/dist/crewfoundry/pages/PaymentsPaymentMethodsPagePage.d.ts.map +0 -1
- package/dist/crewfoundry/pages/PaymentsPaymentMethodsPagePage.js +0 -80
- package/dist/crewfoundry/pages/PaymentsPaymentMethodsPagePage.js.map +0 -1
- package/dist/crewfoundry/pages/PaymentsSecurityPagePage.d.ts +0 -2
- package/dist/crewfoundry/pages/PaymentsSecurityPagePage.d.ts.map +0 -1
- package/dist/crewfoundry/pages/PaymentsSecurityPagePage.js +0 -191
- package/dist/crewfoundry/pages/PaymentsSecurityPagePage.js.map +0 -1
- package/dist/crewfoundry/utils/courseProgress.d.ts +0 -33
- package/dist/crewfoundry/utils/courseProgress.d.ts.map +0 -1
- package/dist/crewfoundry/utils/courseProgress.js +0 -46
- package/dist/crewfoundry/utils/courseProgress.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FormModal.js","names":[],"sources":["../../../../src/crewfoundry/components/forms/FormModal.tsx"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react';\nimport {\n Button,\n Input,\n Label,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n Switch,\n Textarea,\n} from '@burdenoff/fe-libs/ui';\nimport { useGlobalUi } from '@burdenoff/fe-libs/shared/providers/shell';\nimport { X } from 'lucide-react';\nimport { useTr } from '../../../shared/hooks/useTr';\n\n// ── Public API ──────────────────────────────────────────────────\nexport type FormFieldType =\n | 'text'\n | 'number'\n | 'date'\n | 'email'\n | 'textarea'\n | 'select'\n | 'tags'\n | 'switch';\n\nexport type FormValue = string | number | boolean | string[];\nexport type FormValues = Record<string, FormValue>;\n\nexport interface FieldOption {\n label: string;\n value: string;\n}\n\nexport interface FieldConfig {\n name: string;\n label: string;\n type: FormFieldType;\n required?: boolean;\n placeholder?: string;\n options?: FieldOption[];\n /** Column span in the 2-col grid on md+ (defaults to 1; textarea/tags default to 2). */\n colSpan?: 1 | 2;\n}\n\nexport interface FormModalProps {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n title: string;\n description?: string;\n fields: FieldConfig[];\n initialValues?: FormValues;\n submitLabel?: string;\n onSubmit: (values: FormValues) => Promise<void>;\n}\n\n// ── Internal state helpers ──────────────────────────────────────\n// Text-like inputs are held as strings for controlled inputs; switches as\n// booleans; tags as string[]. Values are coerced back to their public shapes\n// on submit.\ntype InternalValue = string | boolean | string[];\ntype InternalValues = Record<string, InternalValue>;\n\nconst buildInitialState = (fields: FieldConfig[], initialValues?: FormValues): InternalValues => {\n const state: InternalValues = {};\n for (const field of fields) {\n const raw = initialValues?.[field.name];\n if (field.type === 'switch') {\n state[field.name] = Boolean(raw);\n } else if (field.type === 'tags') {\n if (Array.isArray(raw)) {\n state[field.name] = raw.map((item) => String(item));\n } else if (typeof raw === 'string' && raw.length > 0) {\n state[field.name] = raw\n .split(',')\n .map((item) => item.trim())\n .filter(Boolean);\n } else {\n state[field.name] = [];\n }\n } else {\n state[field.name] = raw === undefined || raw === null ? '' : String(raw);\n }\n }\n return state;\n};\n\nconst isEmptyValue = (field: FieldConfig, value: InternalValue): boolean => {\n if (field.type === 'switch') {\n return false;\n }\n if (field.type === 'tags') {\n return !Array.isArray(value) || value.length === 0;\n }\n return typeof value !== 'string' || value.trim() === '';\n};\n\nconst toOutput = (fields: FieldConfig[], state: InternalValues): FormValues => {\n const output: FormValues = {};\n for (const field of fields) {\n const value = state[field.name];\n if (field.type === 'switch') {\n output[field.name] = Boolean(value);\n } else if (field.type === 'tags') {\n output[field.name] = Array.isArray(value) ? value : [];\n } else if (field.type === 'number') {\n const raw = typeof value === 'string' ? value.trim() : '';\n output[field.name] = raw === '' ? '' : Number(raw);\n } else {\n output[field.name] = typeof value === 'string' ? value : '';\n }\n }\n return output;\n};\n\n// Native <select> styled with the same semantic tokens as fe-libs Input.\nconst SELECT_CLASS =\n 'flex h-10 w-full rounded-input border border-border-default bg-bg-surface px-3 py-2 text-sm text-text-primary transition-colors duration-200 focus-visible:border-border-focus focus-visible:outline-none focus-visible:shadow-[var(--shadow-focus)] focus-visible:ring-1 focus-visible:ring-[var(--color-focus-ring)] disabled:cursor-not-allowed disabled:opacity-50';\n\n// ── Component ───────────────────────────────────────────────────\nexport function FormModal({\n open,\n onOpenChange,\n title,\n description,\n fields,\n initialValues,\n submitLabel,\n onSubmit,\n}: FormModalProps) {\n const { showToast } = useGlobalUi();\n const tr = useTr();\n const resolvedSubmitLabel = submitLabel ?? tr('formModal.submitLabel', 'Save');\n const [values, setValues] = useState<InternalValues>(() =>\n buildInitialState(fields, initialValues)\n );\n const [tagDrafts, setTagDrafts] = useState<Record<string, string>>({});\n const [submitting, setSubmitting] = useState(false);\n\n // Keep the latest field config / initial values reachable from the reset\n // effect without making them effect dependencies (page callers rarely\n // memoize these, and depending on them would loop or clobber edits).\n const fieldsRef = useRef(fields);\n const initialValuesRef = useRef(initialValues);\n const wasOpenRef = useRef(false);\n\n // Sync the latest field config / initial values into refs after each commit\n // (never during render). Declared before the reset effect so that on a rising\n // edge of `open` the refs are refreshed first, then read by the reset below.\n useEffect(() => {\n fieldsRef.current = fields;\n initialValuesRef.current = initialValues;\n }, [fields, initialValues]);\n\n // Reset the working copy on each rising edge of `open` so edit vs create\n // contexts never leak into each other.\n useEffect(() => {\n if (open && !wasOpenRef.current) {\n setValues(buildInitialState(fieldsRef.current, initialValuesRef.current));\n setTagDrafts({});\n setSubmitting(false);\n }\n wasOpenRef.current = open;\n }, [open]);\n\n const setFieldValue = useCallback((name: string, value: InternalValue) => {\n setValues((prev) => ({ ...prev, [name]: value }));\n }, []);\n\n const commitTag = useCallback((name: string) => {\n setTagDrafts((prevDrafts) => {\n const draft = (prevDrafts[name] ?? '').trim();\n if (draft) {\n setValues((prev) => {\n const current = Array.isArray(prev[name]) ? (prev[name] as string[]) : [];\n if (current.includes(draft)) {\n return prev;\n }\n return { ...prev, [name]: [...current, draft] };\n });\n }\n return { ...prevDrafts, [name]: '' };\n });\n }, []);\n\n const removeTag = useCallback((name: string, tag: string) => {\n setValues((prev) => {\n const current = Array.isArray(prev[name]) ? (prev[name] as string[]) : [];\n return { ...prev, [name]: current.filter((item) => item !== tag) };\n });\n }, []);\n\n const handleSubmit = useCallback(\n async (event: React.FormEvent<HTMLFormElement>) => {\n event.preventDefault();\n if (submitting) {\n return;\n }\n\n // Fold any un-committed tag draft into its field before validating.\n const staged: InternalValues = { ...values };\n for (const field of fields) {\n if (field.type === 'tags') {\n const draft = (tagDrafts[field.name] ?? '').trim();\n if (draft) {\n const current = Array.isArray(staged[field.name])\n ? (staged[field.name] as string[])\n : [];\n if (!current.includes(draft)) {\n staged[field.name] = [...current, draft];\n }\n }\n }\n }\n\n const missing = fields.find(\n (field) => field.required && isEmptyValue(field, staged[field.name])\n );\n if (missing) {\n showToast(\n tr('formModal.fieldRequired', '{{label}} is required', { label: missing.label }),\n 'warning'\n );\n return;\n }\n\n setSubmitting(true);\n try {\n await onSubmit(toOutput(fields, staged));\n onOpenChange(false);\n } catch (error) {\n showToast(\n error instanceof Error\n ? error.message\n : tr('formModal.submitError', 'Something went wrong'),\n 'error'\n );\n } finally {\n setSubmitting(false);\n }\n },\n [fields, onOpenChange, onSubmit, showToast, submitting, tagDrafts, tr, values]\n );\n\n return (\n <ResponsiveDialog open={open} onOpenChange={onOpenChange}>\n <ResponsiveDialogContent className=\"max-h-[85vh] overflow-y-auto sm:max-w-[560px]\">\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>{title}</ResponsiveDialogTitle>\n {description ? (\n <ResponsiveDialogDescription>{description}</ResponsiveDialogDescription>\n ) : null}\n </ResponsiveDialogHeader>\n\n <form onSubmit={handleSubmit} className=\"space-y-4\">\n <div className=\"grid grid-cols-1 gap-4 md:grid-cols-2\">\n {fields.map((field) => {\n const fieldId = `formmodal-${field.name}`;\n const spanTwo =\n field.colSpan === 2 ||\n (field.colSpan === undefined &&\n (field.type === 'textarea' || field.type === 'tags'));\n const value = values[field.name];\n\n return (\n <div\n key={field.name}\n className={`flex flex-col gap-1.5 ${spanTwo ? 'md:col-span-2' : ''}`}\n >\n {field.type === 'switch' ? (\n <div className=\"flex items-center justify-between gap-3 rounded-input border border-border-default bg-bg-surface px-3 py-2\">\n <Label htmlFor={fieldId}>\n {field.label}\n {field.required ? <span className=\"text-status-error-fg\"> *</span> : null}\n </Label>\n <Switch\n id={fieldId}\n checked={Boolean(value)}\n onCheckedChange={(checked) => setFieldValue(field.name, checked)}\n disabled={submitting}\n />\n </div>\n ) : (\n <>\n <Label htmlFor={fieldId}>\n {field.label}\n {field.required ? <span className=\"text-status-error-fg\"> *</span> : null}\n </Label>\n\n {field.type === 'textarea' ? (\n <Textarea\n id={fieldId}\n value={typeof value === 'string' ? value : ''}\n placeholder={field.placeholder}\n disabled={submitting}\n onChange={(event) => setFieldValue(field.name, event.target.value)}\n />\n ) : field.type === 'select' ? (\n <select\n id={fieldId}\n className={SELECT_CLASS}\n value={typeof value === 'string' ? value : ''}\n disabled={submitting}\n onChange={(event) => setFieldValue(field.name, event.target.value)}\n >\n <option value=\"\" disabled>\n {field.placeholder ?? tr('formModal.selectPlaceholder', 'Select…')}\n </option>\n {(field.options ?? []).map((option) => (\n <option key={option.value} value={option.value}>\n {option.label}\n </option>\n ))}\n </select>\n ) : field.type === 'tags' ? (\n <div className=\"flex min-h-10 flex-wrap items-center gap-1.5 rounded-input border border-border-default bg-bg-surface px-2 py-1.5\">\n {(Array.isArray(value) ? value : []).map((tag) => (\n <span\n key={tag}\n className=\"inline-flex items-center gap-1 rounded-full bg-bg-sunken px-2 py-0.5 text-xs text-text-primary\"\n >\n {tag}\n <button\n type=\"button\"\n aria-label={tr('formModal.removeTag', 'Remove {{tag}}', { tag })}\n className=\"text-text-secondary hover:text-text-primary\"\n onClick={() => removeTag(field.name, tag)}\n disabled={submitting}\n >\n <X className=\"h-3 w-3\" />\n </button>\n </span>\n ))}\n <input\n id={fieldId}\n className=\"min-w-[6rem] flex-1 bg-transparent text-sm text-text-primary outline-none placeholder:text-text-placeholder\"\n value={tagDrafts[field.name] ?? ''}\n placeholder={\n field.placeholder ??\n tr('formModal.tagsPlaceholder', 'Add and press Enter')\n }\n disabled={submitting}\n onChange={(event) =>\n setTagDrafts((prev) => ({\n ...prev,\n [field.name]: event.target.value,\n }))\n }\n onKeyDown={(event) => {\n if (event.key === 'Enter' || event.key === ',') {\n event.preventDefault();\n commitTag(field.name);\n } else if (\n event.key === 'Backspace' &&\n (tagDrafts[field.name] ?? '') === ''\n ) {\n const current = Array.isArray(value) ? value : [];\n if (current.length > 0) {\n removeTag(field.name, current[current.length - 1]);\n }\n }\n }}\n onBlur={() => commitTag(field.name)}\n />\n </div>\n ) : (\n <Input\n id={fieldId}\n type={\n field.type === 'number'\n ? 'number'\n : field.type === 'date'\n ? 'date'\n : field.type === 'email'\n ? 'email'\n : 'text'\n }\n value={typeof value === 'string' ? value : ''}\n placeholder={field.placeholder}\n disabled={submitting}\n onChange={(event) => setFieldValue(field.name, event.target.value)}\n />\n )}\n </>\n )}\n </div>\n );\n })}\n </div>\n\n <ResponsiveDialogFooter>\n <Button\n type=\"button\"\n variant=\"outline\"\n onClick={() => onOpenChange(false)}\n disabled={submitting}\n >\n {tr('formModal.cancel', 'Cancel')}\n </Button>\n <Button type=\"submit\" disabled={submitting}>\n {submitting ? tr('formModal.saving', 'Saving…') : resolvedSubmitLabel}\n </Button>\n </ResponsiveDialogFooter>\n </form>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n\n// ── Optional open/close helper ──────────────────────────────────\nexport interface UseFormModalResult<TContext> {\n open: boolean;\n /** The row/context passed to openModal (e.g. the entity being edited). */\n context: TContext | null;\n openModal: (context?: TContext) => void;\n closeModal: () => void;\n onOpenChange: (open: boolean) => void;\n}\n\n/**\n * Tiny helper for pages that toggle a single FormModal and want to remember\n * which row is being edited. `context` is null for a create flow.\n */\nexport function useFormModal<TContext = void>(): UseFormModalResult<TContext> {\n const [open, setOpen] = useState(false);\n const [context, setContext] = useState<TContext | null>(null);\n\n const openModal = useCallback((next?: TContext) => {\n setContext(next ?? null);\n setOpen(true);\n }, []);\n\n const closeModal = useCallback(() => setOpen(false), []);\n\n const onOpenChange = useCallback((nextOpen: boolean) => {\n setOpen(nextOpen);\n if (!nextOpen) {\n setContext(null);\n }\n }, []);\n\n return { open, context, openModal, closeModal, onOpenChange };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"FormModal.js","names":[],"sources":["../../../../src/crewfoundry/components/forms/FormModal.tsx"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react';\nimport {\n Button,\n Input,\n Label,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n Switch,\n Textarea,\n} from '@burdenoff/fe-libs/ui';\nimport { useGlobalUi } from '@burdenoff/fe-libs/shared/providers/shell';\nimport { AlertCircle, X } from 'lucide-react';\nimport { useTr } from '../../../shared/hooks/useTr';\n\n// ── Public API ──────────────────────────────────────────────────\nexport type FormFieldType =\n | 'text'\n | 'number'\n | 'date'\n | 'email'\n | 'textarea'\n | 'select'\n | 'tags'\n | 'switch';\n\nexport type FormValue = string | number | boolean | string[];\nexport type FormValues = Record<string, FormValue>;\n\nexport interface FieldOption {\n label: string;\n value: string;\n}\n\nexport interface FieldConfig {\n name: string;\n label: string;\n type: FormFieldType;\n required?: boolean;\n /**\n * Optional dynamic-required predicate. When provided, the field's `required`\n * state at runtime is `(required || requiredWhen(values))`. Both the `*`\n * marker AND the built-in required-field check (when the form is\n * submitted) honor this rule. Existing forms that don't set it see no\n * change — purely opt-in.\n */\n requiredWhen?: (values: FormValues) => boolean;\n placeholder?: string;\n options?: FieldOption[];\n /** Column span in the 2-col grid on md+ (defaults to 1; textarea/tags default to 2). */\n colSpan?: 1 | 2;\n /** Lower bound for `date` / `number` inputs — the browser will not let the\n * user pick a value below this. Pass a YYYY-MM-DD string for date fields.\n * Ignored for other field types. */\n min?: string | number;\n /** Upper bound for `date` / `number` inputs — symmetric to `min`. */\n max?: string | number;\n}\n\nexport interface FormModalProps {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n title: string;\n description?: string;\n /** Optional inline error banner shown above the form fields. Pages should\n * pass the first validation message so the user sees feedback inside the\n * modal — useful while `useGlobalUi().showToast` has no rendered viewport\n * (shared provider bug — see BOFF-7588). */\n errorMessage?: string;\n /** Fired when this FormModal's own required-field check rejects a submit\n * (the consumer can mirror it into `errorMessage` or its own state).\n * The FormModal still toasts (and silently, while the provider bug stands)\n * and early-returns so onSubmit is not invoked — opt-in duplication. */\n onValidationError?: (message: string) => void;\n fields: FieldConfig[];\n initialValues?: FormValues;\n submitLabel?: string;\n onSubmit: (values: FormValues) => Promise<void>;\n}\n\n// ── Internal state helpers ──────────────────────────────────────\n// Text-like inputs are held as strings for controlled inputs; switches as\n// booleans; tags as string[]. Values are coerced back to their public shapes\n// on submit.\ntype InternalValue = string | boolean | string[];\ntype InternalValues = Record<string, InternalValue>;\n\nconst buildInitialState = (fields: FieldConfig[], initialValues?: FormValues): InternalValues => {\n const state: InternalValues = {};\n for (const field of fields) {\n const raw = initialValues?.[field.name];\n if (field.type === 'switch') {\n state[field.name] = Boolean(raw);\n } else if (field.type === 'tags') {\n if (Array.isArray(raw)) {\n state[field.name] = raw.map((item) => String(item));\n } else if (typeof raw === 'string' && raw.length > 0) {\n state[field.name] = raw\n .split(',')\n .map((item) => item.trim())\n .filter(Boolean);\n } else {\n state[field.name] = [];\n }\n } else {\n state[field.name] = raw === undefined || raw === null ? '' : String(raw);\n }\n }\n return state;\n};\n\nconst isEmptyValue = (field: FieldConfig, value: InternalValue): boolean => {\n if (field.type === 'switch') {\n return false;\n }\n if (field.type === 'tags') {\n return !Array.isArray(value) || value.length === 0;\n }\n return typeof value !== 'string' || value.trim() === '';\n};\n\n/** Whether this field is required at this moment, given the current values.\n * Combines the static `required` flag with the optional `requiredWhen`\n * predicate so the `*` marker and the blocking submit check stay in sync.\n */\nconst isRequiredAt = (field: FieldConfig, values: FormValues): boolean => {\n if (field.required) return true;\n if (field.requiredWhen?.(values)) return true;\n return false;\n};\n\nconst toOutput = (fields: FieldConfig[], state: InternalValues): FormValues => {\n const output: FormValues = {};\n for (const field of fields) {\n const value = state[field.name];\n if (field.type === 'switch') {\n output[field.name] = Boolean(value);\n } else if (field.type === 'tags') {\n output[field.name] = Array.isArray(value) ? value : [];\n } else if (field.type === 'number') {\n const raw = typeof value === 'string' ? value.trim() : '';\n output[field.name] = raw === '' ? '' : Number(raw);\n } else {\n output[field.name] = typeof value === 'string' ? value : '';\n }\n }\n return output;\n};\n\n// Native <select> styled with the same semantic tokens as fe-libs Input.\nconst SELECT_CLASS =\n 'flex h-10 w-full rounded-input border border-border-default bg-bg-surface px-3 py-2 text-sm text-text-primary transition-colors duration-200 focus-visible:border-border-focus focus-visible:outline-none focus-visible:shadow-[var(--shadow-focus)] focus-visible:ring-1 focus-visible:ring-[var(--color-focus-ring)] disabled:cursor-not-allowed disabled:opacity-50';\n\n// ── Component ───────────────────────────────────────────────────\nexport function FormModal({\n open,\n onOpenChange,\n title,\n description,\n errorMessage,\n onValidationError,\n fields,\n initialValues,\n submitLabel,\n onSubmit,\n}: FormModalProps) {\n const { showToast } = useGlobalUi();\n const tr = useTr();\n const resolvedSubmitLabel = submitLabel ?? tr('formModal.submitLabel', 'Save');\n const [values, setValues] = useState<InternalValues>(() =>\n buildInitialState(fields, initialValues)\n );\n const [tagDrafts, setTagDrafts] = useState<Record<string, string>>({});\n const [submitting, setSubmitting] = useState(false);\n\n // Keep the latest field config / initial values reachable from the reset\n // effect without making them effect dependencies (page callers rarely\n // memoize these, and depending on them would loop or clobber edits).\n const fieldsRef = useRef(fields);\n const initialValuesRef = useRef(initialValues);\n const wasOpenRef = useRef(false);\n\n // Sync the latest field config / initial values into refs after each commit\n // (never during render). Declared before the reset effect so that on a rising\n // edge of `open` the refs are refreshed first, then read by the reset below.\n useEffect(() => {\n fieldsRef.current = fields;\n initialValuesRef.current = initialValues;\n }, [fields, initialValues]);\n\n // Reset the working copy on each rising edge of `open` so edit vs create\n // contexts never leak into each other.\n useEffect(() => {\n if (open && !wasOpenRef.current) {\n setValues(buildInitialState(fieldsRef.current, initialValuesRef.current));\n setTagDrafts({});\n setSubmitting(false);\n }\n wasOpenRef.current = open;\n }, [open]);\n\n const setFieldValue = useCallback((name: string, value: InternalValue) => {\n setValues((prev) => ({ ...prev, [name]: value }));\n }, []);\n\n const commitTag = useCallback((name: string) => {\n setTagDrafts((prevDrafts) => {\n const draft = (prevDrafts[name] ?? '').trim();\n if (draft) {\n setValues((prev) => {\n const current = Array.isArray(prev[name]) ? (prev[name] as string[]) : [];\n if (current.includes(draft)) {\n return prev;\n }\n return { ...prev, [name]: [...current, draft] };\n });\n }\n return { ...prevDrafts, [name]: '' };\n });\n }, []);\n\n const removeTag = useCallback((name: string, tag: string) => {\n setValues((prev) => {\n const current = Array.isArray(prev[name]) ? (prev[name] as string[]) : [];\n return { ...prev, [name]: current.filter((item) => item !== tag) };\n });\n }, []);\n\n const handleSubmit = useCallback(\n async (event: React.FormEvent<HTMLFormElement>) => {\n event.preventDefault();\n if (submitting) {\n return;\n }\n\n // Fold any un-committed tag draft into its field before validating.\n const staged: InternalValues = { ...values };\n for (const field of fields) {\n if (field.type === 'tags') {\n const draft = (tagDrafts[field.name] ?? '').trim();\n if (draft) {\n const current = Array.isArray(staged[field.name])\n ? (staged[field.name] as string[])\n : [];\n if (!current.includes(draft)) {\n staged[field.name] = [...current, draft];\n }\n }\n }\n }\n\n const missing = fields.find(\n (field) => isRequiredAt(field, staged) && isEmptyValue(field, staged[field.name])\n );\n if (missing) {\n const message = tr('formModal.fieldRequired', '{{label}} is required', {\n label: missing.label,\n });\n showToast(message, 'warning');\n onValidationError?.(message);\n return;\n }\n\n setSubmitting(true);\n try {\n await onSubmit(toOutput(fields, staged));\n onOpenChange(false);\n } catch (error) {\n showToast(\n error instanceof Error\n ? error.message\n : tr('formModal.submitError', 'Something went wrong'),\n 'error'\n );\n } finally {\n setSubmitting(false);\n }\n },\n [\n fields,\n onOpenChange,\n onSubmit,\n showToast,\n submitting,\n tagDrafts,\n tr,\n values,\n onValidationError,\n ]\n );\n\n return (\n <ResponsiveDialog open={open} onOpenChange={onOpenChange}>\n <ResponsiveDialogContent className=\"max-h-[85vh] overflow-y-auto sm:max-w-[560px]\">\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>{title}</ResponsiveDialogTitle>\n {description ? (\n <ResponsiveDialogDescription>{description}</ResponsiveDialogDescription>\n ) : null}\n </ResponsiveDialogHeader>\n\n <form onSubmit={handleSubmit} className=\"space-y-4\">\n {errorMessage ? (\n <div\n role=\"alert\"\n className=\"flex items-start gap-2 rounded-input border border-status-warning-border bg-status-warning-bg px-3 py-2 text-sm text-status-warning-text\"\n >\n <AlertCircle className=\"mt-0.5 h-4 w-4 shrink-0\" aria-hidden />\n <span>{errorMessage}</span>\n </div>\n ) : null}\n <div className=\"grid grid-cols-1 gap-4 md:grid-cols-2\">\n {fields.map((field) => {\n const fieldId = `formmodal-${field.name}`;\n const spanTwo =\n field.colSpan === 2 ||\n (field.colSpan === undefined &&\n (field.type === 'textarea' || field.type === 'tags'));\n const value = values[field.name];\n\n return (\n <div\n key={field.name}\n className={`flex flex-col gap-1.5 ${spanTwo ? 'md:col-span-2' : ''}`}\n >\n {field.type === 'switch' ? (\n <div className=\"flex items-center justify-between gap-3 rounded-input border border-border-default bg-bg-surface px-3 py-2\">\n <Label htmlFor={fieldId}>\n {field.label}\n {isRequiredAt(field, values) ? (\n <span className=\"text-status-error-fg\"> *</span>\n ) : null}\n </Label>\n <Switch\n id={fieldId}\n checked={Boolean(value)}\n onCheckedChange={(checked) => setFieldValue(field.name, checked)}\n disabled={submitting}\n />\n </div>\n ) : (\n <>\n <Label htmlFor={fieldId}>\n {field.label}\n {isRequiredAt(field, values) ? (\n <span className=\"text-status-error-fg\"> *</span>\n ) : null}\n </Label>\n\n {field.type === 'textarea' ? (\n <Textarea\n id={fieldId}\n value={typeof value === 'string' ? value : ''}\n placeholder={field.placeholder}\n disabled={submitting}\n onChange={(event) => setFieldValue(field.name, event.target.value)}\n />\n ) : field.type === 'select' ? (\n <select\n id={fieldId}\n className={SELECT_CLASS}\n value={typeof value === 'string' ? value : ''}\n disabled={submitting}\n onChange={(event) => setFieldValue(field.name, event.target.value)}\n >\n <option value=\"\" disabled>\n {field.placeholder ?? tr('formModal.selectPlaceholder', 'Select…')}\n </option>\n {(field.options ?? []).map((option) => (\n <option key={option.value} value={option.value}>\n {option.label}\n </option>\n ))}\n </select>\n ) : field.type === 'tags' ? (\n <div className=\"flex min-h-10 flex-wrap items-center gap-1.5 rounded-input border border-border-default bg-bg-surface px-2 py-1.5\">\n {(Array.isArray(value) ? value : []).map((tag) => (\n <span\n key={tag}\n className=\"inline-flex items-center gap-1 rounded-full bg-bg-sunken px-2 py-0.5 text-xs text-text-primary\"\n >\n {tag}\n <button\n type=\"button\"\n aria-label={tr('formModal.removeTag', 'Remove {{tag}}', { tag })}\n className=\"text-text-secondary hover:text-text-primary\"\n onClick={() => removeTag(field.name, tag)}\n disabled={submitting}\n >\n <X className=\"h-3 w-3\" />\n </button>\n </span>\n ))}\n <input\n id={fieldId}\n className=\"min-w-[6rem] flex-1 bg-transparent text-sm text-text-primary outline-none placeholder:text-text-placeholder\"\n value={tagDrafts[field.name] ?? ''}\n placeholder={\n field.placeholder ??\n tr('formModal.tagsPlaceholder', 'Add and press Enter')\n }\n disabled={submitting}\n onChange={(event) =>\n setTagDrafts((prev) => ({\n ...prev,\n [field.name]: event.target.value,\n }))\n }\n onKeyDown={(event) => {\n if (event.key === 'Enter' || event.key === ',') {\n event.preventDefault();\n commitTag(field.name);\n } else if (\n event.key === 'Backspace' &&\n (tagDrafts[field.name] ?? '') === ''\n ) {\n const current = Array.isArray(value) ? value : [];\n if (current.length > 0) {\n removeTag(field.name, current[current.length - 1]);\n }\n }\n }}\n onBlur={() => commitTag(field.name)}\n />\n </div>\n ) : (\n <Input\n id={fieldId}\n type={\n field.type === 'number'\n ? 'number'\n : field.type === 'date'\n ? 'date'\n : field.type === 'email'\n ? 'email'\n : 'text'\n }\n min={field.min}\n max={field.max}\n value={typeof value === 'string' ? value : ''}\n placeholder={field.placeholder}\n disabled={submitting}\n onChange={(event) => setFieldValue(field.name, event.target.value)}\n />\n )}\n </>\n )}\n </div>\n );\n })}\n </div>\n\n <ResponsiveDialogFooter>\n <Button\n type=\"button\"\n variant=\"outline\"\n onClick={() => onOpenChange(false)}\n disabled={submitting}\n >\n {tr('formModal.cancel', 'Cancel')}\n </Button>\n <Button type=\"submit\" disabled={submitting}>\n {submitting ? tr('formModal.saving', 'Saving…') : resolvedSubmitLabel}\n </Button>\n </ResponsiveDialogFooter>\n </form>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n\n// ── Optional open/close helper ──────────────────────────────────\nexport interface UseFormModalResult<TContext> {\n open: boolean;\n /** The row/context passed to openModal (e.g. the entity being edited). */\n context: TContext | null;\n openModal: (context?: TContext) => void;\n closeModal: () => void;\n onOpenChange: (open: boolean) => void;\n}\n\n/**\n * Tiny helper for pages that toggle a single FormModal and want to remember\n * which row is being edited. `context` is null for a create flow.\n */\nexport function useFormModal<TContext = void>(): UseFormModalResult<TContext> {\n const [open, setOpen] = useState(false);\n const [context, setContext] = useState<TContext | null>(null);\n\n const openModal = useCallback((next?: TContext) => {\n setContext(next ?? null);\n setOpen(true);\n }, []);\n\n const closeModal = useCallback(() => setOpen(false), []);\n\n const onOpenChange = useCallback((nextOpen: boolean) => {\n setOpen(nextOpen);\n if (!nextOpen) {\n setContext(null);\n }\n }, []);\n\n return { open, context, openModal, closeModal, onOpenChange };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { CrewServiceListing } from '../../store/crewfoundryStore';
|
|
2
|
+
|
|
3
|
+
interface BookServiceModalProps {
|
|
4
|
+
listing: CrewServiceListing | null;
|
|
5
|
+
onOpenChange: (open: boolean) => void;
|
|
6
|
+
onBooked?: () => void;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Request a booking for a listing. The service snapshots the provider, rate
|
|
10
|
+
* and currency at booking time and opens a calculation-only escrow record.
|
|
11
|
+
*/
|
|
12
|
+
export declare function BookServiceModal({ listing, onOpenChange, onBooked }: BookServiceModalProps): import("react").JSX.Element;
|
|
13
|
+
export {};
|
|
14
|
+
//# sourceMappingURL=BookServiceModal.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BookServiceModal.d.ts","sourceRoot":"","sources":["../../../../src/crewfoundry/components/marketplace/BookServiceModal.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAuB,KAAK,kBAAkB,EAAE,MAAM,8BAA8B,CAAC;AAI5F,UAAU,qBAAqB;IAC7B,OAAO,EAAE,kBAAkB,GAAG,IAAI,CAAC;IACnC,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;IACtC,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;CACvB;AAUD;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,EAAE,OAAO,EAAE,YAAY,EAAE,QAAQ,EAAE,EAAE,qBAAqB,+BAyD1F"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { useCrewFoundryStore as e } from "../../store/crewfoundryStore.js";
|
|
2
|
+
import { FormModal as t } from "../forms/FormModal.js";
|
|
3
|
+
import { localTodayString as n } from "../../utils/dateUtils.js";
|
|
4
|
+
import { formatRate as r } from "../../lib/marketplace.js";
|
|
5
|
+
import { useState as i } from "react";
|
|
6
|
+
import { jsx as a } from "react/jsx-runtime";
|
|
7
|
+
//#region src/crewfoundry/components/marketplace/BookServiceModal.tsx
|
|
8
|
+
var o = (e) => {
|
|
9
|
+
let t = typeof e == "string" ? e.trim() : "";
|
|
10
|
+
if (!t) return null;
|
|
11
|
+
let n = /* @__PURE__ */ new Date(`${t}T00:00:00`);
|
|
12
|
+
return Number.isNaN(n.getTime()) ? null : n.toISOString();
|
|
13
|
+
};
|
|
14
|
+
function s({ listing: s, onOpenChange: c, onBooked: l }) {
|
|
15
|
+
let { bookServiceListing: u, initParams: d } = e(), [f, p] = i(), m = n(), h = [
|
|
16
|
+
{
|
|
17
|
+
name: "clientName",
|
|
18
|
+
label: "Booked for (name or team)",
|
|
19
|
+
type: "text",
|
|
20
|
+
required: !0
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
name: "scheduledStart",
|
|
24
|
+
label: "Start date",
|
|
25
|
+
type: "date",
|
|
26
|
+
min: m
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: "scheduledEnd",
|
|
30
|
+
label: "End date",
|
|
31
|
+
type: "date",
|
|
32
|
+
min: m
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: "notes",
|
|
36
|
+
label: "Notes for the provider",
|
|
37
|
+
type: "textarea"
|
|
38
|
+
}
|
|
39
|
+
];
|
|
40
|
+
return /* @__PURE__ */ a(t, {
|
|
41
|
+
open: s !== null,
|
|
42
|
+
onOpenChange: (e) => {
|
|
43
|
+
e || p(void 0), c(e);
|
|
44
|
+
},
|
|
45
|
+
title: s ? `Book “${s.title}”` : "Book service",
|
|
46
|
+
description: s ? `${s.providerName} · ${r(s)}. The provider confirms the request; the agreed amount is recorded in the escrow ledger (no money is moved).` : void 0,
|
|
47
|
+
fields: h,
|
|
48
|
+
initialValues: { clientName: d.actorName ?? "" },
|
|
49
|
+
submitLabel: "Request booking",
|
|
50
|
+
errorMessage: f,
|
|
51
|
+
onValidationError: p,
|
|
52
|
+
onSubmit: async (e) => {
|
|
53
|
+
p(void 0);
|
|
54
|
+
try {
|
|
55
|
+
if (!s) throw Error("Choose a service to book");
|
|
56
|
+
let t = o(e.scheduledStart), n = o(e.scheduledEnd);
|
|
57
|
+
if (t && n && n < t) throw Error("End date must be on or after the start date");
|
|
58
|
+
let r = String(e.notes ?? "").trim();
|
|
59
|
+
await u({
|
|
60
|
+
listingId: s.id,
|
|
61
|
+
clientName: String(e.clientName ?? "").trim(),
|
|
62
|
+
notes: r || null,
|
|
63
|
+
scheduledStart: t,
|
|
64
|
+
scheduledEnd: n
|
|
65
|
+
}), l?.();
|
|
66
|
+
} catch (e) {
|
|
67
|
+
throw p(e instanceof Error ? e.message : "Unable to book service"), e;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
//#endregion
|
|
73
|
+
export { s as BookServiceModal };
|
|
74
|
+
|
|
75
|
+
//# sourceMappingURL=BookServiceModal.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BookServiceModal.js","names":[],"sources":["../../../../src/crewfoundry/components/marketplace/BookServiceModal.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { FormModal, type FieldConfig, type FormValues } from '../forms/FormModal';\nimport { useCrewFoundryStore, type CrewServiceListing } from '../../store/crewfoundryStore';\nimport { formatRate } from '../../lib/marketplace';\nimport { localTodayString } from '../../utils/dateUtils';\n\ninterface BookServiceModalProps {\n listing: CrewServiceListing | null;\n onOpenChange: (open: boolean) => void;\n onBooked?: () => void;\n}\n\n/** Converts a YYYY-MM-DD date field into an ISO timestamp (local midnight). */\nconst dateToIso = (value: unknown): string | null => {\n const raw = typeof value === 'string' ? value.trim() : '';\n if (!raw) return null;\n const date = new Date(`${raw}T00:00:00`);\n return Number.isNaN(date.getTime()) ? null : date.toISOString();\n};\n\n/**\n * Request a booking for a listing. The service snapshots the provider, rate\n * and currency at booking time and opens a calculation-only escrow record.\n */\nexport function BookServiceModal({ listing, onOpenChange, onBooked }: BookServiceModalProps) {\n const { bookServiceListing, initParams } = useCrewFoundryStore();\n const [formError, setFormError] = useState<string | undefined>();\n const today = localTodayString();\n\n const fields: FieldConfig[] = [\n { name: 'clientName', label: 'Booked for (name or team)', type: 'text', required: true },\n { name: 'scheduledStart', label: 'Start date', type: 'date', min: today },\n { name: 'scheduledEnd', label: 'End date', type: 'date', min: today },\n { name: 'notes', label: 'Notes for the provider', type: 'textarea' },\n ];\n\n const handleSubmit = async (values: FormValues) => {\n setFormError(undefined);\n try {\n if (!listing) throw new Error('Choose a service to book');\n const scheduledStart = dateToIso(values.scheduledStart);\n const scheduledEnd = dateToIso(values.scheduledEnd);\n if (scheduledStart && scheduledEnd && scheduledEnd < scheduledStart) {\n throw new Error('End date must be on or after the start date');\n }\n const notes = String(values.notes ?? '').trim();\n await bookServiceListing({\n listingId: listing.id,\n clientName: String(values.clientName ?? '').trim(),\n notes: notes || null,\n scheduledStart,\n scheduledEnd,\n });\n onBooked?.();\n } catch (error) {\n setFormError(error instanceof Error ? error.message : 'Unable to book service');\n throw error;\n }\n };\n\n return (\n <FormModal\n open={listing !== null}\n onOpenChange={(open) => {\n if (!open) setFormError(undefined);\n onOpenChange(open);\n }}\n title={listing ? `Book “${listing.title}”` : 'Book service'}\n description={\n listing\n ? `${listing.providerName} · ${formatRate(listing)}. The provider confirms the request; the agreed amount is recorded in the escrow ledger (no money is moved).`\n : undefined\n }\n fields={fields}\n initialValues={{ clientName: initParams.actorName ?? '' }}\n submitLabel=\"Request booking\"\n errorMessage={formError}\n onValidationError={setFormError}\n onSubmit={handleSubmit}\n />\n );\n}\n"],"mappings":";;;;;;;AAaA,IAAM,KAAa,MAAkC;CACnD,IAAM,IAAM,OAAO,KAAU,WAAW,EAAM,MAAM,GAAG;AACvD,KAAI,CAAC,EAAK,QAAO;CACjB,IAAM,oBAAO,IAAI,KAAK,GAAG,EAAI,WAAW;AACxC,QAAO,OAAO,MAAM,EAAK,SAAS,CAAC,GAAG,OAAO,EAAK,aAAa;;AAOjE,SAAgB,EAAiB,EAAE,YAAS,iBAAc,eAAmC;CAC3F,IAAM,EAAE,uBAAoB,kBAAe,GAAqB,EAC1D,CAAC,GAAW,KAAgB,GAA8B,EAC1D,IAAQ,GAAkB,EAE1B,IAAwB;EAC5B;GAAE,MAAM;GAAc,OAAO;GAA6B,MAAM;GAAQ,UAAU;GAAM;EACxF;GAAE,MAAM;GAAkB,OAAO;GAAc,MAAM;GAAQ,KAAK;GAAO;EACzE;GAAE,MAAM;GAAgB,OAAO;GAAY,MAAM;GAAQ,KAAK;GAAO;EACrE;GAAE,MAAM;GAAS,OAAO;GAA0B,MAAM;GAAY;EACrE;AA0BD,QACE,kBAAC,GAAD;EACE,MAAM,MAAY;EAClB,eAAe,MAAS;AAEtB,GADK,KAAM,EAAa,KAAA,EAAU,EAClC,EAAa,EAAK;;EAEpB,OAAO,IAAU,SAAS,EAAQ,MAAM,KAAK;EAC7C,aACE,IACI,GAAG,EAAQ,aAAa,KAAK,EAAW,EAAQ,CAAC,gHACjD,KAAA;EAEE;EACR,eAAe,EAAE,YAAY,EAAW,aAAa,IAAI;EACzD,aAAY;EACZ,cAAc;EACd,mBAAmB;EACnB,UA1CiB,OAAO,MAAuB;AACjD,KAAa,KAAA,EAAU;AACvB,OAAI;AACF,QAAI,CAAC,EAAS,OAAU,MAAM,2BAA2B;IACzD,IAAM,IAAiB,EAAU,EAAO,eAAe,EACjD,IAAe,EAAU,EAAO,aAAa;AACnD,QAAI,KAAkB,KAAgB,IAAe,EACnD,OAAU,MAAM,8CAA8C;IAEhE,IAAM,IAAQ,OAAO,EAAO,SAAS,GAAG,CAAC,MAAM;AAQ/C,IAPA,MAAM,EAAmB;KACvB,WAAW,EAAQ;KACnB,YAAY,OAAO,EAAO,cAAc,GAAG,CAAC,MAAM;KAClD,OAAO,KAAS;KAChB;KACA;KACD,CAAC,EACF,KAAY;YACL,GAAO;AAEd,UADA,EAAa,aAAiB,QAAQ,EAAM,UAAU,yBAAyB,EACzE;;;EAuBN,CAAA"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
declare const MARKETPLACE_TABS: readonly [{
|
|
2
|
+
readonly id: "browse";
|
|
3
|
+
readonly label: "Browse";
|
|
4
|
+
readonly path: "../marketplace/marketplace-page";
|
|
5
|
+
}, {
|
|
6
|
+
readonly id: "my-services";
|
|
7
|
+
readonly label: "My Services";
|
|
8
|
+
readonly path: "../marketplace/my-services-page";
|
|
9
|
+
}, {
|
|
10
|
+
readonly id: "bookings";
|
|
11
|
+
readonly label: "Bookings";
|
|
12
|
+
readonly path: "../marketplace/booking-management-page";
|
|
13
|
+
}, {
|
|
14
|
+
readonly id: "escrow";
|
|
15
|
+
readonly label: "Escrow";
|
|
16
|
+
readonly path: "../marketplace/escrow-page";
|
|
17
|
+
}];
|
|
18
|
+
export type MarketplaceTabId = (typeof MARKETPLACE_TABS)[number]['id'];
|
|
19
|
+
/** Section switcher shared by every talent-marketplace page. */
|
|
20
|
+
export declare function MarketplaceTabs({ value }: {
|
|
21
|
+
value: MarketplaceTabId;
|
|
22
|
+
}): import("react").JSX.Element;
|
|
23
|
+
export {};
|
|
24
|
+
//# sourceMappingURL=MarketplaceTabs.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MarketplaceTabs.d.ts","sourceRoot":"","sources":["../../../../src/crewfoundry/components/marketplace/MarketplaceTabs.tsx"],"names":[],"mappings":"AAGA,QAAA,MAAM,gBAAgB;;;;;;;;;;;;;;;;EAKZ,CAAC;AAEX,MAAM,MAAM,gBAAgB,GAAG,CAAC,OAAO,gBAAgB,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC;AAEvE,gEAAgE;AAChE,wBAAgB,eAAe,CAAC,EAAE,KAAK,EAAE,EAAE;IAAE,KAAK,EAAE,gBAAgB,CAAA;CAAE,+BAcrE"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { SegmentedTabs as e } from "../viz/SegmentedTabs.js";
|
|
2
|
+
import "../viz/index.js";
|
|
3
|
+
import { useNavigate as t } from "react-router";
|
|
4
|
+
import { jsx as n } from "react/jsx-runtime";
|
|
5
|
+
//#region src/crewfoundry/components/marketplace/MarketplaceTabs.tsx
|
|
6
|
+
var r = [
|
|
7
|
+
{
|
|
8
|
+
id: "browse",
|
|
9
|
+
label: "Browse",
|
|
10
|
+
path: "../marketplace/marketplace-page"
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
id: "my-services",
|
|
14
|
+
label: "My Services",
|
|
15
|
+
path: "../marketplace/my-services-page"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
id: "bookings",
|
|
19
|
+
label: "Bookings",
|
|
20
|
+
path: "../marketplace/booking-management-page"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
id: "escrow",
|
|
24
|
+
label: "Escrow",
|
|
25
|
+
path: "../marketplace/escrow-page"
|
|
26
|
+
}
|
|
27
|
+
];
|
|
28
|
+
function i({ value: i }) {
|
|
29
|
+
let a = t();
|
|
30
|
+
return /* @__PURE__ */ n(e, {
|
|
31
|
+
"aria-label": "Marketplace sections",
|
|
32
|
+
variant: "underline",
|
|
33
|
+
value: i,
|
|
34
|
+
onChange: (e) => {
|
|
35
|
+
let t = r.find((t) => t.id === e);
|
|
36
|
+
t && e !== i && a(t.path);
|
|
37
|
+
},
|
|
38
|
+
tabs: r.map(({ id: e, label: t }) => ({
|
|
39
|
+
id: e,
|
|
40
|
+
label: t
|
|
41
|
+
}))
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
//#endregion
|
|
45
|
+
export { i as MarketplaceTabs };
|
|
46
|
+
|
|
47
|
+
//# sourceMappingURL=MarketplaceTabs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MarketplaceTabs.js","names":[],"sources":["../../../../src/crewfoundry/components/marketplace/MarketplaceTabs.tsx"],"sourcesContent":["import { useNavigate } from 'react-router';\nimport { SegmentedTabs } from '../viz';\n\nconst MARKETPLACE_TABS = [\n { id: 'browse', label: 'Browse', path: '../marketplace/marketplace-page' },\n { id: 'my-services', label: 'My Services', path: '../marketplace/my-services-page' },\n { id: 'bookings', label: 'Bookings', path: '../marketplace/booking-management-page' },\n { id: 'escrow', label: 'Escrow', path: '../marketplace/escrow-page' },\n] as const;\n\nexport type MarketplaceTabId = (typeof MARKETPLACE_TABS)[number]['id'];\n\n/** Section switcher shared by every talent-marketplace page. */\nexport function MarketplaceTabs({ value }: { value: MarketplaceTabId }) {\n const navigate = useNavigate();\n return (\n <SegmentedTabs\n aria-label=\"Marketplace sections\"\n variant=\"underline\"\n value={value}\n onChange={(id) => {\n const tab = MARKETPLACE_TABS.find((item) => item.id === id);\n if (tab && id !== value) navigate(tab.path);\n }}\n tabs={MARKETPLACE_TABS.map(({ id, label }) => ({ id, label }))}\n />\n );\n}\n"],"mappings":";;;;;AAGA,IAAM,IAAmB;CACvB;EAAE,IAAI;EAAU,OAAO;EAAU,MAAM;EAAmC;CAC1E;EAAE,IAAI;EAAe,OAAO;EAAe,MAAM;EAAmC;CACpF;EAAE,IAAI;EAAY,OAAO;EAAY,MAAM;EAA0C;CACrF;EAAE,IAAI;EAAU,OAAO;EAAU,MAAM;EAA8B;CACtE;AAKD,SAAgB,EAAgB,EAAE,YAAsC;CACtE,IAAM,IAAW,GAAa;AAC9B,QACE,kBAAC,GAAD;EACE,cAAW;EACX,SAAQ;EACD;EACP,WAAW,MAAO;GAChB,IAAM,IAAM,EAAiB,MAAM,MAAS,EAAK,OAAO,EAAG;AAC3D,GAAI,KAAO,MAAO,KAAO,EAAS,EAAI,KAAK;;EAE7C,MAAM,EAAiB,KAAK,EAAE,OAAI,gBAAa;GAAE;GAAI;GAAO,EAAE;EAC9D,CAAA"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { CrewServiceListing } from '../../store/crewfoundryStore';
|
|
2
|
+
|
|
3
|
+
interface ServiceListingModalProps {
|
|
4
|
+
open: boolean;
|
|
5
|
+
onOpenChange: (open: boolean) => void;
|
|
6
|
+
/** Listing being edited; null publishes a new one. */
|
|
7
|
+
listing: CrewServiceListing | null;
|
|
8
|
+
}
|
|
9
|
+
/** Publish or edit one of the caller's own marketplace listings. */
|
|
10
|
+
export declare function ServiceListingModal({ open, onOpenChange, listing }: ServiceListingModalProps): import("react").JSX.Element;
|
|
11
|
+
export {};
|
|
12
|
+
//# sourceMappingURL=ServiceListingModal.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ServiceListingModal.d.ts","sourceRoot":"","sources":["../../../../src/crewfoundry/components/marketplace/ServiceListingModal.tsx"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,kBAAkB,EAExB,MAAM,8BAA8B,CAAC;AAGtC,UAAU,wBAAwB;IAChC,IAAI,EAAE,OAAO,CAAC;IACd,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;IACtC,sDAAsD;IACtD,OAAO,EAAE,kBAAkB,GAAG,IAAI,CAAC;CACpC;AAyCD,oEAAoE;AACpE,wBAAgB,mBAAmB,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,EAAE,wBAAwB,+BAiE5F"}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { useCrewFoundryStore as e } from "../../store/crewfoundryStore.js";
|
|
2
|
+
import { FormModal as t } from "../forms/FormModal.js";
|
|
3
|
+
import { CURRENCY_OPTIONS as n, RATE_UNIT_OPTIONS as r } from "../../lib/marketplace.js";
|
|
4
|
+
import { useState as i } from "react";
|
|
5
|
+
import { jsx as a } from "react/jsx-runtime";
|
|
6
|
+
//#region src/crewfoundry/components/marketplace/ServiceListingModal.tsx
|
|
7
|
+
var o = (e) => [
|
|
8
|
+
{
|
|
9
|
+
name: "title",
|
|
10
|
+
label: "Service title",
|
|
11
|
+
type: "text",
|
|
12
|
+
required: !0,
|
|
13
|
+
colSpan: 2
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
name: "description",
|
|
17
|
+
label: "What you deliver",
|
|
18
|
+
type: "textarea",
|
|
19
|
+
required: !0
|
|
20
|
+
},
|
|
21
|
+
...e ? [] : [{
|
|
22
|
+
name: "providerName",
|
|
23
|
+
label: "Provider name",
|
|
24
|
+
type: "text",
|
|
25
|
+
required: !0
|
|
26
|
+
}],
|
|
27
|
+
{
|
|
28
|
+
name: "category",
|
|
29
|
+
label: "Category",
|
|
30
|
+
type: "text",
|
|
31
|
+
placeholder: "e.g. Engineering"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: "rate",
|
|
35
|
+
label: "Rate",
|
|
36
|
+
type: "number",
|
|
37
|
+
required: !0,
|
|
38
|
+
min: 0
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: "rateUnit",
|
|
42
|
+
label: "Billed",
|
|
43
|
+
type: "select",
|
|
44
|
+
required: !0,
|
|
45
|
+
options: r
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: "currency",
|
|
49
|
+
label: "Currency",
|
|
50
|
+
type: "select",
|
|
51
|
+
required: !0,
|
|
52
|
+
options: n
|
|
53
|
+
},
|
|
54
|
+
...e ? [{
|
|
55
|
+
name: "status",
|
|
56
|
+
label: "Status",
|
|
57
|
+
type: "select",
|
|
58
|
+
required: !0,
|
|
59
|
+
options: [{
|
|
60
|
+
label: "Active (bookable)",
|
|
61
|
+
value: "active"
|
|
62
|
+
}, {
|
|
63
|
+
label: "Paused",
|
|
64
|
+
value: "paused"
|
|
65
|
+
}]
|
|
66
|
+
}] : []
|
|
67
|
+
];
|
|
68
|
+
function s({ open: n, onOpenChange: r, listing: s }) {
|
|
69
|
+
let { createServiceListing: c, updateServiceListing: l, initParams: u } = e(), [d, f] = i(), p = s !== null, m = s ? {
|
|
70
|
+
title: s.title,
|
|
71
|
+
description: s.description,
|
|
72
|
+
category: s.category ?? "",
|
|
73
|
+
rate: s.rate,
|
|
74
|
+
rateUnit: s.rateUnit,
|
|
75
|
+
currency: s.currency,
|
|
76
|
+
status: s.status
|
|
77
|
+
} : {
|
|
78
|
+
providerName: u.actorName ?? "",
|
|
79
|
+
rateUnit: "hour",
|
|
80
|
+
currency: "USD"
|
|
81
|
+
};
|
|
82
|
+
return /* @__PURE__ */ a(t, {
|
|
83
|
+
open: n,
|
|
84
|
+
onOpenChange: (e) => {
|
|
85
|
+
e || f(void 0), r(e);
|
|
86
|
+
},
|
|
87
|
+
title: p ? "Edit service" : "Publish a service",
|
|
88
|
+
description: "Colleagues in this workspace can browse and book it. Bookings record the rate you set here.",
|
|
89
|
+
fields: o(p),
|
|
90
|
+
initialValues: m,
|
|
91
|
+
submitLabel: p ? "Save changes" : "Publish service",
|
|
92
|
+
errorMessage: d,
|
|
93
|
+
onValidationError: f,
|
|
94
|
+
onSubmit: async (e) => {
|
|
95
|
+
f(void 0);
|
|
96
|
+
try {
|
|
97
|
+
let t = Number(e.rate);
|
|
98
|
+
if (!Number.isFinite(t) || t < 0) throw Error("Rate must be zero or more");
|
|
99
|
+
let n = String(e.category ?? "").trim(), r = {
|
|
100
|
+
title: String(e.title ?? "").trim(),
|
|
101
|
+
description: String(e.description ?? "").trim(),
|
|
102
|
+
category: n || null,
|
|
103
|
+
rate: t,
|
|
104
|
+
rateUnit: String(e.rateUnit),
|
|
105
|
+
currency: String(e.currency)
|
|
106
|
+
};
|
|
107
|
+
s ? await l(s.id, {
|
|
108
|
+
...r,
|
|
109
|
+
status: e.status
|
|
110
|
+
}) : await c({
|
|
111
|
+
...r,
|
|
112
|
+
providerName: String(e.providerName ?? "").trim()
|
|
113
|
+
});
|
|
114
|
+
} catch (e) {
|
|
115
|
+
throw f(e instanceof Error ? e.message : "Unable to save service"), e;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
//#endregion
|
|
121
|
+
export { s as ServiceListingModal };
|
|
122
|
+
|
|
123
|
+
//# sourceMappingURL=ServiceListingModal.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ServiceListingModal.js","names":[],"sources":["../../../../src/crewfoundry/components/marketplace/ServiceListingModal.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { FormModal, type FieldConfig, type FormValues } from '../forms/FormModal';\nimport {\n useCrewFoundryStore,\n type CrewServiceListing,\n type CrewServiceListingStatus,\n} from '../../store/crewfoundryStore';\nimport { CURRENCY_OPTIONS, RATE_UNIT_OPTIONS } from '../../lib/marketplace';\n\ninterface ServiceListingModalProps {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n /** Listing being edited; null publishes a new one. */\n listing: CrewServiceListing | null;\n}\n\nconst baseFields = (editing: boolean): FieldConfig[] => [\n { name: 'title', label: 'Service title', type: 'text', required: true, colSpan: 2 },\n { name: 'description', label: 'What you deliver', type: 'textarea', required: true },\n ...(editing\n ? []\n : [\n {\n name: 'providerName',\n label: 'Provider name',\n type: 'text',\n required: true,\n } as FieldConfig,\n ]),\n { name: 'category', label: 'Category', type: 'text', placeholder: 'e.g. Engineering' },\n { name: 'rate', label: 'Rate', type: 'number', required: true, min: 0 },\n { name: 'rateUnit', label: 'Billed', type: 'select', required: true, options: RATE_UNIT_OPTIONS },\n {\n name: 'currency',\n label: 'Currency',\n type: 'select',\n required: true,\n options: CURRENCY_OPTIONS,\n },\n ...(editing\n ? [\n {\n name: 'status',\n label: 'Status',\n type: 'select',\n required: true,\n options: [\n { label: 'Active (bookable)', value: 'active' },\n { label: 'Paused', value: 'paused' },\n ],\n } as FieldConfig,\n ]\n : []),\n];\n\n/** Publish or edit one of the caller's own marketplace listings. */\nexport function ServiceListingModal({ open, onOpenChange, listing }: ServiceListingModalProps) {\n const { createServiceListing, updateServiceListing, initParams } = useCrewFoundryStore();\n const [formError, setFormError] = useState<string | undefined>();\n const editing = listing !== null;\n\n const initialValues: FormValues = listing\n ? {\n title: listing.title,\n description: listing.description,\n category: listing.category ?? '',\n rate: listing.rate,\n rateUnit: listing.rateUnit,\n currency: listing.currency,\n status: listing.status,\n }\n : { providerName: initParams.actorName ?? '', rateUnit: 'hour', currency: 'USD' };\n\n const handleSubmit = async (values: FormValues) => {\n setFormError(undefined);\n try {\n const rate = Number(values.rate);\n if (!Number.isFinite(rate) || rate < 0) throw new Error('Rate must be zero or more');\n const category = String(values.category ?? '').trim();\n const common = {\n title: String(values.title ?? '').trim(),\n description: String(values.description ?? '').trim(),\n category: category || null,\n rate,\n rateUnit: String(values.rateUnit),\n currency: String(values.currency),\n };\n if (listing) {\n await updateServiceListing(listing.id, {\n ...common,\n status: values.status as CrewServiceListingStatus,\n });\n } else {\n await createServiceListing({\n ...common,\n providerName: String(values.providerName ?? '').trim(),\n });\n }\n } catch (error) {\n setFormError(error instanceof Error ? error.message : 'Unable to save service');\n throw error;\n }\n };\n\n return (\n <FormModal\n open={open}\n onOpenChange={(next) => {\n if (!next) setFormError(undefined);\n onOpenChange(next);\n }}\n title={editing ? 'Edit service' : 'Publish a service'}\n description=\"Colleagues in this workspace can browse and book it. Bookings record the rate you set here.\"\n fields={baseFields(editing)}\n initialValues={initialValues}\n submitLabel={editing ? 'Save changes' : 'Publish service'}\n errorMessage={formError}\n onValidationError={setFormError}\n onSubmit={handleSubmit}\n />\n );\n}\n"],"mappings":";;;;;;AAgBA,IAAM,KAAc,MAAoC;CACtD;EAAE,MAAM;EAAS,OAAO;EAAiB,MAAM;EAAQ,UAAU;EAAM,SAAS;EAAG;CACnF;EAAE,MAAM;EAAe,OAAO;EAAoB,MAAM;EAAY,UAAU;EAAM;CACpF,GAAI,IACA,EAAE,GACF,CACE;EACE,MAAM;EACN,OAAO;EACP,MAAM;EACN,UAAU;EACX,CACF;CACL;EAAE,MAAM;EAAY,OAAO;EAAY,MAAM;EAAQ,aAAa;EAAoB;CACtF;EAAE,MAAM;EAAQ,OAAO;EAAQ,MAAM;EAAU,UAAU;EAAM,KAAK;EAAG;CACvE;EAAE,MAAM;EAAY,OAAO;EAAU,MAAM;EAAU,UAAU;EAAM,SAAS;EAAmB;CACjG;EACE,MAAM;EACN,OAAO;EACP,MAAM;EACN,UAAU;EACV,SAAS;EACV;CACD,GAAI,IACA,CACE;EACE,MAAM;EACN,OAAO;EACP,MAAM;EACN,UAAU;EACV,SAAS,CACP;GAAE,OAAO;GAAqB,OAAO;GAAU,EAC/C;GAAE,OAAO;GAAU,OAAO;GAAU,CACrC;EACF,CACF,GACD,EAAE;CACP;AAGD,SAAgB,EAAoB,EAAE,SAAM,iBAAc,cAAqC;CAC7F,IAAM,EAAE,yBAAsB,yBAAsB,kBAAe,GAAqB,EAClF,CAAC,GAAW,KAAgB,GAA8B,EAC1D,IAAU,MAAY,MAEtB,IAA4B,IAC9B;EACE,OAAO,EAAQ;EACf,aAAa,EAAQ;EACrB,UAAU,EAAQ,YAAY;EAC9B,MAAM,EAAQ;EACd,UAAU,EAAQ;EAClB,UAAU,EAAQ;EAClB,QAAQ,EAAQ;EACjB,GACD;EAAE,cAAc,EAAW,aAAa;EAAI,UAAU;EAAQ,UAAU;EAAO;AAiCnF,QACE,kBAAC,GAAD;EACQ;EACN,eAAe,MAAS;AAEtB,GADK,KAAM,EAAa,KAAA,EAAU,EAClC,EAAa,EAAK;;EAEpB,OAAO,IAAU,iBAAiB;EAClC,aAAY;EACZ,QAAQ,EAAW,EAAQ;EACZ;EACf,aAAa,IAAU,iBAAiB;EACxC,cAAc;EACd,mBAAmB;EACnB,UA7CiB,OAAO,MAAuB;AACjD,KAAa,KAAA,EAAU;AACvB,OAAI;IACF,IAAM,IAAO,OAAO,EAAO,KAAK;AAChC,QAAI,CAAC,OAAO,SAAS,EAAK,IAAI,IAAO,EAAG,OAAU,MAAM,4BAA4B;IACpF,IAAM,IAAW,OAAO,EAAO,YAAY,GAAG,CAAC,MAAM,EAC/C,IAAS;KACb,OAAO,OAAO,EAAO,SAAS,GAAG,CAAC,MAAM;KACxC,aAAa,OAAO,EAAO,eAAe,GAAG,CAAC,MAAM;KACpD,UAAU,KAAY;KACtB;KACA,UAAU,OAAO,EAAO,SAAS;KACjC,UAAU,OAAO,EAAO,SAAS;KAClC;AACD,IAAI,IACF,MAAM,EAAqB,EAAQ,IAAI;KACrC,GAAG;KACH,QAAQ,EAAO;KAChB,CAAC,GAEF,MAAM,EAAqB;KACzB,GAAG;KACH,cAAc,OAAO,EAAO,gBAAgB,GAAG,CAAC,MAAM;KACvD,CAAC;YAEG,GAAO;AAEd,UADA,EAAa,aAAiB,QAAQ,EAAM,UAAU,yBAAyB,EACzE;;;EAmBN,CAAA"}
|
|
@@ -1,118 +1,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import { Fragment as i, jsx as a, jsxs as o } from "react/jsx-runtime";
|
|
6
|
-
import { Button as s } from "@burdenoff/fe-libs/ui";
|
|
7
|
-
//#region src/crewfoundry/components/viz/CertificateCard.tsx
|
|
8
|
-
function c() {
|
|
9
|
-
return /* @__PURE__ */ o("svg", {
|
|
10
|
-
viewBox: "0 0 48 60",
|
|
11
|
-
width: 48,
|
|
12
|
-
height: 60,
|
|
13
|
-
"aria-hidden": "true",
|
|
14
|
-
style: {
|
|
15
|
-
color: e,
|
|
16
|
-
display: "block"
|
|
17
|
-
},
|
|
18
|
-
children: [
|
|
19
|
-
/* @__PURE__ */ a("path", {
|
|
20
|
-
d: "M18 38 L18 58 L24 52 L30 58 L30 38 Z",
|
|
21
|
-
fill: "currentColor",
|
|
22
|
-
opacity: .85
|
|
23
|
-
}),
|
|
24
|
-
/* @__PURE__ */ a("circle", {
|
|
25
|
-
cx: "24",
|
|
26
|
-
cy: "22",
|
|
27
|
-
r: "19",
|
|
28
|
-
fill: "currentColor",
|
|
29
|
-
opacity: .1
|
|
30
|
-
}),
|
|
31
|
-
/* @__PURE__ */ a("circle", {
|
|
32
|
-
cx: "24",
|
|
33
|
-
cy: "22",
|
|
34
|
-
r: "19",
|
|
35
|
-
fill: "none",
|
|
36
|
-
stroke: "currentColor",
|
|
37
|
-
strokeWidth: 2
|
|
38
|
-
}),
|
|
39
|
-
/* @__PURE__ */ a("circle", {
|
|
40
|
-
cx: "24",
|
|
41
|
-
cy: "22",
|
|
42
|
-
r: "14",
|
|
43
|
-
fill: "none",
|
|
44
|
-
stroke: "currentColor",
|
|
45
|
-
strokeWidth: 1,
|
|
46
|
-
opacity: .5
|
|
47
|
-
}),
|
|
48
|
-
/* @__PURE__ */ a("path", {
|
|
49
|
-
d: n(24, 22, 9, 3.8),
|
|
50
|
-
fill: "currentColor"
|
|
51
|
-
})
|
|
52
|
-
]
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
function l({ title: e, recipient: n, date: l, issuer: u, credentialId: d, onDownload: f, downloadLabel: p = "Download", className: m }) {
|
|
56
|
-
return /* @__PURE__ */ o("div", {
|
|
57
|
-
className: t("relative flex flex-col gap-4 rounded-[var(--radius-card)] border-2 border-border-default bg-bg-surface p-5 shadow-elevation-1", "before:pointer-events-none before:absolute before:inset-1.5 before:rounded-[calc(var(--radius-card)-4px)] before:border before:border-border-subtle before:content-[\"\"]", m),
|
|
58
|
-
children: [
|
|
59
|
-
/* @__PURE__ */ o("div", {
|
|
60
|
-
className: "flex items-start justify-between gap-3",
|
|
61
|
-
children: [/* @__PURE__ */ o("div", {
|
|
62
|
-
className: "min-w-0",
|
|
63
|
-
children: [/* @__PURE__ */ a("p", {
|
|
64
|
-
className: "text-xs font-medium uppercase tracking-wide text-text-muted",
|
|
65
|
-
children: "Certificate of Completion"
|
|
66
|
-
}), /* @__PURE__ */ a("h3", {
|
|
67
|
-
className: "mt-1 truncate text-lg font-semibold text-text-primary",
|
|
68
|
-
title: e,
|
|
69
|
-
children: e
|
|
70
|
-
})]
|
|
71
|
-
}), /* @__PURE__ */ a(c, {})]
|
|
72
|
-
}),
|
|
73
|
-
/* @__PURE__ */ o("div", {
|
|
74
|
-
className: "min-w-0",
|
|
75
|
-
children: [/* @__PURE__ */ a("p", {
|
|
76
|
-
className: "text-xs text-text-muted",
|
|
77
|
-
children: "Awarded to"
|
|
78
|
-
}), /* @__PURE__ */ a("p", {
|
|
79
|
-
className: "truncate text-base font-medium text-text-primary",
|
|
80
|
-
title: n,
|
|
81
|
-
children: n
|
|
82
|
-
})]
|
|
83
|
-
}),
|
|
84
|
-
/* @__PURE__ */ o("div", {
|
|
85
|
-
className: "flex flex-wrap items-end justify-between gap-3 border-t border-border-subtle pt-3",
|
|
86
|
-
children: [/* @__PURE__ */ o("div", {
|
|
87
|
-
className: "flex flex-col gap-0.5 text-xs text-text-muted",
|
|
88
|
-
children: [/* @__PURE__ */ o("span", { children: [
|
|
89
|
-
"Issued ",
|
|
90
|
-
/* @__PURE__ */ a("span", {
|
|
91
|
-
className: "text-text-secondary",
|
|
92
|
-
children: l
|
|
93
|
-
}),
|
|
94
|
-
u ? /* @__PURE__ */ o(i, { children: [" · ", /* @__PURE__ */ a("span", {
|
|
95
|
-
className: "text-text-secondary",
|
|
96
|
-
children: u
|
|
97
|
-
})] }) : null
|
|
98
|
-
] }), d ? /* @__PURE__ */ o("span", {
|
|
99
|
-
className: "tabular-nums",
|
|
100
|
-
children: ["ID: ", d]
|
|
101
|
-
}) : null]
|
|
102
|
-
}), f ? /* @__PURE__ */ o(s, {
|
|
103
|
-
variant: "outline",
|
|
104
|
-
size: "sm",
|
|
105
|
-
onClick: f,
|
|
106
|
-
children: [/* @__PURE__ */ a(r, {
|
|
107
|
-
"aria-hidden": "true",
|
|
108
|
-
className: "h-4 w-4"
|
|
109
|
-
}), p]
|
|
110
|
-
}) : null]
|
|
111
|
-
})
|
|
112
|
-
]
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
//#endregion
|
|
116
|
-
export { l as CertificateCard };
|
|
117
|
-
|
|
118
|
-
//# sourceMappingURL=CertificateCard.js.map
|
|
1
|
+
import "./lib/palette.js";
|
|
2
|
+
import "lucide-react";
|
|
3
|
+
import "react/jsx-runtime";
|
|
4
|
+
import "@burdenoff/fe-libs/ui";
|