532 lines
22 KiB
JavaScript
532 lines
22 KiB
JavaScript
import React, { useState } from 'react';
|
||
import { base44 } from '@/api/base44Client';
|
||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||
import { Button } from "@/components/ui/button";
|
||
import { Input } from "@/components/ui/input";
|
||
import { Label } from "@/components/ui/label";
|
||
import { Card } from "@/components/ui/card";
|
||
import { Badge } from "@/components/ui/badge";
|
||
import {
|
||
Dialog,
|
||
DialogContent,
|
||
DialogHeader,
|
||
DialogTitle,
|
||
DialogDescription } from
|
||
"@/components/ui/dialog";
|
||
import {
|
||
Select,
|
||
SelectContent,
|
||
SelectItem,
|
||
SelectTrigger,
|
||
SelectValue } from
|
||
"@/components/ui/select";
|
||
import {
|
||
Table,
|
||
TableBody,
|
||
TableCell,
|
||
TableHead,
|
||
TableHeader,
|
||
TableRow } from
|
||
"@/components/ui/table";
|
||
import { Plus, Key, Trash2, Edit2, Monitor } from 'lucide-react';
|
||
import { Checkbox } from "@/components/ui/checkbox";
|
||
import { toast } from 'sonner';
|
||
import { motion } from 'framer-motion';
|
||
|
||
export default function ManageKeys() {
|
||
const [showModal, setShowModal] = useState(false);
|
||
const [editingKey, setEditingKey] = useState(null);
|
||
const [misdarEditKey, setMisdarEditKey] = useState(null);
|
||
const [misdarValue, setMisdarValue] = useState('');
|
||
const [user, setUser] = useState(null);
|
||
const [formData, setFormData] = useState({ room_number: '', room_type: 'צוותי', has_computers: false, zone: '' });
|
||
const queryClient = useQueryClient();
|
||
|
||
React.useEffect(() => {
|
||
base44.auth.me().then(setUser).catch(() => {});
|
||
}, []);
|
||
|
||
const isAdmin = user?.role === 'admin';
|
||
|
||
const { data: keys = [], isLoading } = useQuery({
|
||
queryKey: ['keys'],
|
||
queryFn: () => base44.entities.ClassroomKey.list()
|
||
});
|
||
|
||
const { data: zones = [] } = useQuery({
|
||
queryKey: ['zones'],
|
||
queryFn: () => base44.entities.Zone.list('order'),
|
||
enabled: isAdmin
|
||
});
|
||
|
||
const { data: todayLessons = [] } = useQuery({
|
||
queryKey: ['today-lessons'],
|
||
queryFn: async () => {
|
||
const today = new Date().toISOString().split('T')[0];
|
||
return base44.entities.Lesson.filter({ date: today, status: 'assigned' });
|
||
}
|
||
});
|
||
|
||
const { data: wednesdayLessons = [] } = useQuery({
|
||
queryKey: ['wednesday-lessons'],
|
||
queryFn: async () => {
|
||
// Find the next Wednesday (or today if it's Wednesday)
|
||
const today = new Date();
|
||
const dayOfWeek = today.getDay();
|
||
const daysUntilWednesday = dayOfWeek === 3 ? 0 : (3 - dayOfWeek + 7) % 7;
|
||
const nextWednesday = new Date(today);
|
||
nextWednesday.setDate(today.getDate() + daysUntilWednesday);
|
||
const wednesdayDate = nextWednesday.toISOString().split('T')[0];
|
||
|
||
return base44.entities.Lesson.filter({ date: wednesdayDate, status: 'assigned' }, '-end_time');
|
||
},
|
||
enabled: isAdmin
|
||
});
|
||
|
||
const { data: allUsers = [] } = useQuery({
|
||
queryKey: ['users'],
|
||
queryFn: () => base44.entities.User.list(),
|
||
enabled: isAdmin
|
||
});
|
||
|
||
// Get current key holder for a room
|
||
const getCurrentHolder = (roomNumber) => {
|
||
const now = new Date();
|
||
const currentTime = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`;
|
||
|
||
const currentLesson = todayLessons.find((lesson) =>
|
||
lesson.assigned_key === roomNumber &&
|
||
lesson.start_time <= currentTime &&
|
||
lesson.end_time > currentTime
|
||
);
|
||
|
||
return currentLesson ? currentLesson.crew_name : null;
|
||
};
|
||
|
||
// Get who's responsible for cleaning this room (Misdar)
|
||
const getMisdarResponsible = (key) => {
|
||
// Check for manual assignment first
|
||
if (key.manual_misdar_assignment) {
|
||
return { crewName: key.manual_misdar_assignment, platoon: null };
|
||
}
|
||
|
||
if (!wednesdayLessons.length) return null;
|
||
|
||
// Find all lessons for this room
|
||
const roomLessons = wednesdayLessons.filter((l) => l.assigned_key === key.room_number);
|
||
if (roomLessons.length === 0) return null;
|
||
|
||
// Check each lesson to see if the key was passed to another crew
|
||
for (const lesson of roomLessons) {
|
||
// Check if there's another lesson that took this key after this one
|
||
const nextLesson = wednesdayLessons.find((l) =>
|
||
l.assigned_key === key.room_number &&
|
||
l.crew_manager !== lesson.crew_manager &&
|
||
l.start_time >= lesson.end_time
|
||
);
|
||
|
||
// If no one took the key after this lesson, this crew is responsible
|
||
if (!nextLesson) {
|
||
// Find the user who created this lesson to get their platoon
|
||
const userWhoCreated = allUsers.find((u) => u.email === lesson.crew_manager);
|
||
const platoon = userWhoCreated?.platoon_name || null;
|
||
return { crewName: lesson.crew_name, platoon };
|
||
}
|
||
}
|
||
|
||
return null;
|
||
};
|
||
|
||
const createMutation = useMutation({
|
||
mutationFn: (data) => base44.entities.ClassroomKey.create(data),
|
||
onSuccess: () => {
|
||
queryClient.invalidateQueries({ queryKey: ['keys'] });
|
||
setShowModal(false);
|
||
setFormData({ room_number: '', room_type: 'צוותי', has_computers: false, zone: '' });
|
||
toast.success('מפתח נוסף בהצלחה');
|
||
}
|
||
});
|
||
|
||
const updateMutation = useMutation({
|
||
mutationFn: ({ id, data }) => base44.entities.ClassroomKey.update(id, data),
|
||
onSuccess: () => {
|
||
queryClient.invalidateQueries({ queryKey: ['keys'] });
|
||
setShowModal(false);
|
||
setEditingKey(null);
|
||
setFormData({ room_number: '', room_type: 'צוותי', has_computers: false, zone: '' });
|
||
toast.success('מפתח עודכן בהצלחה');
|
||
}
|
||
});
|
||
|
||
const deleteMutation = useMutation({
|
||
mutationFn: (id) => base44.entities.ClassroomKey.delete(id),
|
||
onSuccess: () => {
|
||
queryClient.invalidateQueries({ queryKey: ['keys'] });
|
||
toast.success('מפתח נמחק בהצלחה');
|
||
}
|
||
});
|
||
|
||
const handleSubmit = () => {
|
||
if (editingKey) {
|
||
updateMutation.mutate({ id: editingKey.id, data: formData });
|
||
} else {
|
||
createMutation.mutate({ ...formData, status: 'available' });
|
||
}
|
||
};
|
||
|
||
const handleEdit = (key) => {
|
||
setEditingKey(key);
|
||
setFormData({ room_number: key.room_number, room_type: key.room_type, has_computers: key.has_computers || false, zone: key.zone || '' });
|
||
setShowModal(true);
|
||
};
|
||
|
||
const handleClose = () => {
|
||
setShowModal(false);
|
||
setEditingKey(null);
|
||
setFormData({ room_number: '', room_type: 'צוותי', has_computers: false, zone: '' });
|
||
};
|
||
|
||
const handleMisdarEdit = (key) => {
|
||
setMisdarEditKey(key);
|
||
setMisdarValue(key.manual_misdar_assignment || '');
|
||
};
|
||
|
||
const handleMisdarSave = async () => {
|
||
if (misdarEditKey) {
|
||
await updateMutation.mutateAsync({
|
||
id: misdarEditKey.id,
|
||
data: { manual_misdar_assignment: misdarValue || null }
|
||
});
|
||
setMisdarEditKey(null);
|
||
setMisdarValue('');
|
||
}
|
||
};
|
||
|
||
const smallCount = keys.filter((k) => k.room_type === 'צוותי').length;
|
||
const largeCount = keys.filter((k) => k.room_type === 'פלוגתי').length;
|
||
|
||
return (
|
||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-slate-100">
|
||
<div className="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||
<motion.div
|
||
initial={{ opacity: 0, y: -20 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
className="mb-8">
|
||
|
||
<h1 className="text-3xl font-bold text-slate-800 mb-2">
|
||
ניהול מפתחות 🗝️
|
||
</h1>
|
||
<p className="text-slate-500">
|
||
הוסף, ערוך או הסר מפתחות כיתות
|
||
</p>
|
||
</motion.div>
|
||
|
||
{/* Stats */}
|
||
<div className="grid grid-cols-2 md:grid-cols-3 gap-4 mb-8">
|
||
<Card className="p-4 border-slate-200">
|
||
<p className="text-sm text-slate-500">סה״כ מפתחות</p>
|
||
<p className="text-2xl font-bold text-slate-800">{keys.length}</p>
|
||
</Card>
|
||
<Card className="p-4 border-blue-200 bg-blue-50/50">
|
||
<p className="text-sm text-blue-600">חדרים צוותיים</p>
|
||
<p className="text-2xl font-bold text-blue-700">{smallCount}</p>
|
||
</Card>
|
||
<Card className="p-4 border-purple-200 bg-purple-50/50">
|
||
<p className="text-sm text-purple-600">חדרים פלוגתיים</p>
|
||
<p className="text-2xl font-bold text-purple-700">{largeCount}</p>
|
||
</Card>
|
||
</div>
|
||
|
||
{isAdmin &&
|
||
<div className="flex justify-end mb-6">
|
||
<Button
|
||
onClick={() => setShowModal(true)}
|
||
className="bg-emerald-600 hover:bg-emerald-700">
|
||
|
||
<Plus className="w-4 h-4 ml-2" />
|
||
הוסף מפתח חדש
|
||
</Button>
|
||
</div>
|
||
}
|
||
|
||
{/* Keys Table */}
|
||
<Card className="overflow-hidden border-slate-200">
|
||
<Table>
|
||
<TableHeader>
|
||
<TableRow className="bg-slate-50">
|
||
<TableHead className="h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px] text-center">מספר חדר</TableHead>
|
||
<TableHead className="h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px] text-center">סוג</TableHead>
|
||
<TableHead className="h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px] text-center">אזור</TableHead>
|
||
<TableHead className="h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px] text-center">מחשבים</TableHead>
|
||
<TableHead className="h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px] text-center">סטטוס / מחזיק</TableHead>
|
||
{isAdmin &&
|
||
<TableHead className="h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px] text-center">מסדר כיתות 🧹</TableHead>
|
||
}
|
||
{isAdmin &&
|
||
<TableHead className="h-10 px-2 align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px] text-center">פעולות</TableHead>
|
||
}
|
||
</TableRow>
|
||
</TableHeader>
|
||
<TableBody>
|
||
{isLoading ?
|
||
<TableRow>
|
||
<TableCell colSpan={isAdmin ? 7 : 5} className="text-center py-8 text-slate-400">
|
||
טוען...
|
||
</TableCell>
|
||
</TableRow> :
|
||
keys.length === 0 ?
|
||
<TableRow>
|
||
<TableCell colSpan={isAdmin ? 7 : 5} className="text-center py-8 text-slate-400">
|
||
עדיין לא נוספו מפתחות
|
||
</TableCell>
|
||
</TableRow> :
|
||
|
||
keys.map((key) =>
|
||
<TableRow key={key.id} className="hover:bg-slate-50/50">
|
||
<TableCell className="p-2 text-center flex items-center justify-center align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px] font-medium">
|
||
<div className="flex items-center gap-2">
|
||
<Key className="w-4 h-4 text-slate-400" />
|
||
{key.room_number}
|
||
</div>
|
||
</TableCell>
|
||
<TableCell className="p-2 align-middle text-center [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]">
|
||
<Badge variant="outline" className={
|
||
key.room_type === 'פלוגתי' ?
|
||
'border-purple-300 text-purple-700' :
|
||
'border-blue-300 text-blue-700'
|
||
}>
|
||
{key.room_type === 'פלוגתי' ? '🏢 פלוגתי' : '🏠 צוותי'}
|
||
</Badge>
|
||
</TableCell>
|
||
<TableCell className="p-2 align-middle text-center [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]">
|
||
{key.zone ? (
|
||
<Badge variant="outline" className="border-slate-300 text-slate-700">
|
||
📍 {key.zone}
|
||
</Badge>
|
||
) : (
|
||
<span className="text-slate-400">—</span>
|
||
)}
|
||
</TableCell>
|
||
<TableCell className="text-cente my-3 p-2 text-center t te tex texx text align-middle flex items-center justify-center [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]">
|
||
{key.has_computers ?
|
||
<Monitor className="w-4 h-4 text-blue-600" /> :
|
||
|
||
<span className="text-slate-300">—</span>
|
||
}
|
||
</TableCell>
|
||
<TableCell className="p-2 align-middle text-center [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]">
|
||
{(() => {
|
||
const holder = getCurrentHolder(key.room_number);
|
||
return holder ?
|
||
<div className="flex flex-col items-center gap-1">
|
||
<Badge className="bg-amber-100 text-amber-700 hover:bg-amber-100">
|
||
תפוס
|
||
</Badge>
|
||
<span className="text-xs text-slate-600">{holder}</span>
|
||
</div> :
|
||
|
||
<Badge className="bg-emerald-100 text-emerald-700 hover:bg-emerald-100">
|
||
זמין
|
||
</Badge>;
|
||
|
||
})()}
|
||
</TableCell>
|
||
{isAdmin &&
|
||
<TableCell className="p-2 align-middle text-center [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]">
|
||
<div className="flex items-center justify-center gap-2">
|
||
{(() => {
|
||
const responsible = getMisdarResponsible(key);
|
||
return responsible ?
|
||
<div className="flex flex-col items-center gap-1">
|
||
<Badge variant="outline" className="bg-orange-50 text-orange-700 border-orange-200">
|
||
🧹 {responsible.crewName}
|
||
</Badge>
|
||
{responsible.platoon &&
|
||
<span className="text-xs text-slate-500 font-medium">
|
||
{responsible.platoon}
|
||
</span>
|
||
}
|
||
</div> :
|
||
|
||
<span className="text-slate-400 text-xs">—</span>;
|
||
|
||
})()}
|
||
<Button
|
||
variant="ghost"
|
||
size="icon"
|
||
onClick={() => handleMisdarEdit(key)}
|
||
className="h-6 w-6 text-slate-400 hover:text-orange-600">
|
||
|
||
<Edit2 className="w-3 h-3" />
|
||
</Button>
|
||
</div>
|
||
</TableCell>
|
||
}
|
||
{isAdmin &&
|
||
<TableCell className="text-right">
|
||
<div className="flex justify-center gap-2">
|
||
<Button
|
||
variant="ghost"
|
||
size="icon"
|
||
onClick={() => handleEdit(key)}
|
||
className="text-slate-400 hover:text-slate-600">
|
||
<Edit2 className="w-4 h-4" />
|
||
</Button>
|
||
<Button
|
||
variant="ghost"
|
||
size="icon"
|
||
onClick={() => deleteMutation.mutate(key.id)}
|
||
className="text-red-400 hover:text-red-600 hover:bg-red-50">
|
||
<Trash2 className="w-4 h-4" />
|
||
</Button>
|
||
</div>
|
||
</TableCell>
|
||
}
|
||
</TableRow>
|
||
)
|
||
}
|
||
</TableBody>
|
||
</Table>
|
||
</Card>
|
||
</div>
|
||
|
||
{/* Misdar Edit Modal */}
|
||
<Dialog open={!!misdarEditKey} onOpenChange={() => {setMisdarEditKey(null);setMisdarValue('');}}>
|
||
<DialogContent className="sm:max-w-md" dir="rtl">
|
||
<DialogHeader className="text-right">
|
||
<DialogTitle className="flex flex-row-reverse items-center gap-2 justify-end">
|
||
ערוך מסדר כיתות
|
||
<div className="p-2 bg-orange-100 rounded-lg">
|
||
<span className="text-lg">🧹</span>
|
||
</div>
|
||
</DialogTitle>
|
||
<DialogDescription className="text-right">
|
||
הגדר ידנית איזו פלוגה אחראית על מסדר חדר {misdarEditKey?.room_number}
|
||
</DialogDescription>
|
||
</DialogHeader>
|
||
|
||
<div className="space-y-4 py-4">
|
||
<div className="space-y-2">
|
||
<Label className="text-right block">שם הפלוגה האחראית</Label>
|
||
<select
|
||
value={misdarValue}
|
||
onChange={(e) => setMisdarValue(e.target.value)}
|
||
className="w-full px-3 py-2 border border-slate-300 rounded-md text-right">
|
||
|
||
<option value="">חישוב אוטומטי</option>
|
||
<option value="פלוגה א - סהר">פלוגה א - סהר</option>
|
||
<option value="פלוגה ב - יפתח">פלוגה ב - יפתח</option>
|
||
<option value="פלוגה ג - אייל">פלוגה ג - אייל</option>
|
||
<option value="פלוגה ד - אסף">פלוגה ד - אסף</option>
|
||
<option value="פלוגה ה - איתן">פלוגה ה - איתן</option>
|
||
</select>
|
||
<p className="text-xs text-slate-500 text-right">
|
||
בחר "חישוב אוטומטי" כדי להשתמש בחישוב לפי לוח השיעורים ביום רביעי
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex flex-row-reverse gap-3">
|
||
<Button variant="outline" onClick={() => {setMisdarEditKey(null);setMisdarValue('');}} className="flex-1">
|
||
ביטול
|
||
</Button>
|
||
<Button
|
||
onClick={handleMisdarSave}
|
||
className="flex-1 bg-orange-600 hover:bg-orange-700">
|
||
|
||
שמור
|
||
</Button>
|
||
</div>
|
||
</DialogContent>
|
||
</Dialog>
|
||
|
||
{/* Add/Edit Modal */}
|
||
<Dialog open={showModal} onOpenChange={handleClose}>
|
||
<DialogContent className="sm:max-w-md" dir="rtl">
|
||
<DialogHeader className="text-right">
|
||
<DialogTitle className="flex flex-row-reverse items-center gap-2 justify-end">
|
||
|
||
{editingKey ? 'ערוך מפתח' : 'הוסף מפתח חדש'}
|
||
<div className="p-2 bg-emerald-100 rounded-lg">
|
||
|
||
<Key className="w-5 h-5 text-emerald-600" />
|
||
</div>
|
||
|
||
</DialogTitle>
|
||
<DialogDescription className="text-right">
|
||
{editingKey ? 'עדכן את פרטי המפתח' : 'הוסף מפתח חדש למעקב'}
|
||
</DialogDescription>
|
||
</DialogHeader>
|
||
|
||
<div className="space-y-4 py-4">
|
||
<div className="space-y-2">
|
||
<Label className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 text-right block">מספר חדר *</Label>
|
||
<Input
|
||
placeholder="למשל, 101..."
|
||
value={formData.room_number}
|
||
onChange={(e) => setFormData({ ...formData, room_number: e.target.value })}
|
||
className="text-right" />
|
||
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<Label className="text-right block">סוג חדר</Label>
|
||
<Select
|
||
value={formData.room_type}
|
||
onValueChange={(value) => setFormData({ ...formData, room_type: value })}>
|
||
|
||
<SelectTrigger className="text-right" dir="rtl">
|
||
<SelectValue className="text-right" />
|
||
</SelectTrigger>
|
||
<SelectContent align="end" dir="rtl">
|
||
<SelectItem value="צוותי">צוותי 🏠</SelectItem>
|
||
<SelectItem value="פלוגתי">פלוגתי 🏢</SelectItem>
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
|
||
<div className="flex flex-row-reverse items-center gap-2 justify-end">
|
||
<Label htmlFor="has_computers" className="cursor-pointer">
|
||
יש מחשב בכיתה 💻
|
||
</Label>
|
||
<Checkbox
|
||
id="has_computers"
|
||
checked={formData.has_computers}
|
||
onCheckedChange={(checked) =>
|
||
setFormData({ ...formData, has_computers: checked })
|
||
} />
|
||
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<Label className="text-right block">אזור (אופציונלי)</Label>
|
||
<select
|
||
value={formData.zone}
|
||
onChange={(e) => setFormData({ ...formData, zone: e.target.value })}
|
||
className="w-full px-3 py-2 border border-slate-300 rounded-md text-right"
|
||
>
|
||
<option value="">בחר אזור...</option>
|
||
{zones.map((zone) => (
|
||
<option key={zone.id} value={zone.name}>{zone.name}</option>
|
||
))}
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex flex-row-reverse gap-3">
|
||
<Button variant="outline" onClick={handleClose} className="flex-1">
|
||
ביטול
|
||
</Button>
|
||
<Button
|
||
onClick={handleSubmit}
|
||
disabled={!formData.room_number}
|
||
className="flex-1 bg-emerald-600 hover:bg-emerald-700">
|
||
|
||
{editingKey ? 'עדכן מפתח' : 'הוסף מפתח'}
|
||
</Button>
|
||
</div>
|
||
</DialogContent>
|
||
</Dialog>
|
||
</div>);
|
||
|
||
} |