v.0.9.66 UI changes
This commit is contained in:
@@ -10,7 +10,7 @@
|
||||
PROJECT_NAME=jama
|
||||
|
||||
# Image version to run (set by build.sh, or use 'latest')
|
||||
JAMA_VERSION=0.9.65
|
||||
JAMA_VERSION=0.9.66
|
||||
|
||||
# App port — the host port Docker maps to the container
|
||||
PORT=3000
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jama-backend",
|
||||
"version": "0.9.65",
|
||||
"version": "0.9.66",
|
||||
"description": "TeamChat backend server",
|
||||
"main": "src/index.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -195,6 +195,29 @@ router.patch('/:id', authMiddleware, teamManagerMiddleware, (req, res) => {
|
||||
req.params.id
|
||||
);
|
||||
if (Array.isArray(userGroupIds)) {
|
||||
// Find which groups are being removed
|
||||
const prevGroupIds = db.prepare('SELECT user_group_id FROM event_user_groups WHERE event_id = ?')
|
||||
.all(req.params.id).map(r => r.user_group_id);
|
||||
const newGroupSet = new Set(userGroupIds.map(Number));
|
||||
const removedGroupIds = prevGroupIds.filter(id => !newGroupSet.has(id));
|
||||
|
||||
// Remove availability responses for users who are only in removed groups
|
||||
for (const removedGid of removedGroupIds) {
|
||||
const removedUserIds = db.prepare('SELECT user_id FROM user_group_members WHERE user_group_id = ?')
|
||||
.all(removedGid).map(r => r.user_id);
|
||||
for (const uid of removedUserIds) {
|
||||
// Check if user is still in ANY remaining group for this event
|
||||
const stillAssigned = newGroupSet.size > 0 && db.prepare(`
|
||||
SELECT 1 FROM user_group_members
|
||||
WHERE user_id = ? AND user_group_id IN (${[...newGroupSet].map(()=>'?').join(',')})
|
||||
`).get(uid, ...[...newGroupSet]);
|
||||
if (!stillAssigned) {
|
||||
db.prepare('DELETE FROM event_availability WHERE event_id = ? AND user_id = ?')
|
||||
.run(req.params.id, uid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
db.prepare('DELETE FROM event_user_groups WHERE event_id = ?').run(req.params.id);
|
||||
for (const ugId of userGroupIds)
|
||||
db.prepare('INSERT OR IGNORE INTO event_user_groups (event_id, user_group_id) VALUES (?, ?)').run(req.params.id, ugId);
|
||||
|
||||
2
build.sh
2
build.sh
@@ -13,7 +13,7 @@
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
set -euo pipefail
|
||||
|
||||
VERSION="${1:-0.9.65}"
|
||||
VERSION="${1:-0.9.66}"
|
||||
ACTION="${2:-}"
|
||||
REGISTRY="${REGISTRY:-}"
|
||||
IMAGE_NAME="jama"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jama-frontend",
|
||||
"version": "0.9.65",
|
||||
"version": "0.9.66",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -170,6 +170,19 @@ function RecurrenceSheet({ value, onChange, onClose }) {
|
||||
);
|
||||
}
|
||||
|
||||
// ── Row — must be defined OUTSIDE the component to avoid focus loss ─────────────
|
||||
function MobileRow({ icon, label, children, onPress, border=true }) {
|
||||
return (
|
||||
<div onClick={onPress} style={{ display:'flex',alignItems:'center',gap:16,padding:'14px 20px',borderBottom:border?'1px solid var(--border)':'none',cursor:onPress?'pointer':'default',minHeight:52 }}>
|
||||
<span style={{ color:'var(--text-tertiary)',flexShrink:0,width:20,textAlign:'center' }}>{icon}</span>
|
||||
<div style={{ flex:1,minWidth:0 }}>
|
||||
{label && <div style={{ fontSize:12,color:'var(--text-tertiary)',marginBottom:2 }}>{label}</div>}
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Main Mobile Event Form ────────────────────────────────────────────────────
|
||||
export default function MobileEventForm({ event, eventTypes, userGroups, selectedDate, onSave, onCancel, onDelete, isToolManager }) {
|
||||
const toast = useToast();
|
||||
@@ -228,15 +241,7 @@ export default function MobileEventForm({ event, eventTypes, userGroups, selecte
|
||||
|
||||
const currentType = eventTypes.find(t=>t.id===Number(typeId));
|
||||
|
||||
const Row = ({ icon, label, children, onPress, value, border=true }) => (
|
||||
<div onClick={onPress} style={{ display:'flex',alignItems:'center',gap:16,padding:'14px 20px',borderBottom:border?'1px solid var(--border)':'none',cursor:onPress?'pointer':'default',minHeight:52 }}>
|
||||
<span style={{ color:'var(--text-tertiary)',flexShrink:0,width:20,textAlign:'center' }}>{icon}</span>
|
||||
<div style={{ flex:1,minWidth:0 }}>
|
||||
{label && <div style={{ fontSize:12,color:'var(--text-tertiary)',marginBottom:2 }}>{label}</div>}
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
return (
|
||||
<div style={{ display:'flex',flexDirection:'column',height:'100%',background:'var(--background)' }}>
|
||||
@@ -256,11 +261,11 @@ export default function MobileEventForm({ event, eventTypes, userGroups, selecte
|
||||
</div>
|
||||
|
||||
{/* Event Type */}
|
||||
<Row icon={<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M22 2H2l8 9.46V19l4 2v-8.54L22 2z"/></svg>} label="Event Type">
|
||||
<MobileRow icon={<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M22 2H2l8 9.46V19l4 2v-8.54L22 2z"/></svg>} label="Event Type">
|
||||
<select value={typeId} onChange={e=>setTypeId(e.target.value)} style={{ background:'transparent',border:'none',fontSize:15,color:'var(--text-primary)',width:'100%',outline:'none' }}>
|
||||
{eventTypes.map(t=><option key={t.id} value={t.id}>{t.name}</option>)}
|
||||
</select>
|
||||
</Row>
|
||||
</MobileRow>
|
||||
|
||||
{/* All-day toggle */}
|
||||
<div style={{ display:'flex',alignItems:'center',padding:'14px 20px',borderBottom:'1px solid var(--border)' }}>
|
||||
@@ -292,9 +297,9 @@ export default function MobileEventForm({ event, eventTypes, userGroups, selecte
|
||||
|
||||
|
||||
{/* Recurrence */}
|
||||
<Row icon={<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>} onPress={()=>setShowRecurrence(true)}>
|
||||
<MobileRow icon={<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>} onPress={()=>setShowRecurrence(true)}>
|
||||
<span style={{ fontSize:15 }}>{recurrenceLabel(recRule)}</span>
|
||||
</Row>
|
||||
</MobileRow>
|
||||
|
||||
{/* Track Availability */}
|
||||
<div style={{ display:'flex',alignItems:'center',padding:'14px 20px',borderBottom:'1px solid var(--border)' }}>
|
||||
@@ -326,14 +331,14 @@ export default function MobileEventForm({ event, eventTypes, userGroups, selecte
|
||||
</div>
|
||||
|
||||
{/* Location */}
|
||||
<Row icon={<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/></svg>}>
|
||||
<MobileRow icon={<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/></svg>}>
|
||||
<input value={location} onChange={e=>setLocation(e.target.value)} placeholder="Add location" style={{ width:'100%',border:'none',background:'transparent',fontSize:15,color:'var(--text-primary)',outline:'none' }}/>
|
||||
</Row>
|
||||
</MobileRow>
|
||||
|
||||
{/* Description */}
|
||||
<Row icon={<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><line x1="21" y1="10" x2="3" y2="10"/><line x1="21" y1="6" x2="3" y2="6"/><line x1="21" y1="14" x2="3" y2="14"/><line x1="21" y1="18" x2="3" y2="18"/></svg>} border={false}>
|
||||
<MobileRow icon={<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><line x1="21" y1="10" x2="3" y2="10"/><line x1="21" y1="6" x2="3" y2="6"/><line x1="21" y1="14" x2="3" y2="14"/><line x1="21" y1="18" x2="3" y2="18"/></svg>} border={false}>
|
||||
<textarea value={description} onChange={e=>setDescription(e.target.value)} placeholder="Add description" rows={3} style={{ width:'100%',border:'none',background:'transparent',fontSize:15,color:'var(--text-primary)',outline:'none',resize:'none' }}/>
|
||||
</Row>
|
||||
</MobileRow>
|
||||
|
||||
{/* Delete */}
|
||||
{event && isToolManager && (
|
||||
|
||||
@@ -905,9 +905,6 @@ export default function SchedulePage({ isToolManager, isMobile, onProfile, onHel
|
||||
{isToolManager && (
|
||||
<div style={{ position:'relative', marginBottom:12 }} ref={createRef}>
|
||||
<button className="newchat-btn" onClick={() => setCreateOpen(v=>!v)} style={{ width:'100%', justifyContent:'center', gap:8 }}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" width="18" height="18">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
||||
</svg>
|
||||
Create Event
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><polyline points="6 9 12 15 18 9"/></svg>
|
||||
</button>
|
||||
@@ -1019,10 +1016,7 @@ export default function SchedulePage({ isToolManager, isMobile, onProfile, onHel
|
||||
onSave={handleSaved} onCancel={()=>{setPanel('calendar');setEditingEvent(null);}} onDelete={handleDelete}/>
|
||||
</div>
|
||||
)}
|
||||
{panel === 'eventForm' && isToolManager && isMobile && (
|
||||
<MobileEventForm event={editingEvent} userGroups={userGroups} eventTypes={eventTypes} selectedDate={selDate} isToolManager={isToolManager}
|
||||
onSave={handleSaved} onCancel={()=>{setPanel('calendar');setEditingEvent(null);}} onDelete={handleDelete}/>
|
||||
)}
|
||||
|
||||
{panel === 'eventTypes' && isToolManager && (
|
||||
<div style={{ padding:28 }}>
|
||||
<div style={{ display:'flex', alignItems:'center', gap:10, marginBottom:24 }}>
|
||||
@@ -1051,6 +1045,22 @@ export default function SchedulePage({ isToolManager, isMobile, onProfile, onHel
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Mobile Event Form — full screen overlay, hides toolbar/date picker */}
|
||||
{panel === 'eventForm' && isToolManager && isMobile && (
|
||||
<div style={{ position:'fixed', top:0, left:0, right:0, bottom:0, zIndex:40, background:'var(--background)', display:'flex', flexDirection:'column' }}>
|
||||
<MobileEventForm
|
||||
event={editingEvent}
|
||||
userGroups={userGroups}
|
||||
eventTypes={eventTypes}
|
||||
selectedDate={selDate}
|
||||
isToolManager={isToolManager}
|
||||
onSave={handleSaved}
|
||||
onCancel={()=>{setPanel('calendar');setEditingEvent(null);}}
|
||||
onDelete={handleDelete}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Mobile FAB for creating events */}
|
||||
{isMobile && isToolManager && panel === 'calendar' && (
|
||||
<button onClick={()=>{setPanel('eventForm');setEditingEvent(null);}} style={{ position:'fixed',bottom:24,right:24,zIndex:30,width:56,height:56,borderRadius:'50%',background:'var(--primary)',color:'white',border:'none',cursor:'pointer',boxShadow:'0 4px 16px rgba(0,0,0,0.25)',display:'flex',alignItems:'center',justifyContent:'center' }}>
|
||||
|
||||
Reference in New Issue
Block a user