fix: 8 improvements — calendar overlaps, legend, nav, tech tasks photos/sort/modal, hk history active maintenance
This commit is contained in:
@@ -126,12 +126,28 @@ export function TechnicalPage() {
|
||||
}
|
||||
}
|
||||
|
||||
const filtered = tasks.filter(t => {
|
||||
const s = t.status as string
|
||||
if (filterStatus === 'active') return s !== 'done' && s !== 'cancelled'
|
||||
if (filterStatus === 'done') return s === 'done'
|
||||
return true
|
||||
})
|
||||
const filtered = tasks
|
||||
.filter(t => {
|
||||
const s = t.status as string
|
||||
if (filterStatus === 'active') return s !== 'done' && s !== 'cancelled'
|
||||
if (filterStatus === 'done') return s === 'done'
|
||||
return true
|
||||
})
|
||||
.sort((a, b) => {
|
||||
const aRec = a as unknown as Record<string, string>
|
||||
const bRec = b as unknown as Record<string, string>
|
||||
// Done tasks: sort by completedAt DESC
|
||||
if (a.status === 'done' && b.status === 'done') {
|
||||
const aT = aRec.completedAt ?? aRec.completed_at ?? ''
|
||||
const bT = bRec.completedAt ?? bRec.completed_at ?? ''
|
||||
return bT.localeCompare(aT)
|
||||
}
|
||||
// Active tasks: urgent first, then by dueDate
|
||||
const priorityOrder: Record<string, number> = { urgent: 0, medium: 1, low: 2 }
|
||||
const pa = priorityOrder[String(aRec.priority ?? 'medium')] ?? 1
|
||||
const pb = priorityOrder[String(bRec.priority ?? 'medium')] ?? 1
|
||||
return pa - pb
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="p-4 md:p-6 max-w-4xl mx-auto">
|
||||
@@ -278,19 +294,9 @@ export function TechnicalPage() {
|
||||
{(problemPhotos.length > 0 || !isDone) && (
|
||||
<div className="flex flex-wrap gap-2 items-center">
|
||||
{problemPhotos.map(url => (
|
||||
<div key={url} className="relative group">
|
||||
<a href={url} target="_blank" rel="noreferrer">
|
||||
<img src={url} alt="" className="w-14 h-14 object-cover rounded-lg border border-slate-200 dark:border-slate-600 hover:opacity-90 transition-opacity" />
|
||||
</a>
|
||||
{!isDone && (
|
||||
<button
|
||||
onClick={() => handlePhotoRemove(task.id, url)}
|
||||
className="absolute -top-1.5 -right-1.5 w-4 h-4 rounded-full bg-red-500 text-white flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
>
|
||||
<XIcon size={10} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<a key={url} href={url} target="_blank" rel="noreferrer">
|
||||
<img src={url} alt="" className="w-14 h-14 object-cover rounded-lg border border-slate-200 dark:border-slate-600 hover:opacity-90 transition-opacity" />
|
||||
</a>
|
||||
))}
|
||||
{!isDone && (
|
||||
<PhotoUploadButton isUploading={isUploading} onFile={file => handlePhotoUpload(task.id, file)} />
|
||||
@@ -406,12 +412,25 @@ function TaskCreateModal({
|
||||
const [rooms, setRooms] = useState<{ id: string; number: string }[]>([])
|
||||
const [users, setUsers] = useState<{ id: string; name: string }[]>([])
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [photos, setPhotos] = useState<string[]>([])
|
||||
const [photoUploading, setPhotoUploading] = useState(false)
|
||||
const photoRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
api.rooms.list(slug).then(r => setRooms(r.map(rm => ({ id: rm.id, number: rm.number })))).catch(() => {})
|
||||
api.users.list(slug).then(u => setUsers(u)).catch(() => {})
|
||||
}, [slug])
|
||||
|
||||
const handlePhotoUpload = async (file: File) => {
|
||||
setPhotoUploading(true)
|
||||
try {
|
||||
const url = await api.upload.photo(file, 'tasks')
|
||||
setPhotos(prev => [...prev, url])
|
||||
} catch { /* ignore */ } finally {
|
||||
setPhotoUploading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!form.notes.trim()) return
|
||||
setSaving(true)
|
||||
@@ -424,6 +443,7 @@ function TaskCreateModal({
|
||||
due_date: form.due_date || undefined,
|
||||
assignee_id: form.assignee_id || undefined,
|
||||
category: 'maintenance',
|
||||
photos,
|
||||
})
|
||||
onCreated(task)
|
||||
} catch (err) {
|
||||
@@ -450,7 +470,7 @@ function TaskCreateModal({
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">Приоритет</label>
|
||||
<select className="input" value={form.priority} onChange={e => setForm(p => ({ ...p, priority: e.target.value }))}>
|
||||
<option value="urgent">Срочно</option>
|
||||
<option value="urgent">Экстренно!</option>
|
||||
<option value="medium">Средний</option>
|
||||
<option value="low">Низкий</option>
|
||||
</select>
|
||||
@@ -474,9 +494,33 @@ function TaskCreateModal({
|
||||
{users.map(u => <option key={u.id} value={u.id}>{u.name}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">Фото (необязательно)</label>
|
||||
<input ref={photoRef} type="file" accept="image/*" className="hidden"
|
||||
onChange={e => { const f = e.target.files?.[0]; if (f) { handlePhotoUpload(f); e.target.value = '' } }} />
|
||||
<div className="flex flex-wrap gap-2 items-center">
|
||||
{photos.map((url, i) => (
|
||||
<div key={i} className="relative group">
|
||||
<img src={url} alt="" className="w-16 h-16 object-cover rounded-lg border border-slate-200 dark:border-slate-600" />
|
||||
<button
|
||||
onClick={() => setPhotos(prev => prev.filter((_, j) => j !== i))}
|
||||
className="absolute -top-1.5 -right-1.5 w-5 h-5 rounded-full bg-red-500 text-white flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
><XIcon size={11} /></button>
|
||||
</div>
|
||||
))}
|
||||
<button
|
||||
onClick={() => photoRef.current?.click()}
|
||||
disabled={photoUploading}
|
||||
className="w-16 h-16 rounded-lg border-2 border-dashed border-slate-300 dark:border-slate-600 flex flex-col items-center justify-center gap-1 text-slate-400 hover:border-brand-400 hover:text-brand-500 transition-colors disabled:opacity-50"
|
||||
>
|
||||
{photoUploading ? <Loader2 size={16} className="animate-spin" /> : <Camera size={16} />}
|
||||
{!photoUploading && <span className="text-[10px]">Фото</span>}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end gap-2 pt-2">
|
||||
<button onClick={onClose} className="btn-secondary">Отмена</button>
|
||||
<button onClick={handleSave} disabled={saving || !form.notes.trim()} className="btn-primary">
|
||||
<button onClick={handleSave} disabled={saving || !form.notes.trim() || photoUploading} className="btn-primary">
|
||||
{saving ? 'Создание...' : 'Создать'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user