Merge from dev to loaf-prod for DEMO #25

Merged
andika merged 45 commits from dev into loaf-prod 2026-02-02 11:12:58 +00:00
2 changed files with 201 additions and 34 deletions
Showing only changes of commit 82ef36b439 - Show all commits

View File

@@ -7,7 +7,6 @@ const settingsItems = [
{ label: 'Permissions', path: '/admin/settings/permissions', icon: Shield }, { label: 'Permissions', path: '/admin/settings/permissions', icon: Shield },
{ label: 'Theme', path: '/admin/settings/theme', icon: Palette }, { label: 'Theme', path: '/admin/settings/theme', icon: Palette },
{ label: 'Directory', path: '/admin/settings/directory', icon: BookUser }, { label: 'Directory', path: '/admin/settings/directory', icon: BookUser },
// { label: 'Registration', path: '/admin/settings/registration', icon: FileEdit }, // Commented out for future fallback
]; ];
const SettingsTabs = () => { const SettingsTabs = () => {

View File

@@ -1026,45 +1026,213 @@ const AdminRegistrationBuilder = () => {
{/* Conditional Rules Dialog */} {/* Conditional Rules Dialog */}
<Dialog open={conditionalDialogOpen} onOpenChange={setConditionalDialogOpen}> <Dialog open={conditionalDialogOpen} onOpenChange={setConditionalDialogOpen}>
<DialogContent className="max-w-2xl"> <DialogContent className="max-w-3xl">
<DialogHeader> <DialogHeader>
<DialogTitle>Conditional Rules</DialogTitle> <DialogTitle>Conditional Rules</DialogTitle>
</DialogHeader> </DialogHeader>
<div className="space-y-4 py-4 max-h-96 overflow-y-auto"> <div className="space-y-4 py-4 max-h-[60vh] overflow-y-auto">
{(schema?.conditional_rules || []).map((rule, index) => ( <p className="text-sm text-muted-foreground">
<div key={rule.id} className="p-4 border rounded-lg space-y-3"> Rules allow you to show or hide fields based on other field values. For example, show a "Scholarship Reason" field only when "Request Scholarship" is checked.
<div className="flex justify-between items-center"> </p>
<span className="font-medium">Rule {index + 1}</span>
<Button {(schema?.conditional_rules || []).map((rule, index) => {
size="icon" // Get all available fields for dropdowns
variant="ghost" const allFields = schema?.steps?.flatMap(step =>
className="h-6 w-6 text-red-500" step.sections?.flatMap(section =>
onClick={() => { section.fields?.map(field => ({
updateSchema((prev) => ({ id: field.id,
...prev, label: field.label,
conditional_rules: prev.conditional_rules.filter((r) => r.id !== rule.id), type: field.type,
})); stepTitle: step.title,
}} sectionTitle: section.title,
> })) || []
<Trash2 className="h-4 w-4" /> ) || []
</Button> ) || [];
// Filter trigger fields (checkbox, dropdown, radio work best)
const triggerFields = allFields.filter(f =>
['checkbox', 'dropdown', 'radio', 'text'].includes(f.type)
);
// Update a specific rule
const updateRule = (ruleId, updates) => {
updateSchema((prev) => ({
...prev,
conditional_rules: prev.conditional_rules.map((r) =>
r.id === ruleId ? { ...r, ...updates } : r
),
}));
};
// Get the trigger field's type to determine value input
const triggerFieldData = allFields.find(f => f.id === rule.trigger_field);
return (
<div key={rule.id} className="p-4 border rounded-lg space-y-4 bg-gray-50">
<div className="flex justify-between items-center">
<span className="font-medium text-sm">Rule {index + 1}</span>
<Button
size="icon"
variant="ghost"
className="h-6 w-6 text-red-500 hover:text-red-700"
onClick={() => {
updateSchema((prev) => ({
...prev,
conditional_rules: prev.conditional_rules.filter((r) => r.id !== rule.id),
}));
}}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
{/* Trigger Field Selection */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
<div>
<Label className="text-xs">When this field...</Label>
<Select
value={rule.trigger_field || ''}
onValueChange={(value) => updateRule(rule.id, { trigger_field: value })}
>
<SelectTrigger className="mt-1">
<SelectValue placeholder="Select field" />
</SelectTrigger>
<SelectContent>
{triggerFields.map((field) => (
<SelectItem key={field.id} value={field.id}>
{field.label}
<span className="text-xs text-muted-foreground ml-1">
({field.type})
</span>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label className="text-xs">Operator</Label>
<Select
value={rule.trigger_operator || 'equals'}
onValueChange={(value) => updateRule(rule.id, { trigger_operator: value })}
>
<SelectTrigger className="mt-1">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="equals">equals</SelectItem>
<SelectItem value="not_equals">not equals</SelectItem>
<SelectItem value="contains">contains</SelectItem>
<SelectItem value="not_empty">is not empty</SelectItem>
<SelectItem value="empty">is empty</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label className="text-xs">Value</Label>
{triggerFieldData?.type === 'checkbox' ? (
<Select
value={String(rule.trigger_value)}
onValueChange={(value) =>
updateRule(rule.id, { trigger_value: value === 'true' })
}
>
<SelectTrigger className="mt-1">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="true">Checked (true)</SelectItem>
<SelectItem value="false">Unchecked (false)</SelectItem>
</SelectContent>
</Select>
) : ['empty', 'not_empty'].includes(rule.trigger_operator) ? (
<Input
className="mt-1"
value="(no value needed)"
disabled
/>
) : (
<Input
className="mt-1"
value={rule.trigger_value || ''}
onChange={(e) => updateRule(rule.id, { trigger_value: e.target.value })}
placeholder="Enter value"
/>
)}
</div>
</div>
{/* Action Selection */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
<div>
<Label className="text-xs">Action</Label>
<Select
value={rule.action || 'show'}
onValueChange={(value) => updateRule(rule.id, { action: value })}
>
<SelectTrigger className="mt-1">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="show">Show fields</SelectItem>
<SelectItem value="hide">Hide fields</SelectItem>
<SelectItem value="require">Make required</SelectItem>
<SelectItem value="optional">Make optional</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label className="text-xs">Target Fields</Label>
<div className="mt-1 border rounded-md p-2 bg-white max-h-32 overflow-y-auto">
{allFields.length === 0 ? (
<p className="text-xs text-muted-foreground">No fields available</p>
) : (
<div className="space-y-1">
{allFields
.filter(f => f.id !== rule.trigger_field) // Don't show trigger field as target
.map((field) => (
<div key={field.id} className="flex items-center gap-2">
<Checkbox
id={`${rule.id}-${field.id}`}
checked={(rule.target_fields || []).includes(field.id)}
onCheckedChange={(checked) => {
const currentTargets = rule.target_fields || [];
const newTargets = checked
? [...currentTargets, field.id]
: currentTargets.filter((id) => id !== field.id);
updateRule(rule.id, { target_fields: newTargets });
}}
/>
<Label
htmlFor={`${rule.id}-${field.id}`}
className="text-xs font-normal cursor-pointer"
>
{field.label}
</Label>
</div>
))}
</div>
)}
</div>
</div>
</div>
{/* Rule Summary */}
{rule.trigger_field && (rule.target_fields?.length || 0) > 0 && (
<div className="text-xs bg-blue-50 border border-blue-200 rounded p-2 text-blue-800">
<strong>Summary:</strong> When "{triggerFieldData?.label || rule.trigger_field}" {rule.trigger_operator} "{String(rule.trigger_value)}", {rule.action} the following fields: {rule.target_fields?.map(id => allFields.find(f => f.id === id)?.label || id).join(', ')}
</div>
)}
</div> </div>
<div className="text-sm text-muted-foreground"> );
When <span className="font-mono bg-gray-100 px-1">{rule.trigger_field}</span>{' '} })}
{rule.trigger_operator}{' '}
<span className="font-mono bg-gray-100 px-1">{String(rule.trigger_value)}</span>,{' '}
{rule.action} fields:{' '}
<span className="font-mono bg-gray-100 px-1">
{rule.target_fields?.join(', ')}
</span>
</div>
</div>
))}
{(schema?.conditional_rules || []).length === 0 && ( {(schema?.conditional_rules || []).length === 0 && (
<div className="text-center py-8 text-muted-foreground"> <div className="text-center py-8 text-muted-foreground border-2 border-dashed rounded-lg">
No conditional rules configured. Rules allow you to show or hide fields based on No conditional rules configured yet.<br />
other field values. Click "Add Rule" to create your first rule.
</div> </div>
)} )}