DevicesView.tsx 17.5 KB
import React, { useState } from 'react';
import { Search, Plus, Link2, History, Upload, Download, Edit } from 'lucide-react';
import { Button } from '../ui/button';
import { Input } from '../ui/input';
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from '../ui/table';
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
} from '../ui/dialog';
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from '../ui/select';
import { Label } from '../ui/label';
import { Switch } from '../ui/switch';

// --- Types & Rules ---
// 1. 新增设备必须与 Location 绑定
// 2. Sensor(温度湿度传感器)可批量绑定/导入/导出/编辑
// 3. E-label(电子标签)可批量绑定/导入/导出/编辑
// 4. 后台可查看所有设备运行状况;打印机可查看剩余打印米数及充值记录
// 5. Printer、Scale 可批量导入/导出/编辑
// 6. 已创建捆绑的设备可编辑为 Active/Inactive

export type DeviceCategory = 'Printer' | 'Sensor' | 'Scale' | 'E-label';

export interface RechargeRecord {
  id: string;
  date: string;
  amount: string; // e.g. "100 meters"
  note?: string;
}

export interface Device {
  id: string;
  locationId: string;
  category: DeviceCategory;
  device: string;
  deviceId: string;
  balance: string; // 打印机剩余米数,其他类型为空
  status: 'Online' | 'Offline';
  active: boolean; // Active / Inactive
  rechargeRecords?: RechargeRecord[]; // 打印机充值记录
}

const MOCK_LOCATIONS = [
  { id: '12345', name: 'Location 12345 - Downtown' },
  { id: '67890', name: 'Location 67890 - Uptown' },
];

const CATEGORY_LABELS: Record<DeviceCategory, string> = {
  Printer: 'Printer',
  Sensor: 'Sensor (Temp. & Humidity)',
  Scale: 'Scale',
  'E-label': 'E-label (Electronic Label)',
};

const INITIAL_DEVICES: Device[] = [
  { id: '1', locationId: '12345', category: 'Printer', device: 'printer 1', deviceId: '123456789', balance: '51 meters', status: 'Online', active: true, rechargeRecords: [{ id: 'r1', date: '2025-01-15', amount: '200 meters', note: 'Initial' }, { id: 'r2', date: '2025-02-01', amount: '100 meters' }] },
  { id: '2', locationId: '12345', category: 'Printer', device: 'printer2', deviceId: '113456789', balance: '155 meters', status: 'Online', active: true, rechargeRecords: [{ id: 'r3', date: '2025-01-20', amount: '300 meters' }] },
  { id: '3', locationId: '12345', category: 'Sensor', device: 'sensor 1', deviceId: '103456789', balance: '', status: 'Online', active: true },
  { id: '4', locationId: '12345', category: 'Sensor', device: 'sensor 2', deviceId: '093456789', balance: '', status: 'Online', active: true },
  { id: '5', locationId: '12345', category: 'Sensor', device: 'sensor 3', deviceId: '083456789', balance: '', status: 'Online', active: false },
  { id: '6', locationId: '12345', category: 'Scale', device: 'Scale 1', deviceId: '073456789', balance: '', status: 'Online', active: true },
  { id: '7', locationId: '12345', category: 'E-label', device: 'e-label 1', deviceId: '063456789', balance: '', status: 'Online', active: true },
  { id: '8', locationId: '12345', category: 'E-label', device: 'e-label 2', deviceId: '053456789', balance: '', status: 'Online', active: true },
  { id: '9', locationId: '12345', category: 'E-label', device: 'e-label 3', deviceId: '043456789', balance: '', status: 'Offline', active: true },
  { id: '10', locationId: '12345', category: 'E-label', device: 'e-label 4', deviceId: '033456789', balance: '', status: 'Offline', active: false },
  { id: '11', locationId: '12345', category: 'E-label', device: 'e-label 5', deviceId: '023456789', balance: '', status: 'Online', active: true },
  { id: '12', locationId: '12345', category: 'E-label', device: 'e-label 6', deviceId: '013456789', balance: '', status: 'Online', active: true },
  { id: '13', locationId: '12345', category: 'E-label', device: 'e-label 7', deviceId: '003456789', balance: '', status: 'Online', active: true },
];

