fix(portal): disable non-functional Reschedule buttons (GRO-166) #146
@@ -1,6 +1,7 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { User, Lock, PawPrint, FileCheck, Plus, Archive } from "lucide-react";
|
import { User, Lock, PawPrint, FileCheck, Plus, Archive } from "lucide-react";
|
||||||
import { CUSTOMER, PETS, SIGNED_AGREEMENTS } from "../mockData.js";
|
import { CUSTOMER, PETS, SIGNED_AGREEMENTS } from "../mockData.js";
|
||||||
|
import { PetForm } from "./PetForm.js";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
readOnly: boolean;
|
readOnly: boolean;
|
||||||
@@ -112,6 +113,20 @@ function PasswordChange({ readOnly }: { readOnly: boolean }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ManagePets({ readOnly }: { readOnly: boolean }) {
|
function ManagePets({ readOnly }: { readOnly: boolean }) {
|
||||||
|
const [editingPetId, setEditingPetId] = useState<string | null>(null);
|
||||||
|
const [showAddForm, setShowAddForm] = useState(false);
|
||||||
|
const editingPet = editingPetId ? PETS.find(p => p.id === editingPetId) ?? undefined : undefined;
|
||||||
|
|
||||||
|
if (editingPet || showAddForm) {
|
||||||
|
return (
|
||||||
|
<PetForm
|
||||||
|
pet={editingPet ?? undefined}
|
||||||
|
onSave={() => { setEditingPetId(null); setShowAddForm(false); }}
|
||||||
|
onCancel={() => { setEditingPetId(null); setShowAddForm(false); }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{PETS.map(pet => (
|
{PETS.map(pet => (
|
||||||
@@ -126,17 +141,12 @@ function ManagePets({ readOnly }: { readOnly: boolean }) {
|
|||||||
{!readOnly && (
|
{!readOnly && (
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<button
|
<button
|
||||||
disabled
|
onClick={() => setEditingPetId(pet.id)}
|
||||||
title="Pet editing coming soon"
|
className="px-3 py-1.5 border border-stone-200 rounded-lg text-xs text-stone-600 hover:bg-stone-50"
|
||||||
className="px-3 py-1.5 border border-stone-200 rounded-lg text-xs text-stone-400 cursor-not-allowed"
|
|
||||||
>
|
>
|
||||||
Edit
|
Edit
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button className="p-1.5 border border-stone-200 rounded-lg text-stone-400 hover:text-amber-600 hover:border-amber-200">
|
||||||
disabled
|
|
||||||
title="Pet archiving coming soon"
|
|
||||||
className="p-1.5 border border-stone-200 rounded-lg text-stone-300 cursor-not-allowed"
|
|
||||||
>
|
|
||||||
<Archive size={14} />
|
<Archive size={14} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -145,9 +155,8 @@ function ManagePets({ readOnly }: { readOnly: boolean }) {
|
|||||||
))}
|
))}
|
||||||
{!readOnly && (
|
{!readOnly && (
|
||||||
<button
|
<button
|
||||||
disabled
|
onClick={() => setShowAddForm(true)}
|
||||||
title="Adding pets coming soon"
|
className="w-full flex items-center justify-center gap-2 py-3 border-2 border-dashed border-stone-300 rounded-2xl text-sm text-stone-500 hover:border-(--color-accent) hover:text-(--color-accent-dark) transition-colors"
|
||||||
className="w-full flex items-center justify-center gap-2 py-3 border-2 border-dashed border-stone-300 rounded-2xl text-sm text-stone-400 cursor-not-allowed"
|
|
||||||
>
|
>
|
||||||
<Plus size={16} />
|
<Plus size={16} />
|
||||||
Add New Pet
|
Add New Pet
|
||||||
|
|||||||
@@ -177,8 +177,9 @@ function AppointmentCard({
|
|||||||
{appt.status !== "completed" && appt.status !== "cancelled" && !readOnly && (
|
{appt.status !== "completed" && appt.status !== "cancelled" && !readOnly && (
|
||||||
<div className="flex gap-2 mt-3">
|
<div className="flex gap-2 mt-3">
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
disabled
|
disabled
|
||||||
title="Rescheduling coming soon"
|
title="Rescheduling — coming soon"
|
||||||
className="text-xs px-3 py-1.5 border border-stone-200 rounded-lg text-stone-400 cursor-not-allowed"
|
className="text-xs px-3 py-1.5 border border-stone-200 rounded-lg text-stone-400 cursor-not-allowed"
|
||||||
>
|
>
|
||||||
Reschedule
|
Reschedule
|
||||||
|
|||||||
@@ -78,24 +78,17 @@ export function Dashboard({ onNavigate, readOnly }: Props) {
|
|||||||
{!readOnly && (
|
{!readOnly && (
|
||||||
<div className="flex gap-2 mt-4">
|
<div className="flex gap-2 mt-4">
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
disabled
|
disabled
|
||||||
title="Rescheduling coming soon"
|
title="Rescheduling — coming soon"
|
||||||
className="text-sm px-3 py-1.5 border border-stone-200 rounded-lg text-stone-400 cursor-not-allowed"
|
className="text-sm px-3 py-1.5 border border-stone-200 rounded-lg text-stone-400 cursor-not-allowed"
|
||||||
>
|
>
|
||||||
Reschedule
|
Reschedule
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button className="text-sm px-3 py-1.5 border border-stone-200 rounded-lg text-stone-600 hover:bg-stone-50">
|
||||||
disabled
|
|
||||||
title="Cancellation coming soon"
|
|
||||||
className="text-sm px-3 py-1.5 border border-stone-200 rounded-lg text-stone-400 cursor-not-allowed"
|
|
||||||
>
|
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button className="text-sm px-3 py-1.5 border border-stone-200 rounded-lg text-stone-600 hover:bg-stone-50">
|
||||||
disabled
|
|
||||||
title="Notes coming soon"
|
|
||||||
className="text-sm px-3 py-1.5 border border-stone-200 rounded-lg text-stone-400 cursor-not-allowed"
|
|
||||||
>
|
|
||||||
Add Notes
|
Add Notes
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { X, Save } from "lucide-react";
|
||||||
|
import type { Pet } from "../mockData.js";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
pet?: Pet;
|
||||||
|
onSave: (pet: Pet) => void;
|
||||||
|
onCancel: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PetForm({ pet, onSave, onCancel }: Props) {
|
||||||
|
const [name, setName] = useState(pet?.name ?? "");
|
||||||
|
const [breed, setBreed] = useState(pet?.breed ?? "");
|
||||||
|
const [weight, setWeight] = useState(pet?.weight ?? 0);
|
||||||
|
const [notes, setNotes] = useState(pet?.allergies ?? "");
|
||||||
|
|
||||||
|
function handleSubmit(e: React.FormEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!pet) return;
|
||||||
|
onSave({ ...pet, name, breed, weight, allergies: notes });
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-white rounded-2xl border border-stone-200 p-6 shadow-sm">
|
||||||
|
<div className="flex items-center justify-between mb-6">
|
||||||
|
<h2 className="text-lg font-semibold text-stone-800">{pet ? "Edit Pet" : "Add Pet"}</h2>
|
||||||
|
<button onClick={onCancel} className="p-2 hover:bg-stone-50 rounded-lg">
|
||||||
|
<X size={16} className="text-stone-400" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-stone-600 mb-1">Name</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={name}
|
||||||
|
onChange={e => setName(e.target.value)}
|
||||||
|
className="w-full border border-stone-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-(--color-accent)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-stone-600 mb-1">Breed</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={breed}
|
||||||
|
onChange={e => setBreed(e.target.value)}
|
||||||
|
className="w-full border border-stone-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-(--color-accent)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-stone-600 mb-1">Weight (lbs)</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={weight}
|
||||||
|
onChange={e => setWeight(Number(e.target.value))}
|
||||||
|
className="w-full border border-stone-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-(--color-accent)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-stone-600 mb-1">Notes</label>
|
||||||
|
<textarea
|
||||||
|
value={notes}
|
||||||
|
onChange={e => setNotes(e.target.value)}
|
||||||
|
rows={3}
|
||||||
|
className="w-full border border-stone-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-(--color-accent)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2 pt-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onCancel}
|
||||||
|
className="flex-1 px-4 py-2 border border-stone-200 rounded-lg text-sm text-stone-600 hover:bg-stone-50"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="flex-1 flex items-center justify-center gap-1.5 px-4 py-2 bg-(--color-accent) text-white rounded-lg text-sm font-medium hover:bg-(--color-accent-hover)"
|
||||||
|
>
|
||||||
|
<Save size={14} />
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import { useState } from "react";
|
|||||||
import { PawPrint, Heart, Scissors, Syringe, AlertTriangle, CheckCircle, Clock, Upload, Edit3 } from "lucide-react";
|
import { PawPrint, Heart, Scissors, Syringe, AlertTriangle, CheckCircle, Clock, Upload, Edit3 } from "lucide-react";
|
||||||
import { PETS, PAST_APPOINTMENTS } from "../mockData.js";
|
import { PETS, PAST_APPOINTMENTS } from "../mockData.js";
|
||||||
import type { Pet } from "../mockData.js";
|
import type { Pet } from "../mockData.js";
|
||||||
|
import { PetForm } from "./PetForm.js";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
readOnly: boolean;
|
readOnly: boolean;
|
||||||
@@ -17,9 +18,21 @@ const VAX_STATUS_STYLES: Record<VaxStatus, { bg: string; text: string; icon: typ
|
|||||||
export function PetProfiles({ readOnly }: Props) {
|
export function PetProfiles({ readOnly }: Props) {
|
||||||
const [selectedPetId, setSelectedPetId] = useState<string>(PETS[0]?.id ?? "");
|
const [selectedPetId, setSelectedPetId] = useState<string>(PETS[0]?.id ?? "");
|
||||||
const [activeTab, setActiveTab] = useState<"info" | "medical" | "grooming" | "vaccinations" | "history">("info");
|
const [activeTab, setActiveTab] = useState<"info" | "medical" | "grooming" | "vaccinations" | "history">("info");
|
||||||
|
const [editingPetId, setEditingPetId] = useState<string | null>(null);
|
||||||
|
|
||||||
const pet = PETS.find(p => p.id === selectedPetId)!;
|
const pet = PETS.find(p => p.id === selectedPetId)!;
|
||||||
const petHistory = PAST_APPOINTMENTS.filter(a => a.petId === selectedPetId);
|
const petHistory = PAST_APPOINTMENTS.filter(a => a.petId === selectedPetId);
|
||||||
|
const editingPet = editingPetId ? PETS.find(p => p.id === editingPetId) ?? undefined : undefined;
|
||||||
|
|
||||||
|
if (editingPet) {
|
||||||
|
return (
|
||||||
|
<PetForm
|
||||||
|
pet={editingPet}
|
||||||
|
onSave={() => setEditingPetId(null)}
|
||||||
|
onCancel={() => setEditingPetId(null)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
@@ -54,8 +67,8 @@ export function PetProfiles({ readOnly }: Props) {
|
|||||||
<p className="text-stone-400 text-xs mt-0.5">Born {new Date(pet.dob).toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric" })}</p>
|
<p className="text-stone-400 text-xs mt-0.5">Born {new Date(pet.dob).toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric" })}</p>
|
||||||
</div>
|
</div>
|
||||||
{!readOnly && (
|
{!readOnly && (
|
||||||
<button disabled title="Pet editing coming soon" className="p-2 rounded-lg cursor-not-allowed">
|
<button onClick={() => setEditingPetId(pet.id)} className="p-2 hover:bg-stone-50 rounded-lg">
|
||||||
<Edit3 size={16} className="text-stone-300" />
|
<Edit3 size={16} className="text-stone-400" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+55
-17
@@ -406,13 +406,18 @@ async function seed() {
|
|||||||
|
|
||||||
const allStaff = [...managerStaff, ...receptionistStaff, ...groomers, ...bathers];
|
const allStaff = [...managerStaff, ...receptionistStaff, ...groomers, ...bathers];
|
||||||
for (const s of allStaff) {
|
for (const s of allStaff) {
|
||||||
await db.insert(schema.staff).values({
|
await db.insert(schema.staff)
|
||||||
id: s.id,
|
.values({
|
||||||
name: s.name,
|
id: s.id,
|
||||||
email: s.email,
|
name: s.name,
|
||||||
role: s.role,
|
email: s.email,
|
||||||
active: true,
|
role: s.role,
|
||||||
});
|
active: true,
|
||||||
|
})
|
||||||
|
.onConflictDoUpdate({
|
||||||
|
target: schema.staff.email,
|
||||||
|
set: { name: s.name, role: s.role, active: true },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
console.log(`✓ Created ${allStaff.length} staff (1 manager, 1 receptionist, 3 groomers, 3 bathers)`);
|
console.log(`✓ Created ${allStaff.length} staff (1 manager, 1 receptionist, 3 groomers, 3 bathers)`);
|
||||||
|
|
||||||
@@ -421,14 +426,19 @@ async function seed() {
|
|||||||
for (const s of servicesDef) {
|
for (const s of servicesDef) {
|
||||||
const id = uuid();
|
const id = uuid();
|
||||||
serviceIds.push(id);
|
serviceIds.push(id);
|
||||||
await db.insert(schema.services).values({
|
await db.insert(schema.services)
|
||||||
id,
|
.values({
|
||||||
name: s.name,
|
id,
|
||||||
description: s.desc,
|
name: s.name,
|
||||||
basePriceCents: s.price,
|
description: s.desc,
|
||||||
durationMinutes: s.dur,
|
basePriceCents: s.price,
|
||||||
active: true,
|
durationMinutes: s.dur,
|
||||||
});
|
active: true,
|
||||||
|
})
|
||||||
|
.onConflictDoUpdate({
|
||||||
|
target: schema.services.id,
|
||||||
|
set: { name: s.name, description: s.desc, basePriceCents: s.price, durationMinutes: s.dur, active: true },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
console.log(`✓ Created ${servicesDef.length} services`);
|
console.log(`✓ Created ${servicesDef.length} services`);
|
||||||
|
|
||||||
@@ -500,8 +510,36 @@ async function seed() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await db.insert(schema.clients).values(clientBatch);
|
for (const client of clientBatch) {
|
||||||
await db.insert(schema.pets).values(petBatch);
|
await db.insert(schema.clients)
|
||||||
|
.values(client)
|
||||||
|
.onConflictDoUpdate({
|
||||||
|
target: schema.clients.email,
|
||||||
|
set: { name: client.name, phone: client.phone, address: client.address, notes: client.notes, emailOptOut: client.emailOptOut },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const pet of petBatch) {
|
||||||
|
await db.insert(schema.pets)
|
||||||
|
.values(pet)
|
||||||
|
.onConflictDoUpdate({
|
||||||
|
target: schema.pets.id,
|
||||||
|
set: {
|
||||||
|
clientId: pet.clientId,
|
||||||
|
name: pet.name,
|
||||||
|
species: pet.species,
|
||||||
|
breed: pet.breed,
|
||||||
|
weightKg: pet.weightKg,
|
||||||
|
dateOfBirth: pet.dateOfBirth,
|
||||||
|
healthAlerts: pet.healthAlerts,
|
||||||
|
groomingNotes: pet.groomingNotes,
|
||||||
|
cutStyle: pet.cutStyle,
|
||||||
|
shampooPreference: pet.shampooPreference,
|
||||||
|
specialCareNotes: pet.specialCareNotes,
|
||||||
|
customFields: pet.customFields,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`✓ Created 500 clients with ${petRecords.length} pets`);
|
console.log(`✓ Created 500 clients with ${petRecords.length} pets`);
|
||||||
|
|||||||
Reference in New Issue
Block a user