QRCodesView.tsx 10.5 KB
import React, { useState, useCallback } from 'react';
import { QRCodeSVG } from 'qrcode.react';
import { Download, Plus } from 'lucide-react';
import { Button } from '../ui/button';
import { Input } from '../ui/input';
import { Label } from '../ui/label';
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from '../ui/select';
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from '../ui/table';
import { cn } from '../ui/utils';

// Notes:
// 1. Enter quantity to generate the corresponding number of QR codes
// 2. QR code size can be selected
// 3. Each generated QR has a numeric ID for manual entry when scanning is not available (e.g. recharge)
// 4. Generated QR codes can be bulk exported for batch printing
// 5. QR report shows generation status and usage status

export type QRSize = 'small' | 'medium' | 'large';

const SIZE_OPTIONS: { value: QRSize; label: string; pixels: number }[] = [
  { value: 'small', label: 'Small (80px)', pixels: 80 },
  { value: 'medium', label: 'Medium (120px)', pixels: 120 },
  { value: 'large', label: 'Large (160px)', pixels: 160 },
];

export type QRUsageStatus = 'Unused' | 'Used';

export interface QRCodeItem {
  id: string;
  numericId: string; // For manual entry when scan doesn't work (e.g. recharge)
  value: string;    // Encoded in QR (same as numericId or extended code)
  usageStatus: QRUsageStatus;
  createdAt: string;
}

const PREFIX = 'QR';
let nextNumericId = 100001;

function generateNumericId(): string {
  return String(nextNumericId++);
}