export function DevicesView() {
  const [search, setSearch] = useState('');
  const [devices, setDevices] = useState<Device[]>(INITIAL_DEVICES);
  const [filterCategory, setFilterCategory] = useState<string>('all');
  const [newDialogOpen, setNewDialogOpen] = useState(false);
  const [rechargeDialogDevice, setRechargeDialogDevice] = useState<Device | null>(null);

  // 新增设备:必须与 Location 绑定
  const [newLocationId, setNewLocationId] = useState('');
  const [newCategory, setNewCategory] = useState<DeviceCategory>('Printer');
  const [newDeviceName, setNewDeviceName] = useState('');
  const [newDeviceId, setNewDeviceId] = useState('');

  const filteredDevices = devices.filter((d) => {
    const matchSearch = !search || d.device.toLowerCase().includes(search.toLowerCase()) || d.deviceId.includes(search);
    const matchCategory = filterCategory === 'all' || d.category === filterCategory;
    return matchSearch && matchCategory;
  });

  const handleAddDevice = () => {
    if (!newLocationId.trim()) return; // 规则1:必须选择 Location
    const loc = MOCK_LOCATIONS.find((l) => l.id === newLocationId);
    setDevices((prev) => [
      ...prev,
      {
        id: String(prev.length + 1),
        locationId: newLocationId,
        category: newCategory,
        device: newDeviceName || `${newCategory} ${prev.filter((d) => d.category === newCategory).length + 1}`,
        deviceId: newDeviceId || String(100000000 + prev.length),
        balance: newCategory === 'Printer' ? '0 meters' : '',
        status: 'Online',
        active: true,
        rechargeRecords: newCategory === 'Printer' ? [] : undefined,
      },
    ]);
    setNewLocationId('');
    setNewCategory('Printer');
    setNewDeviceName('');
    setNewDeviceId('');
    setNewDialogOpen(false);
  };

  const toggleActive = (id: string) => {
    setDevices((prev) => prev.map((d) => (d.id === id ? { ...d, active: !d.active } : d)));
  };

  const canBulkBind = filterCategory === 'Sensor' || filterCategory === 'E-label';
  const canBulkImportExportEdit = ['Printer', 'Scale', 'Sensor', 'E-label'].includes(filterCategory) || filterCategory === 'all';

  return (
    <div className="h-full flex flex-col bg-white">
      {/* Toolbar:与 Products 一致样式 — Search 在前,Partner/Group/Location/Device Category 下拉,右侧 Bulk 与 New+ */}
      <div className="border-b border-gray-200 py-4 bg-gray-50">
        <div className="flex flex-nowrap items-center gap-2 overflow-x-auto min-w-0">
          <div className="relative w-32 shrink-0">
            <Search className="absolute left-2.5 top-2.5 h-4 w-4 text-gray-400" />
            <Input
              placeholder="Search"
              value={search}
              onChange={(e) => setSearch(e.target.value)}
              className="pl-9 h-9 rounded-lg border border-black bg-white text-black"
            />
          </div>
          <Select defaultValue="partner-a">
            <SelectTrigger className="w-[140px] h-9 rounded-lg border border-black font-medium text-black bg-white shrink-0">
              <SelectValue placeholder="Partner" />
            </SelectTrigger>
            <SelectContent>
              <SelectItem value="partner-a">Partner A</SelectItem>
            </SelectContent>
          </Select>
          <Select defaultValue="group-b">
            <SelectTrigger className="w-[140px] h-9 rounded-lg border border-black font-medium text-black bg-white shrink-0">
              <SelectValue placeholder="Group" />
            </SelectTrigger>
            <SelectContent>
              <SelectItem value="group-b">Group B</SelectItem>
            </SelectContent>
          </Select>
          <Select defaultValue="loc-12345">
            <SelectTrigger className="w-[160px] h-9 rounded-lg border border-black font-medium text-black bg-white shrink-0">
              <SelectValue placeholder="Location" />
            </SelectTrigger>
            <SelectContent>
              <SelectItem value="loc-12345">Location 12345</SelectItem>
              <SelectItem value="67890">Location 67890 - Uptown</SelectItem>
            </SelectContent>
          </Select>
          <Select value={filterCategory} onValueChange={setFilterCategory}>
            <SelectTrigger className="w-[180px] h-9 rounded-lg border border-black font-medium text-black bg-white shrink-0">
              <SelectValue placeholder="Device Category" />
            </SelectTrigger>
            <SelectContent>
              <SelectItem value="all">Device Category (All)</SelectItem>
              <SelectItem value="Printer">Printer</SelectItem>
              <SelectItem value="Sensor">Sensor (Temp. & Humidity)</SelectItem>
              <SelectItem value="Scale">Scale</SelectItem>
              <SelectItem value="E-label">E-label (Electronic Label)</SelectItem>
            </SelectContent>
          </Select>
          <div className="flex-1 min-w-0" />
          {canBulkBind && (
            <Button variant="outline" className="h-9 rounded-lg border border-black text-black gap-2 bg-white hover:bg-gray-50 shrink-0">
              <Link2 className="w-4 h-4" /> Bulk Bind
            </Button>
          )}
          {canBulkImportExportEdit && (
            <>
              <Button variant="outline" className="h-9 rounded-lg border border-black text-black gap-2 bg-white hover:bg-gray-50 shrink-0">
                <Upload className="w-4 h-4" /> Bulk Import
              </Button>
              <Button variant="outline" className="h-9 rounded-lg border border-black text-black gap-2 bg-white hover:bg-gray-50 shrink-0">
                <Download className="w-4 h-4" /> Bulk Export
              </Button>
              <Button variant="outline" className="h-9 rounded-lg border border-black text-black gap-2 bg-white hover:bg-gray-50 shrink-0">
                <Edit className="w-4 h-4" /> Bulk Edit
              </Button>
            </>
          )}
          <Button className="h-9 rounded-lg bg-blue-600 text-white hover:bg-blue-700 font-medium gap-1 border-0 shrink-0" onClick={() => setNewDialogOpen(true)}>
            New+ <Plus className="w-4 h-4" />
          </Button>
        </div>
      </div>

      {/* Content Area:与 Products 一致 — 无内边距、bg-gray-50,表格包在白色圆角框内 */}
      <div className="flex-1 overflow-auto bg-gray-50">
        <div className="bg-white border border-gray-200 shadow-sm rounded-sm overflow-hidden">
          <Table>
            <TableHeader>
              <TableRow className="bg-gray-100 hover:bg-gray-100">
                <TableHead className="font-bold text-gray-900 border-r">Location ID</TableHead>
                <TableHead className="font-bold text-gray-900 border-r">Device Category</TableHead>
                <TableHead className="font-bold text-gray-900 border-r">Device</TableHead>
                <TableHead className="font-bold text-gray-900 border-r">Device ID</TableHead>
                <TableHead className="font-bold text-gray-900 border-r">Balance</TableHead>
                <TableHead className="font-bold text-gray-900 border-r">Recharge</TableHead>
                <TableHead className="font-bold text-gray-900 border-r">Running Status</TableHead>
                <TableHead className="font-bold text-gray-900 text-center">Active</TableHead>
              </TableRow>
            </TableHeader>
          <TableBody>
            {filteredDevices.map((row) => (
              <TableRow key={row.id} className="hover:bg-gray-50">
                <TableCell className="text-gray-700">{row.locationId}</TableCell>
                <TableCell>
                  <span className={row.category === 'Printer' ? 'text-red-600' : 'text-gray-700'}>
                    {CATEGORY_LABELS[row.category]}
                  </span>
                </TableCell>
                <TableCell>
                  <span className={row.category === 'Printer' ? 'text-red-600' : 'text-gray-700'}>
                    {row.device}
                  </span>
                </TableCell>
                <TableCell className="text-gray-700">{row.deviceId}</TableCell>
                <TableCell className="text-gray-700">{row.balance || '—'}</TableCell>
                <TableCell>
                  {row.category === 'Printer' ? (
                    <Button
                      variant="ghost"
                      size="sm"
                      className="h-8 text-blue-600 hover:text-blue-800"
                      onClick={() => setRechargeDialogDevice(row)}
                    >
                      <History className="w-4 h-4 mr-1" />
                      View
                    </Button>
                  ) : (
                    '—'
                  )}
                </TableCell>
                <TableCell>
                  <span className={row.status === 'Offline' ? 'text-red-600' : 'text-green-600'}>
                    {row.status}
                  </span>
                </TableCell>
                <TableCell>
                  <div className="flex items-center gap-2">
                    <Switch
                      checked={row.active}
                      onCheckedChange={() => toggleActive(row.id)}
                    />
                    <span className="text-sm text-gray-600">{row.active ? 'Active' : 'Inactive'}</span>
                  </div>
                </TableCell>
              </TableRow>
            ))}
          </TableBody>
        </Table>
        </div>
      </div>

      {/* New Device Dialog - 规则1:必须与 Location 绑定 */}
      <Dialog open={newDialogOpen} onOpenChange={setNewDialogOpen}>
        <DialogContent className="sm:max-w-md">
          <DialogHeader>
            <DialogTitle>Add New Device</DialogTitle>
            <DialogDescription>
              New devices must be bound to a location. Select a location first, then fill in device details.
            </DialogDescription>
          </DialogHeader>
          <div className="grid gap-4 py-4">
            <div className="grid gap-2">
              <Label>Location *</Label>
              <Select value={newLocationId} onValueChange={setNewLocationId} required>
                <SelectTrigger>
                  <SelectValue placeholder="Select location (required)" />
                </SelectTrigger>
                <SelectContent>
                  {MOCK_LOCATIONS.map((loc) => (
                    <SelectItem key={loc.id} value={loc.id}>
                      {loc.name}
                    </SelectItem>
                  ))}
                </SelectContent>
              </Select>
            </div>
            <div className="grid gap-2">
              <Label>Device Category</Label>
              <Select value={newCategory} onValueChange={(v) => setNewCategory(v as DeviceCategory)}>
                <SelectTrigger>
                  <SelectValue />
                </SelectTrigger>
                <SelectContent>
                  {(Object.keys(CATEGORY_LABELS) as DeviceCategory[]).map((cat) => (
                    <SelectItem key={cat} value={cat}>
                      {CATEGORY_LABELS[cat]}
                    </SelectItem>
                  ))}
                </SelectContent>
              </Select>
            </div>
            <div className="grid gap-2">
              <Label>Device Name</Label>
              <Input
                placeholder="e.g. printer 1"
                value={newDeviceName}
                onChange={(e) => setNewDeviceName(e.target.value)}
              />
            </div>
            <div className="grid gap-2">
              <Label>Device ID</Label>
              <Input
                placeholder="e.g. 123456789"
                value={newDeviceId}
                onChange={(e) => setNewDeviceId(e.target.value)}
              />
            </div>
          </div>
          <DialogFooter>
            <Button variant="outline" onClick={() => setNewDialogOpen(false)}>
              Cancel
            </Button>
            <Button onClick={handleAddDevice} disabled={!newLocationId}>
              Add Device
            </Button>
          </DialogFooter>
        </DialogContent>
      </Dialog>

      {/* Printer Recharge Records Dialog - 规则4 */}
      <Dialog open={!!rechargeDialogDevice} onOpenChange={(open) => !open && setRechargeDialogDevice(null)}>
        <DialogContent className="sm:max-w-md">
          <DialogHeader>
            <DialogTitle>Recharge Records — {rechargeDialogDevice?.device}</DialogTitle>
            <DialogDescription>
              Balance: {rechargeDialogDevice?.balance ?? '—'}
            </DialogDescription>
          </DialogHeader>
          <div className="py-2 max-h-64 overflow-y-auto">
            {rechargeDialogDevice?.rechargeRecords?.length ? (
              <Table>
                <TableHeader>
                  <TableRow>
                    <TableHead>Date</TableHead>
                    <TableHead>Amount</TableHead>
                    <TableHead>Note</TableHead>
                  </TableRow>
                </TableHeader>
                <TableBody>
                  {rechargeDialogDevice.rechargeRecords.map((r) => (
                    <TableRow key={r.id}>
                      <TableCell>{r.date}</TableCell>
                      <TableCell>{r.amount}</TableCell>
                      <TableCell>{r.note ?? '—'}</TableCell>
                    </TableRow>
                  ))}
                </TableBody>
              </Table>
            ) : (
              <p className="text-sm text-gray-500">No recharge records yet.</p>
            )}
          </div>
          <DialogFooter>
            <Button variant="outline" onClick={() => setRechargeDialogDevice(null)}>
              Close
            </Button>
          </DialogFooter>
        </DialogContent>
      </Dialog>
    </div>
  );
}