38740-vm/Migdalor-main/src/pages/ManageKeys.jsx
2026-02-24 15:03:51 +00:00

532 lines
22 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>);
}