Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions frontend/app/(dashboard)/activity/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
'use client';

import { useState } from 'react';
import { Package, UserCheck, Wrench, AlertTriangle, Settings } from 'lucide-react';

type EventType = 'checkout' | 'return' | 'maintenance' | 'alert' | 'system';

interface ActivityEvent { id: string; type: EventType; actor: string; description: string; timestamp: string }

const MOCK: ActivityEvent[] = [
{ id:'1', type:'checkout', actor:'Alice M.', description:'checked out Dell Laptop #7', timestamp:'2024-01-22T09:15:00Z' },
{ id:'2', type:'maintenance', actor:'Tech Team', description:'completed maintenance on Server Rack A', timestamp:'2024-01-22T08:30:00Z' },
{ id:'3', type:'alert', actor:'System', description:'Printer Ink stock fell below reorder level', timestamp:'2024-01-22T07:45:00Z' },
{ id:'4', type:'return', actor:'Bob K.', description:'returned Canon Camera', timestamp:'2024-01-21T17:00:00Z' },
{ id:'5', type:'system', actor:'Admin', description:'updated permissions for Manager role', timestamp:'2024-01-21T14:20:00Z' },
];

const ICON: Record<EventType, React.ReactNode> = {
checkout: <Package size={16} className="text-blue-500"/>,
return: <UserCheck size={16} className="text-green-500"/>,
maintenance: <Wrench size={16} className="text-yellow-500"/>,
alert: <AlertTriangle size={16} className="text-red-500"/>,
system: <Settings size={16} className="text-gray-400"/>,
};

export default function ActivityFeedPage() {
const [events] = useState<ActivityEvent[]>(MOCK);
return (
<div>
<div className="mb-6"><h1 className="text-2xl font-bold text-gray-900">Activity Feed</h1><p className="text-sm text-gray-500 mt-1">Org-wide timeline of asset and system events</p></div>
<div className="relative">
<div className="absolute left-6 top-0 bottom-0 w-px bg-gray-200" />
<div className="space-y-4 pl-14">
{events.map(ev => (
<div key={ev.id} className="relative bg-white rounded-xl border border-gray-200 p-4">
<div className="absolute -left-8 top-4 flex items-center justify-center w-8 h-8 bg-white border border-gray-200 rounded-full">{ICON[ev.type]}</div>
<p className="text-sm text-gray-900"><span className="font-medium">{ev.actor}</span> {ev.description}</p>
<p className="text-xs text-gray-400 mt-1">{new Date(ev.timestamp).toLocaleString()}</p>
</div>
))}
</div>
</div>
</div>
);
}
77 changes: 77 additions & 0 deletions frontend/app/(dashboard)/asset-notes/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
'use client';

import { useState, useRef } from 'react';
import { Send } from 'lucide-react';
import { Button } from '@/components/ui/button';

const USERS = ['alice.m', 'bob.k', 'carol.s', 'dave.t', 'emma.w'];

interface Note { id: string; author: string; text: string; timestamp: string }

const MOCK_NOTES: Note[] = [
{ id:'1', author:'Alice M.', text:'Sent for repair. @bob.k please follow up with vendor.', timestamp:'2024-01-20T10:00:00Z' },
{ id:'2', author:'Bob K.', text:'Will check @carol.s for parts availability.', timestamp:'2024-01-20T10:30:00Z' },
];

export default function AssetNotesPage() {
const [notes, setNotes] = useState<Note[]>(MOCK_NOTES);
const [text, setText] = useState('');
const [suggestions, setSuggestions] = useState<string[]>([]);
const [mentionStart, setMentionStart] = useState(-1);
const inputRef = useRef<HTMLTextAreaElement>(null);

const handleChange = (val: string) => {
setText(val);
const atIdx = val.lastIndexOf('@');
if (atIdx >= 0 && atIdx === val.length - 1 - (val.slice(atIdx + 1).length) + atIdx) {
const fragment = val.slice(atIdx + 1);
const matches = USERS.filter(u => u.startsWith(fragment));
setSuggestions(matches);
setMentionStart(atIdx);
} else if (val.slice(val.lastIndexOf('@') + 1).includes(' ')) {
setSuggestions([]);
}
};

const insertMention = (user: string) => {
const newText = text.slice(0, mentionStart) + @ ;
setText(newText);
setSuggestions([]);
inputRef.current?.focus();
};

const addNote = () => {
if (!text.trim()) return;
setNotes(prev => [...prev, { id: Date.now().toString(), author:'You', text, timestamp: new Date().toISOString() }]);
setText('');
};

const renderText = (t: string) => t.replace(/@(\w+\.\w+)/g, '<span class="text-blue-600 font-medium">@</span>');

return (
<div>
<div className="mb-6"><h1 className="text-2xl font-bold text-gray-900">Asset Notes</h1><p className="text-sm text-gray-500 mt-1">@mention support with user autocomplete</p></div>
<div className="bg-white rounded-xl border border-gray-200 p-5 space-y-4 mb-4">
{notes.map(n => (
<div key={n.id} className="border-b border-gray-100 pb-3 last:border-0">
<div className="flex items-center justify-between mb-1">
<span className="text-sm font-medium text-gray-900">{n.author}</span>
<span className="text-xs text-gray-400">{new Date(n.timestamp).toLocaleString()}</span>
</div>
<p className="text-sm text-gray-700" dangerouslySetInnerHTML={{ __html: renderText(n.text) }} />
</div>
))}
</div>
<div className="relative">
<textarea ref={inputRef} value={text} onChange={e => handleChange(e.target.value)} placeholder="Add a note... type @ to mention a user" rows={3}
className="w-full text-sm border border-gray-300 rounded-lg p-3 focus:outline-none focus:ring-2 focus:ring-gray-900 resize-none" />
{suggestions.length > 0 && (
<div className="absolute z-10 bg-white border border-gray-200 rounded-lg shadow-sm mt-1 w-48">
{suggestions.map(u => <button key={u} onClick={() => insertMention(u)} className="w-full text-left px-3 py-2 text-sm hover:bg-gray-50">@{u}</button>)}
</div>
)}
<div className="mt-2 flex justify-end"><Button onClick={addNote}><Send size={14} className="mr-1.5"/>Post Note</Button></div>
</div>
</div>
);
}
Loading