mirror of
https://github.com/Oak-and-Sprout/sprout-track.git
synced 2026-02-12 18:59:52 -06:00
added ability to add caretakers
This commit is contained in:
233
app/api/caretaker/route.ts
Normal file
233
app/api/caretaker/route.ts
Normal file
@@ -0,0 +1,233 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import prisma from '../db';
|
||||
import { ApiResponse, CaretakerCreate, CaretakerUpdate, CaretakerResponse } from '../types';
|
||||
import { formatLocalTime } from '../utils/timezone';
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const body: CaretakerCreate = await req.json();
|
||||
|
||||
// Check if loginId is already in use
|
||||
const existingCaretaker = await prisma.caretaker.findFirst({
|
||||
where: {
|
||||
// Using type assertion to handle new field that TypeScript doesn't know about yet
|
||||
loginId: body.loginId,
|
||||
deletedAt: null,
|
||||
} as any,
|
||||
});
|
||||
|
||||
if (existingCaretaker) {
|
||||
return NextResponse.json<ApiResponse<CaretakerResponse>>(
|
||||
{
|
||||
success: false,
|
||||
error: 'Login ID is already in use. Please choose a different one.',
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const caretaker = await prisma.caretaker.create({
|
||||
data: body,
|
||||
});
|
||||
|
||||
// Format response with local timezone
|
||||
const response: CaretakerResponse = {
|
||||
...caretaker,
|
||||
createdAt: await formatLocalTime(caretaker.createdAt),
|
||||
updatedAt: await formatLocalTime(caretaker.updatedAt),
|
||||
deletedAt: caretaker.deletedAt ? await formatLocalTime(caretaker.deletedAt) : null,
|
||||
};
|
||||
|
||||
return NextResponse.json<ApiResponse<CaretakerResponse>>({
|
||||
success: true,
|
||||
data: response,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error creating caretaker:', error);
|
||||
return NextResponse.json<ApiResponse<CaretakerResponse>>(
|
||||
{
|
||||
success: false,
|
||||
error: 'Failed to create caretaker',
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function PUT(req: NextRequest) {
|
||||
try {
|
||||
const body: CaretakerUpdate = await req.json();
|
||||
const { id, ...updateData } = body;
|
||||
|
||||
const existingCaretaker = await prisma.caretaker.findUnique({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
if (!existingCaretaker) {
|
||||
return NextResponse.json<ApiResponse<CaretakerResponse>>(
|
||||
{
|
||||
success: false,
|
||||
error: 'Caretaker not found',
|
||||
},
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
// If loginId is being updated, check if it's already in use by another caretaker
|
||||
if (updateData.loginId) {
|
||||
const duplicateLoginId = await prisma.caretaker.findFirst({
|
||||
where: {
|
||||
// Using type assertion to handle new field that TypeScript doesn't know about yet
|
||||
loginId: updateData.loginId,
|
||||
id: { not: id },
|
||||
deletedAt: null,
|
||||
} as any,
|
||||
});
|
||||
|
||||
if (duplicateLoginId) {
|
||||
return NextResponse.json<ApiResponse<CaretakerResponse>>(
|
||||
{
|
||||
success: false,
|
||||
error: 'Login ID is already in use. Please choose a different one.',
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const caretaker = await prisma.caretaker.update({
|
||||
where: { id },
|
||||
data: updateData,
|
||||
});
|
||||
|
||||
// Format response with local timezone
|
||||
const response: CaretakerResponse = {
|
||||
...caretaker,
|
||||
createdAt: await formatLocalTime(caretaker.createdAt),
|
||||
updatedAt: await formatLocalTime(caretaker.updatedAt),
|
||||
deletedAt: caretaker.deletedAt ? await formatLocalTime(caretaker.deletedAt) : null,
|
||||
};
|
||||
|
||||
return NextResponse.json<ApiResponse<CaretakerResponse>>({
|
||||
success: true,
|
||||
data: response,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error updating caretaker:', error);
|
||||
return NextResponse.json<ApiResponse<CaretakerResponse>>(
|
||||
{
|
||||
success: false,
|
||||
error: 'Failed to update caretaker',
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(req: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(req.url);
|
||||
const id = searchParams.get('id');
|
||||
|
||||
if (!id) {
|
||||
return NextResponse.json<ApiResponse>(
|
||||
{
|
||||
success: false,
|
||||
error: 'Caretaker ID is required',
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Soft delete by setting deletedAt
|
||||
await prisma.caretaker.update({
|
||||
where: { id },
|
||||
data: { deletedAt: new Date() },
|
||||
});
|
||||
|
||||
return NextResponse.json<ApiResponse>({
|
||||
success: true,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error deleting caretaker:', error);
|
||||
return NextResponse.json<ApiResponse>(
|
||||
{
|
||||
success: false,
|
||||
error: 'Failed to delete caretaker',
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(req.url);
|
||||
const id = searchParams.get('id');
|
||||
|
||||
if (id) {
|
||||
const caretaker = await prisma.caretaker.findUnique({
|
||||
where: {
|
||||
id,
|
||||
deletedAt: null,
|
||||
},
|
||||
});
|
||||
|
||||
if (!caretaker) {
|
||||
return NextResponse.json<ApiResponse<CaretakerResponse>>(
|
||||
{
|
||||
success: false,
|
||||
error: 'Caretaker not found',
|
||||
},
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
// Format response with local timezone
|
||||
const response: CaretakerResponse = {
|
||||
...caretaker,
|
||||
createdAt: await formatLocalTime(caretaker.createdAt),
|
||||
updatedAt: await formatLocalTime(caretaker.updatedAt),
|
||||
deletedAt: caretaker.deletedAt ? await formatLocalTime(caretaker.deletedAt) : null,
|
||||
};
|
||||
|
||||
return NextResponse.json<ApiResponse<CaretakerResponse>>({
|
||||
success: true,
|
||||
data: response,
|
||||
});
|
||||
}
|
||||
|
||||
const caretakers = await prisma.caretaker.findMany({
|
||||
where: {
|
||||
deletedAt: null,
|
||||
},
|
||||
orderBy: {
|
||||
name: 'asc',
|
||||
},
|
||||
});
|
||||
|
||||
// Format response with local timezone
|
||||
const response: CaretakerResponse[] = await Promise.all(
|
||||
caretakers.map(async (caretaker) => ({
|
||||
...caretaker,
|
||||
createdAt: await formatLocalTime(caretaker.createdAt),
|
||||
updatedAt: await formatLocalTime(caretaker.updatedAt),
|
||||
deletedAt: caretaker.deletedAt ? await formatLocalTime(caretaker.deletedAt) : null,
|
||||
}))
|
||||
);
|
||||
|
||||
return NextResponse.json<ApiResponse<CaretakerResponse[]>>({
|
||||
success: true,
|
||||
data: response,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching caretakers:', error);
|
||||
return NextResponse.json<ApiResponse<CaretakerResponse[]>>(
|
||||
{
|
||||
success: false,
|
||||
error: 'Failed to fetch caretakers',
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Baby, SleepLog, FeedLog, DiaperLog, MoodLog, Note, Settings as PrismaSettings, Gender, SleepType, SleepQuality, FeedType, BreastSide, DiaperType, Mood } from '@prisma/client';
|
||||
import { Baby, SleepLog, FeedLog, DiaperLog, MoodLog, Note, Caretaker, Settings as PrismaSettings, Gender, SleepType, SleepQuality, FeedType, BreastSide, DiaperType, Mood } from '@prisma/client';
|
||||
|
||||
// Settings types
|
||||
export type Settings = PrismaSettings;
|
||||
@@ -116,3 +116,21 @@ export interface NoteCreate {
|
||||
content: string;
|
||||
category?: string;
|
||||
}
|
||||
|
||||
// Caretaker types
|
||||
export type CaretakerResponse = Omit<Caretaker, 'createdAt' | 'updatedAt' | 'deletedAt'> & {
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
deletedAt: string | null;
|
||||
};
|
||||
|
||||
export interface CaretakerCreate {
|
||||
loginId: string;
|
||||
name: string;
|
||||
type?: string;
|
||||
securityPin: string;
|
||||
}
|
||||
|
||||
export interface CaretakerUpdate extends Partial<CaretakerCreate> {
|
||||
id: string;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- Added the required column `loginId` to the `Caretaker` table without a default value. This is not possible if the table is not empty.
|
||||
|
||||
*/
|
||||
-- RedefineTables
|
||||
PRAGMA defer_foreign_keys=ON;
|
||||
PRAGMA foreign_keys=OFF;
|
||||
CREATE TABLE "new_Caretaker" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"loginId" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"type" TEXT,
|
||||
"securityPin" TEXT NOT NULL,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL,
|
||||
"deletedAt" DATETIME
|
||||
);
|
||||
INSERT INTO "new_Caretaker" ("loginId", "createdAt", "deletedAt", "id", "name", "securityPin", "type", "updatedAt") SELECT '01', "createdAt", "deletedAt", "id", "name", "securityPin", "type", "updatedAt" FROM "Caretaker";
|
||||
DROP TABLE "Caretaker";
|
||||
ALTER TABLE "new_Caretaker" RENAME TO "Caretaker";
|
||||
CREATE INDEX "Caretaker_deletedAt_idx" ON "Caretaker"("deletedAt");
|
||||
PRAGMA foreign_keys=ON;
|
||||
PRAGMA defer_foreign_keys=OFF;
|
||||
@@ -77,6 +77,7 @@ model Baby {
|
||||
|
||||
model Caretaker {
|
||||
id String @id @default(uuid())
|
||||
loginId String // Two-digit login identifier for quick authentication
|
||||
name String
|
||||
type String? // parent, daycare, nanny, grandparent, uncle, etc.
|
||||
securityPin String
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Baby, Unit } from '@prisma/client';
|
||||
import { Baby, Unit, Caretaker } from '@prisma/client';
|
||||
import { Settings } from '@/app/api/types';
|
||||
import { Settings as Plus, Edit, Download, Upload } from 'lucide-react';
|
||||
import { Button } from '@/src/components/ui/button';
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
} from '@/src/components/ui/form-page';
|
||||
import BabyModal from '@/src/components/modals/BabyModal';
|
||||
import ChangePinModal from '@/src/components/modals/ChangePinModal';
|
||||
import CaretakerModal from '@/src/components/modals/CaretakerModal';
|
||||
|
||||
interface SettingsFormProps {
|
||||
isOpen: boolean;
|
||||
@@ -39,10 +40,13 @@ export default function SettingsForm({
|
||||
}: SettingsFormProps) {
|
||||
const [settings, setSettings] = useState<Settings | null>(null);
|
||||
const [babies, setBabies] = useState<Baby[]>([]);
|
||||
const [caretakers, setCaretakers] = useState<Caretaker[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [showBabyModal, setShowBabyModal] = useState(false);
|
||||
const [showCaretakerModal, setShowCaretakerModal] = useState(false);
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [selectedBaby, setSelectedBaby] = useState<Baby | null>(null);
|
||||
const [selectedCaretaker, setSelectedCaretaker] = useState<Caretaker | null>(null);
|
||||
const [localSelectedBabyId, setLocalSelectedBabyId] = useState<string | undefined>(selectedBabyId);
|
||||
const [showChangePinModal, setShowChangePinModal] = useState(false);
|
||||
const [isRestoring, setIsRestoring] = useState(false);
|
||||
@@ -56,10 +60,11 @@ export default function SettingsForm({
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const [settingsResponse, babiesResponse, unitsResponse] = await Promise.all([
|
||||
const [settingsResponse, babiesResponse, unitsResponse, caretakersResponse] = await Promise.all([
|
||||
fetch('/api/settings'),
|
||||
fetch('/api/baby'),
|
||||
fetch('/api/units')
|
||||
fetch('/api/units'),
|
||||
fetch('/api/caretaker')
|
||||
]);
|
||||
|
||||
if (settingsResponse.ok) {
|
||||
@@ -76,6 +81,11 @@ export default function SettingsForm({
|
||||
const unitsData = await unitsResponse.json();
|
||||
setUnits(unitsData.data);
|
||||
}
|
||||
|
||||
if (caretakersResponse.ok) {
|
||||
const caretakersData = await caretakersResponse.json();
|
||||
setCaretakers(caretakersData.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching data:', error);
|
||||
} finally {
|
||||
@@ -115,6 +125,11 @@ export default function SettingsForm({
|
||||
onBabyStatusChange?.(); // Refresh parent's babies list
|
||||
};
|
||||
|
||||
const handleCaretakerModalClose = async () => {
|
||||
setShowCaretakerModal(false);
|
||||
await fetchData(); // Refresh local caretakers list
|
||||
};
|
||||
|
||||
const handleBackup = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/database');
|
||||
@@ -291,6 +306,53 @@ export default function SettingsForm({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-slate-200 pt-6">
|
||||
<h3 className="form-label mb-4">Manage Caretakers</h3>
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-wrap items-center gap-2 w-full">
|
||||
<div className="flex-1 min-w-[200px]">
|
||||
<Select
|
||||
value={selectedCaretaker?.id || ''}
|
||||
onValueChange={(caretakerId) => {
|
||||
const caretaker = caretakers.find(c => c.id === caretakerId);
|
||||
setSelectedCaretaker(caretaker || null);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a caretaker" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{caretakers.map((caretaker) => (
|
||||
<SelectItem key={caretaker.id} value={caretaker.id}>
|
||||
{caretaker.name} {caretaker.type ? `(${caretaker.type})` : ''}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
disabled={!selectedCaretaker}
|
||||
onClick={() => {
|
||||
setIsEditing(true);
|
||||
setShowCaretakerModal(true);
|
||||
}}
|
||||
>
|
||||
<Edit className="h-4 w-3 mr-2" />
|
||||
Edit
|
||||
</Button>
|
||||
<Button variant="outline" onClick={() => {
|
||||
setIsEditing(false);
|
||||
setSelectedCaretaker(null);
|
||||
setShowCaretakerModal(true);
|
||||
}}>
|
||||
<Plus className="h-4 w-3 mr-2" />
|
||||
Add
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-slate-200 pt-6">
|
||||
<h3 className="form-label mb-4">Default Units</h3>
|
||||
<div className="space-y-4">
|
||||
@@ -432,6 +494,13 @@ export default function SettingsForm({
|
||||
baby={selectedBaby}
|
||||
/>
|
||||
|
||||
<CaretakerModal
|
||||
open={showCaretakerModal}
|
||||
onClose={handleCaretakerModalClose}
|
||||
isEditing={isEditing}
|
||||
caretaker={selectedCaretaker}
|
||||
/>
|
||||
|
||||
<ChangePinModal
|
||||
open={showChangePinModal}
|
||||
onClose={() => setShowChangePinModal(false)}
|
||||
|
||||
264
src/components/modals/CaretakerModal.tsx
Normal file
264
src/components/modals/CaretakerModal.tsx
Normal file
@@ -0,0 +1,264 @@
|
||||
import { Caretaker as PrismaCaretaker } from '@prisma/client';
|
||||
|
||||
// Extended type to include the new loginId field
|
||||
interface Caretaker extends PrismaCaretaker {
|
||||
loginId: string;
|
||||
}
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
} from '@/src/components/ui/dialog';
|
||||
import { Button } from '@/src/components/ui/button';
|
||||
import { Input } from '@/src/components/ui/input';
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
interface CaretakerModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
isEditing: boolean;
|
||||
caretaker: (PrismaCaretaker & { loginId?: string }) | null;
|
||||
}
|
||||
|
||||
const defaultFormData = {
|
||||
loginId: '',
|
||||
name: '',
|
||||
type: '',
|
||||
securityPin: '',
|
||||
};
|
||||
|
||||
export default function CaretakerModal({
|
||||
open,
|
||||
onClose,
|
||||
isEditing,
|
||||
caretaker,
|
||||
}: CaretakerModalProps) {
|
||||
const [formData, setFormData] = useState(defaultFormData);
|
||||
const [confirmPin, setConfirmPin] = useState('');
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
// Reset form when modal opens/closes or caretaker changes
|
||||
useEffect(() => {
|
||||
if (caretaker && open) {
|
||||
setFormData({
|
||||
loginId: caretaker.loginId || '',
|
||||
name: caretaker.name,
|
||||
type: caretaker.type || '',
|
||||
securityPin: caretaker.securityPin,
|
||||
});
|
||||
setConfirmPin(caretaker.securityPin);
|
||||
} else if (!open) {
|
||||
setFormData(defaultFormData);
|
||||
setConfirmPin('');
|
||||
setError('');
|
||||
}
|
||||
}, [caretaker, open]);
|
||||
|
||||
const validatePIN = () => {
|
||||
if (formData.securityPin.length < 6) {
|
||||
setError('PIN must be at least 6 digits');
|
||||
return false;
|
||||
}
|
||||
if (formData.securityPin.length > 10) {
|
||||
setError('PIN cannot be longer than 10 digits');
|
||||
return false;
|
||||
}
|
||||
if (formData.securityPin !== confirmPin) {
|
||||
setError('PINs do not match');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (isSubmitting) return;
|
||||
|
||||
// Validate form
|
||||
if (!formData.loginId.trim()) {
|
||||
setError('Login ID is required');
|
||||
return;
|
||||
}
|
||||
|
||||
if (formData.loginId.length !== 2) {
|
||||
setError('Login ID must be exactly 2 characters');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!formData.name.trim()) {
|
||||
setError('Name is required');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!validatePIN()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
setError('');
|
||||
|
||||
const response = await fetch('/api/caretaker', {
|
||||
method: isEditing ? 'PUT' : 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
...formData,
|
||||
id: caretaker?.id,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.message || 'Failed to save caretaker');
|
||||
}
|
||||
|
||||
onClose();
|
||||
} catch (error) {
|
||||
console.error('Error saving caretaker:', error);
|
||||
setError(error instanceof Error ? error.message : 'Failed to save caretaker');
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setFormData(defaultFormData);
|
||||
setConfirmPin('');
|
||||
setError('');
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={handleClose}>
|
||||
<DialogContent className="dialog-content !p-4 sm:!p-6">
|
||||
<DialogHeader className="dialog-header">
|
||||
<DialogTitle className="dialog-title">
|
||||
{isEditing ? 'Edit Caretaker' : 'Add New Caretaker'}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="dialog-description">
|
||||
{isEditing
|
||||
? "Update caretaker information"
|
||||
: "Enter caretaker information to add them to the system"
|
||||
}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<label className="form-label">Login ID</label>
|
||||
<Input
|
||||
value={formData.loginId}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
// Allow any input up to 2 characters
|
||||
setFormData({ ...formData, loginId: value });
|
||||
}}
|
||||
className="w-full"
|
||||
placeholder="Enter 2-digit login ID"
|
||||
maxLength={2}
|
||||
required
|
||||
autoComplete="off"
|
||||
/>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
Login ID must be exactly 2 digits or characters (currently: {formData.loginId.length}/2)
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="form-label">Name</label>
|
||||
<Input
|
||||
value={formData.name}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, name: e.target.value })
|
||||
}
|
||||
className="w-full"
|
||||
placeholder="Enter caretaker name"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="form-label">Type (Optional)</label>
|
||||
<Input
|
||||
value={formData.type}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, type: e.target.value })
|
||||
}
|
||||
className="w-full"
|
||||
placeholder="Parent, Grandparent, Nanny, etc."
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="form-label">Security PIN</label>
|
||||
<Input
|
||||
type="password"
|
||||
value={formData.securityPin}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value.replace(/\D/g, '');
|
||||
if (value.length <= 10) {
|
||||
setFormData({ ...formData, securityPin: value });
|
||||
}
|
||||
}}
|
||||
className="w-full"
|
||||
placeholder="Enter 6-10 digit PIN"
|
||||
minLength={6}
|
||||
maxLength={10}
|
||||
pattern="\d*"
|
||||
required
|
||||
/>
|
||||
<p className="text-xs text-gray-500 mt-1">PIN must be between 6 and 10 digits</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="form-label">Confirm PIN</label>
|
||||
<Input
|
||||
type="password"
|
||||
value={confirmPin}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value.replace(/\D/g, '');
|
||||
if (value.length <= 10) {
|
||||
setConfirmPin(value);
|
||||
}
|
||||
}}
|
||||
className="w-full"
|
||||
placeholder="Confirm PIN"
|
||||
minLength={6}
|
||||
maxLength={10}
|
||||
pattern="\d*"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="text-sm text-red-500 font-medium">{error}</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-2 sm:flex sm:justify-end gap-3 mt-6">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={handleClose}
|
||||
className="hover:bg-slate-50"
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
className="bg-gradient-to-r from-teal-600 to-emerald-600 text-white hover:from-teal-700 hover:to-emerald-700"
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{isSubmitting
|
||||
? 'Saving...'
|
||||
: isEditing
|
||||
? 'Save Changes'
|
||||
: 'Add Caretaker'
|
||||
}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
export const dialogStyles = {
|
||||
overlay: "fixed inset-0 z-50 bg-black/40 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
content: "fixed left-[50%] top-[50%] z-50 grid w-full max-w-[400px] sm:max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 bg-white/95 backdrop-blur-sm px-4 py-6 sm:p-6 shadow-xl duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-2xl border border-slate-200",
|
||||
overlay: "fixed inset-0 z-[100] bg-black/40 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
content: "fixed left-[50%] top-[50%] z-[101] grid w-full max-w-[400px] sm:max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 bg-white/95 backdrop-blur-sm px-4 py-6 sm:p-6 shadow-xl duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-2xl border border-slate-200",
|
||||
closeButton: "absolute right-4 top-4 rounded-full p-2 text-slate-400 hover:text-slate-600 hover:bg-slate-100/80 transition-colors",
|
||||
header: "flex flex-col space-y-1.5 text-center sm:text-left",
|
||||
footer: "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
|
||||
Reference in New Issue
Block a user