export function QRCodesView() {
  const [quantity, setQuantity] = useState<number>(10);
  const [sizeOption, setSizeOption] = useState<QRSize>('medium');
  const [items, setItems] = useState<QRCodeItem[]>([]);
  const [activeTab, setActiveTab] = useState<'generate' | 'report'>('generate');

  const pixels = SIZE_OPTIONS.find((s) => s.value === sizeOption)?.pixels ?? 120;

  const handleGenerate = useCallback(() => {
    const count = Math.min(Math.max(1, Math.floor(quantity) || 0), 500);
    const newItems: QRCodeItem[] = [];
    for (let i = 0; i < count; i++) {
      const numericId = generateNumericId();
      newItems.push({
        id: `${PREFIX}-${numericId}`,
        numericId,
        value: numericId,
        usageStatus: 'Unused',
        createdAt: new Date().toISOString().slice(0, 10),
      });
    }
    setItems((prev) => [...prev, ...newItems]);
  }, [quantity]);

  const [showPrintView, setShowPrintView] = useState(false);

  const handleBulkExportPrint = useCallback(() => {
    if (items.length === 0) return;
    setShowPrintView(true);
  }, [items.length]);

  const handlePrint = useCallback(() => {
    window.print();
  }, []);

  const handleClosePrintView = useCallback(() => {
    setShowPrintView(false);
  }, []);

  const setUsageStatus = useCallback((id: string, status: QRUsageStatus) => {
    setItems((prev) => prev.map((item) => (item.id === id ? { ...item, usageStatus: status } : item)));
  }, []);

  return (
    <div className="h-full flex flex-col bg-white">
      {/* Print-only view: full grid of QRs for batch printing */}
      {showPrintView && (
        <div className="fixed inset-0 z-50 bg-white p-6 overflow-auto print:block">
          <div className="flex flex-wrap items-center justify-between gap-4 mb-4 no-print">
            <h2 className="text-lg font-semibold">QR Codes – Batch Print</h2>
            <div className="flex gap-2">
              <Button onClick={handlePrint}>Print</Button>
              <Button variant="outline" onClick={handleClosePrintView}>Close</Button>
            </div>
          </div>
          <div className="grid grid-cols-4 gap-6 print:grid-cols-4" style={{ breakInside: 'avoid' }}>
            {items.map((item) => (
              <div key={item.id} className="text-center break-inside-avoid p-2 border border-gray-200 rounded">
                <QRCodeSVG value={item.value} size={pixels} level="M" includeMargin={false} className="mx-auto" />
                <div className="mt-2 text-sm font-semibold">ID: {item.numericId}</div>
              </div>
            ))}
          </div>
        </div>
      )}

      {/* Tabs:与 Labels/Reports 一致 */}
      <div className="flex overflow-x-auto border-b border-gray-200 bg-gray-50 print:hidden">
        {(['generate', 'report'] as const).map((tab) => (
          <button
            key={tab}
            type="button"
            onClick={() => setActiveTab(tab)}
            className={cn(
              'px-6 py-3 text-sm font-medium whitespace-nowrap cursor-pointer transition-colors',
              activeTab === tab ? 'border-b-2' : 'text-gray-500 hover:text-gray-700'
            )}
            style={activeTab === tab ? { color: '#1b46c7', borderBottomColor: '#1b46c7' } : undefined}
          >
            {tab === 'generate' ? 'Generate & Export' : 'QR Report'}
          </button>
        ))}
      </div>

      {/* Content Area:上下左右间距与 Products/Locations 一致(由 Layout main p-8 控制) */}
      <div className="flex-1 overflow-auto bg-gray-50 print:hidden">
          {activeTab === 'generate' && (
          <div>
            {/* 1. Quantity input and size selection — 圆角、细边框 */}
            <div className="flex flex-wrap items-end gap-4 mt-4 mb-6 p-4 bg-white border border-gray-200 rounded-lg shadow-sm">
              <div className="grid gap-2">
                <Label>Quantity</Label>
                <Input
                  type="number"
                  min={1}
                  max={500}
                  value={quantity}
                  onChange={(e) => setQuantity(Number(e.target.value) || 0)}
                  className="w-32 rounded-lg border border-black h-9"
                />
              </div>
              <div className="grid gap-2">
                <Label>QR Code Size</Label>
                <Select value={sizeOption} onValueChange={(v) => setSizeOption(v as QRSize)}>
                  <SelectTrigger className="w-[180px] h-9 rounded-lg border border-black">
                    <SelectValue />
                  </SelectTrigger>
                  <SelectContent>
                    {SIZE_OPTIONS.map((opt) => (
                      <SelectItem key={opt.value} value={opt.value}>
                        {opt.label}
                      </SelectItem>
                    ))}
                  </SelectContent>
                </Select>
              </div>
              <Button onClick={handleGenerate} className="h-9 rounded-lg bg-green-600 hover:bg-green-700 border-0">
                <Plus className="w-4 h-4 mr-1" />
                Generate
              </Button>
            </div>

            {/* 2 & 3. Generated QRs; 4. Bulk export */}
            <div className="flex flex-wrap items-center justify-between gap-4 mb-4">
              <span className="text-sm text-gray-600">
                {items.length} QR code(s) generated. Each has a numeric ID for manual entry when scanning is not available.
              </span>
              {items.length > 0 && (
                <Button variant="outline" className="h-9 rounded-lg border border-black text-black gap-2 bg-white hover:bg-gray-50" onClick={handleBulkExportPrint}>
                  <Download className="w-4 h-4" /> Bulk Export for Print
                </Button>
              )}
            </div>

            {items.length > 0 && (
              <div className="grid grid-cols-2 sm:grid-cols-4 md:grid-cols-6 gap-4">
                {items.map((item) => (
                  <div
                    key={item.id}
                    className="border rounded-lg p-3 bg-white shadow-sm text-center"
                  >
                    <QRCodeSVG value={item.value} size={pixels} level="M" includeMargin={false} className="mx-auto" />
                    <div className="mt-2 text-sm font-semibold text-gray-700">ID: {item.numericId}</div>
                    <div className="text-xs text-gray-500">{item.usageStatus}</div>
                  </div>
                ))}
              </div>
            )}
          </div>
          )}

          {activeTab === 'report' && (
          <div>
            <p className="text-sm text-gray-600 mb-4">
              QR code generation and usage status. Use this report to see which codes have been used (e.g. after recharge or scan).
            </p>
            <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">QR ID</TableHead>
                    <TableHead className="font-bold text-gray-900 border-r">Generation Status</TableHead>
                    <TableHead className="font-bold text-gray-900 border-r">Usage Status</TableHead>
                    <TableHead className="font-bold text-gray-900 text-center">Date Generated</TableHead>
                  </TableRow>
                </TableHeader>
                <TableBody>
                  {items.length === 0 ? (
                    <TableRow>
                      <TableCell colSpan={4} className="text-center text-gray-500 py-8">
                        No QR codes generated yet. Go to Generate & Export and enter a quantity, then click Generate.
                      </TableCell>
                    </TableRow>
                  ) : (
                    items.map((row) => (
                      <TableRow key={row.id} className="hover:bg-gray-50">
                        <TableCell className="font-mono border-r">{row.numericId}</TableCell>
                        <TableCell className="border-r">Generated</TableCell>
                        <TableCell className="border-r">
                          <span className={row.usageStatus === 'Used' ? 'text-green-600' : 'text-gray-600'}>
                            {row.usageStatus}
                          </span>
                          <Button
                            variant="ghost"
                            size="sm"
                            className="ml-2 h-7 text-xs rounded-lg"
                            onClick={() => setUsageStatus(row.id, row.usageStatus === 'Used' ? 'Unused' : 'Used')}
                          >
                            {row.usageStatus === 'Used' ? 'Mark Unused' : 'Mark Used'}
                          </Button>
                        </TableCell>
                        <TableCell>{row.createdAt}</TableCell>
                      </TableRow>
                    ))
                  )}
                </TableBody>
              </Table>
            </div>
          </div>
          )}
      </div>
    </div>
  );
}