3import { useState, useEffect } from 'react';
4import Link from 'next/link';
7 f100: string; // Account ID
8 f109: string; // Account Name
9 f110: string; // Type (SMS, SMTP, POP3, etc)
10 f120: number; // Status flags (1=Hold)
11 f120s?: string; // Status string (Active/Hold)
12 f120s_bkcolor?: string; // Background color
13 f101?: string; // Server/Config
14 f102?: string; // Port
15 f103?: string; // Username/Email
16 f104?: string; // Password
17 f130?: string; // SMS Provider type
20 TRCE?: Array<{ f110: string }>;
24interface SmsProvider {
28 requiresEmail: boolean;
31const SMS_PROVIDERS: SmsProvider[] = [
32 { value: '0', label: '--Select One', description: '', requiresEmail: false },
33 { value: '2', label: 'Fieldpine Gateway', description: 'Fieldpine provide a general purpose gateway for most countries. See Fieldpine SMS for pricing and further details', requiresEmail: false },
34 { value: '1', label: 'Email Gateway', description: 'Enter the details of the email address to be sent too. This option requires an SMTP (Send Email) account.', requiresEmail: true },
35 { value: '6401', label: 'NZ. Spark eTxt', description: 'Configure the SMS setup after you have registered for Spark eTxt and can successfully send Txt messages from your email account outside the POS. This option requires an SMTP (Send Email) account.', requiresEmail: false },
38export default function TxtOptionsPage() {
39 const [accounts, setAccounts] = useState<SmsAccount[]>([]);
40 const [loading, setLoading] = useState(true);
41 const [error, setError] = useState('');
44 const [showNewAccountModal, setShowNewAccountModal] = useState(false);
45 const [showTestModal, setShowTestModal] = useState(false);
46 const [showLogModal, setShowLogModal] = useState(false);
47 const [showControlPanelModal, setShowControlPanelModal] = useState(false);
49 // Selected account for modals
50 const [selectedAccount, setSelectedAccount] = useState<SmsAccount | null>(null);
53 const [accountType, setAccountType] = useState<'SMTP' | 'POP3' | 'UPLOAD' | 'FTP' | 'SMS'>('SMS');
54 const [newAccountForm, setNewAccountForm] = useState({
66 const [testTarget, setTestTarget] = useState('');
67 const [testMessage, setTestMessage] = useState('This is a test message to verify the system is configured correctly. You can delete this message.');
70 const [controlPanelStatus, setControlPanelStatus] = useState('Contacting Server, please wait...');
74 const interval = setInterval(loadAccounts, 30000); // Refresh every 30 seconds
75 return () => clearInterval(interval);
78 const loadAccounts = async () => {
80 const response = await fetch('/api/v1/messaging/config');
82 if (!response.ok || !response.headers.get("content-type")?.includes("application/json")) {
83 setError('⚠️ Messaging Config API not yet implemented - UI demonstration mode');
87 f109: 'Demo SMS Account',
91 f120s_bkcolor: '#00ff00',
93 STAT: [{ f120: 'Connected and ready' }]
100 const data = await response.json();
103 const smsAccounts = (data.DATS || [])
104 .filter((acc: SmsAccount) => acc.f110 === 'SMS')
105 .map((acc: SmsAccount) => {
106 const status = acc.f120 || 0;
107 acc.f120s = (status & 1) === 1 ? 'Hold' : 'Active';
108 acc.f120s_bkcolor = (status & 1) === 1 ? '#ff8080' : '#00ff00';
112 setAccounts(smsAccounts);
115 console.error('Error loading accounts:', err);
116 setError('Failed to load accounts. Using demo mode.');
120 f109: 'Demo SMS Account',
124 f120s_bkcolor: '#00ff00',
126 STAT: [{ f120: 'Connected and ready' }]
134 const createNewAccount = async () => {
136 const payload: any = {
137 f8_s: 'messaging.config.edit',
139 f109_s: newAccountForm.name,
143 if (accountType === 'SMTP' || accountType === 'POP3' || accountType === 'FTP') {
144 payload.f101_s = newAccountForm.server;
145 payload.f102_s = newAccountForm.port;
146 payload.f103_s = newAccountForm.username;
147 payload.f104_s = newAccountForm.password;
148 } else if (accountType === 'UPLOAD') {
149 payload.f101_s = newAccountForm.directory;
150 } else if (accountType === 'SMS') {
151 payload.f103_s = newAccountForm.smsEmail;
152 payload.f130_E = newAccountForm.smsProvider;
155 const response = await fetch('/api/v1/messaging/config', {
157 headers: { 'Content-Type': 'application/json' },
158 body: JSON.stringify(payload),
161 if (!response.ok || !response.headers.get("content-type")?.includes("application/json")) {
162 alert('⚠️ Account creation API not yet implemented. In production, this would create the account.');
164 await response.json();
165 alert('Account created successfully');
169 setTimeout(loadAccounts, 100);
171 console.error('Error creating account:', err);
172 alert('Failed to create account');
176 const sendTestMessage = async () => {
177 if (!selectedAccount || !testTarget || !testMessage) return;
180 const response = await fetch('/api/v1/messaging/config', {
182 headers: { 'Content-Type': 'application/json' },
183 body: JSON.stringify({
184 f8_s: 'messaging.config.edit',
186 f100_s: selectedAccount.f100,
192 if (!response.ok || !response.headers.get("content-type")?.includes("application/json")) {
193 alert('⚠️ Test message API not yet implemented. In production, this would send: "' + testMessage.substring(0, 50) + '..." to ' + testTarget);
195 await response.json();
196 alert('Test message queued successfully');
201 console.error('Error sending test:', err);
202 alert('Failed to send test message');
206 const toggleAccountStatus = async (account: SmsAccount, holdStatus: number) => {
208 const response = await fetch('/api/v1/messaging/config', {
210 headers: { 'Content-Type': 'application/json' },
211 body: JSON.stringify({
212 f8_s: 'messaging.config.edit',
214 f100_s: account.f100,
219 if (!response.ok || !response.headers.get("content-type")?.includes("application/json")) {
220 alert(`⚠️ Status change API not yet implemented. In production, this would ${holdStatus === 1 ? 'hold' : 'activate'} the account.`);
222 await response.json();
225 setTimeout(loadAccounts, 100);
227 console.error('Error toggling status:', err);
228 alert('Failed to update account status');
232 const openControlPanel = async (account: SmsAccount) => {
233 setSelectedAccount(account);
234 setShowControlPanelModal(true);
235 setControlPanelStatus('Contacting Server, please wait...');
238 const response = await fetch('/api/v1/messaging/sms/control', {
240 headers: { 'Content-Type': 'application/json' },
241 body: JSON.stringify({
242 f8_s: 'messaging.sms.control',
244 f100_s: account.f100,
249 if (!response.ok || !response.headers.get("content-type")?.includes("application/json")) {
250 setControlPanelStatus('⚠️ Control panel API not yet implemented. In production, this would open the SMS provider control panel.');
254 const data = await response.json();
255 const url = data.f130 || '';
257 if (url.length > 8) {
258 window.open(url, '_blank');
261 setControlPanelStatus('Unable to connect to server at this time. Please try again later');
264 console.error('Error opening control panel:', err);
265 setControlPanelStatus('Error connecting to control panel');
269 const closeAllModals = () => {
270 setShowNewAccountModal(false);
271 setShowTestModal(false);
272 setShowLogModal(false);
273 setShowControlPanelModal(false);
274 setSelectedAccount(null);
287 const openTestModal = (account: SmsAccount) => {
288 setSelectedAccount(account);
289 setShowTestModal(true);
292 const openLogModal = (account: SmsAccount) => {
293 setSelectedAccount(account);
294 setShowLogModal(true);
297 const selectedProvider = SMS_PROVIDERS.find(p => p.value === newAccountForm.smsProvider);
301 <div className="min-h-screen bg-gray-50 p-8">
302 <div className="max-w-7xl mx-auto">
303 <div className="text-center py-12">
304 <div className="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-[#00946b]"></div>
305 <p className="mt-2 text-gray-600">Loading accounts...</p>
313 <div className="min-h-screen bg-gray-50 p-8">
314 <div className="max-w-7xl mx-auto">
316 <div className="mb-8">
317 <div className="flex items-center justify-between mb-4">
319 <Link href="/marketing/messaging" className="text-[#00946b] hover:text-[#007055] mb-2 inline-block">
322 <h1 className="text-3xl font-bold text-gray-900">💬 Txt Messaging Options</h1>
325 onClick={() => setShowNewAccountModal(true)}
326 className="bg-[#00946b] text-white px-6 py-3 rounded-lg hover:bg-[#007055] font-medium transition-colors"
333 <div className="bg-yellow-50 border-l-4 border-yellow-400 p-4 mb-4">
334 <p className="text-yellow-700">{error}</p>
339 {/* Accounts Table */}
340 <div className="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
341 <div className="px-6 py-4 border-b border-gray-200">
342 <h2 className="text-xl font-semibold text-gray-900">Current SMS Accounts</h2>
345 <div className="overflow-x-auto">
346 <table className="min-w-full divide-y divide-gray-200">
347 <thead className="bg-gray-50">
349 <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Account Name</th>
350 <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Type</th>
351 <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
352 <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Current State</th>
353 <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
356 <tbody className="bg-white divide-y divide-gray-200">
357 {accounts.length === 0 ? (
359 <td colSpan={5} className="px-6 py-8 text-center text-gray-500">
360 No SMS accounts configured. Click "Add New Account" to create one.
364 accounts.map((account) => (
365 <tr key={account.f100} className="hover:bg-gray-50">
366 <td className="px-6 py-4 whitespace-nowrap">
367 <div className="text-sm font-medium text-gray-900">{account.f109}</div>
369 <td className="px-6 py-4 whitespace-nowrap">
370 <div className="text-sm text-gray-700">{account.f110}</div>
372 <td className="px-6 py-4 whitespace-nowrap">
374 className="px-3 py-1 inline-flex text-xs leading-5 font-semibold rounded-full"
375 style={{ backgroundColor: account.f120s_bkcolor, color: account.f120s === 'Active' ? '#000' : '#fff' }}
380 <td className="px-6 py-4">
381 <div className="text-sm text-gray-700">{account.STAT?.[0]?.f120 || 'Ready'}</div>
383 <td className="px-6 py-4 whitespace-nowrap text-sm">
384 <div className="flex flex-wrap gap-2">
386 onClick={() => openControlPanel(account)}
387 className="text-[#00946b] hover:text-[#007055] font-medium border border-[#00946b] px-3 py-1 rounded hover:bg-[#00946b] hover:text-white transition-colors"
392 onClick={() => openTestModal(account)}
393 className="text-blue-600 hover:text-blue-800 font-medium border border-blue-600 px-3 py-1 rounded hover:bg-blue-600 hover:text-white transition-colors"
398 onClick={() => openLogModal(account)}
399 className="text-gray-600 hover:text-gray-800 font-medium border border-gray-400 px-3 py-1 rounded hover:bg-gray-600 hover:text-white transition-colors"
403 {account.f120s === 'Active' ? (
405 onClick={() => toggleAccountStatus(account, 1)}
406 className="text-orange-600 hover:text-orange-800 font-medium border border-orange-600 px-3 py-1 rounded hover:bg-orange-600 hover:text-white transition-colors"
412 onClick={() => toggleAccountStatus(account, 0)}
413 className="text-green-600 hover:text-green-800 font-medium border border-green-600 px-3 py-1 rounded hover:bg-green-600 hover:text-white transition-colors"
429 {/* New Account Modal */}
430 {showNewAccountModal && (
431 <div className="modal-overlay" onClick={closeAllModals}>
432 <div className="bg-white rounded-lg shadow-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto" onClick={(e) => e.stopPropagation()}>
433 <div className="bg-[#00946b] text-white px-6 py-4 rounded-t-lg flex justify-between items-center">
434 <h2 className="text-xl font-semibold">Add New Messaging Account</h2>
435 <button onClick={closeAllModals} className="text-white hover:text-gray-200 text-2xl">×</button>
438 <div className="p-6">
439 <div className="mb-6">
440 <p className="text-gray-600 mb-4">Select the type of account to create:</p>
441 <div className="flex gap-2 border-b border-gray-200 mb-4">
442 {['SMTP', 'POP3', 'UPLOAD', 'FTP', 'SMS'].map((type) => (
445 onClick={() => setAccountType(type as any)}
446 className={`px-4 py-2 font-medium transition-colors ${
448 ? 'border-b-2 border-[#00946b] text-[#00946b]'
449 : 'text-gray-600 hover:text-gray-900'
452 {type === 'SMTP' && 'SMTP (Send Email)'}
453 {type === 'POP3' && 'POP3 (Receive Email)'}
454 {type === 'UPLOAD' && 'Upload Folder'}
455 {type === 'FTP' && 'FTP'}
456 {type === 'SMS' && 'SMS/Txt'}
462 <div className="space-y-4">
464 <label className="block text-sm font-medium text-gray-700 mb-1">Account Name</label>
467 value={newAccountForm.name}
468 onChange={(e) => setNewAccountForm({ ...newAccountForm, name: e.target.value })}
469 className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-[#00946b]"
470 placeholder="Enter account name"
474 {accountType === 'SMS' && (
477 <label className="block text-sm font-medium text-gray-700 mb-1">SMS Provider</label>
479 value={newAccountForm.smsProvider}
480 onChange={(e) => setNewAccountForm({ ...newAccountForm, smsProvider: e.target.value })}
481 className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-[#00946b]"
483 {SMS_PROVIDERS.map((provider) => (
484 <option key={provider.value} value={provider.value}>{provider.label}</option>
487 {selectedProvider && selectedProvider.description && (
488 <p className="mt-2 text-sm text-gray-600">{selectedProvider.description}</p>
492 {selectedProvider?.requiresEmail && (
494 <label className="block text-sm font-medium text-gray-700 mb-1">Email Address</label>
497 value={newAccountForm.smsEmail}
498 onChange={(e) => setNewAccountForm({ ...newAccountForm, smsEmail: e.target.value })}
499 className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-[#00946b]"
500 placeholder="sms@provider.com"
505 <div className="bg-blue-50 border-l-4 border-blue-400 p-4">
506 <p className="text-sm text-blue-700">
507 SMS allows you to send Txt messages from within the POS. Charges per message sent may apply.
513 {(accountType === 'SMTP' || accountType === 'POP3' || accountType === 'FTP') && (
516 <label className="block text-sm font-medium text-gray-700 mb-1">Server Address</label>
519 value={newAccountForm.server}
520 onChange={(e) => setNewAccountForm({ ...newAccountForm, server: e.target.value })}
521 className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-[#00946b]"
522 placeholder="mail.example.com"
526 <label className="block text-sm font-medium text-gray-700 mb-1">Port</label>
529 value={newAccountForm.port}
530 onChange={(e) => setNewAccountForm({ ...newAccountForm, port: e.target.value })}
531 className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-[#00946b]"
532 placeholder={accountType === 'SMTP' ? '587' : accountType === 'POP3' ? '995' : '21'}
536 <label className="block text-sm font-medium text-gray-700 mb-1">Username</label>
539 value={newAccountForm.username}
540 onChange={(e) => setNewAccountForm({ ...newAccountForm, username: e.target.value })}
541 className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-[#00946b]"
542 placeholder="username"
546 <label className="block text-sm font-medium text-gray-700 mb-1">Password</label>
549 value={newAccountForm.password}
550 onChange={(e) => setNewAccountForm({ ...newAccountForm, password: e.target.value })}
551 className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-[#00946b]"
552 placeholder="••••••••"
558 {accountType === 'UPLOAD' && (
561 <label className="block text-sm font-medium text-gray-700 mb-1">Local Directory on Server</label>
564 value={newAccountForm.directory}
565 onChange={(e) => setNewAccountForm({ ...newAccountForm, directory: e.target.value })}
566 className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-[#00946b]"
567 placeholder="C:\uploads"
570 <div className="bg-blue-50 border-l-4 border-blue-400 p-4">
571 <p className="text-sm text-blue-700">
572 An Upload account is a folder that is read on the local machine and any transactions present are automatically applied.
573 This is sometimes used when interfacing other systems.
580 <div className="mt-6 flex justify-end gap-3">
582 onClick={closeAllModals}
583 className="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50"
588 onClick={createNewAccount}
589 disabled={!newAccountForm.name}
590 className="px-4 py-2 bg-[#00946b] text-white rounded-md hover:bg-[#007055] disabled:bg-gray-400 disabled:cursor-not-allowed"
601 {showTestModal && selectedAccount && (
602 <div className="modal-overlay" onClick={closeAllModals}>
603 <div className="bg-white rounded-lg shadow-xl max-w-lg w-full" onClick={(e) => e.stopPropagation()}>
604 <div className="bg-[#00946b] text-white px-6 py-4 rounded-t-lg flex justify-between items-center">
605 <h2 className="text-xl font-semibold">{selectedAccount.f109} - Test</h2>
606 <button onClick={closeAllModals} className="text-white hover:text-gray-200 text-2xl">×</button>
609 <div className="p-6">
610 <div className="space-y-4">
612 <label className="block text-sm font-medium text-gray-700 mb-1">Phone Number</label>
616 onChange={(e) => setTestTarget(e.target.value)}
617 className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-[#00946b]"
618 placeholder="6421234567"
620 <p className="mt-1 text-sm text-gray-600">
621 Enter the full phone number including the country code. eg. To Txt a New Zealand phone, prefix the number with '64'
626 <label className="block text-sm font-medium text-gray-700 mb-1">Message</label>
629 onChange={(e) => setTestMessage(e.target.value)}
631 className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-[#00946b]"
635 <div className="bg-yellow-50 border-l-4 border-yellow-400 p-4">
636 <p className="text-sm text-yellow-700">
637 The system automatically blocks sending the exact same message to a phone number within a reasonable time frame.
638 You may need to slightly change the message in order to send a message.
643 <div className="mt-6 flex justify-end gap-3">
645 onClick={closeAllModals}
646 className="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50"
651 onClick={sendTestMessage}
652 disabled={!testTarget || !testMessage}
653 className="px-4 py-2 bg-[#00946b] text-white rounded-md hover:bg-[#007055] disabled:bg-gray-400 disabled:cursor-not-allowed"
664 {showLogModal && selectedAccount && (
665 <div className="modal-overlay" onClick={closeAllModals}>
666 <div className="bg-white rounded-lg shadow-xl max-w-3xl w-full max-h-[80vh] overflow-y-auto" onClick={(e) => e.stopPropagation()}>
667 <div className="bg-[#00946b] text-white px-6 py-4 rounded-t-lg flex justify-between items-center">
668 <h2 className="text-xl font-semibold">{selectedAccount.f109} - Log</h2>
669 <button onClick={closeAllModals} className="text-white hover:text-gray-200 text-2xl">×</button>
672 <div className="p-6">
673 <p className="text-sm text-gray-600 mb-4">Log reads bottom to top. Most recent entries are at the top</p>
675 <div className="bg-gray-50 border border-gray-200 rounded-md">
676 {selectedAccount.STAT?.[0]?.TRCE && selectedAccount.STAT[0].TRCE.length > 0 ? (
677 <table className="min-w-full">
678 <tbody className="divide-y divide-gray-200">
679 {[...selectedAccount.STAT[0].TRCE].reverse().map((entry, idx) => (
680 <tr key={idx} className="hover:bg-gray-100">
681 <td className="px-4 py-3 text-sm text-gray-700">{entry.f110}</td>
687 <div className="px-4 py-8 text-center text-gray-500">
693 <div className="mt-6 flex justify-end">
695 onClick={closeAllModals}
696 className="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50"
706 {/* Control Panel Modal */}
707 {showControlPanelModal && selectedAccount && (
708 <div className="modal-overlay" onClick={closeAllModals}>
709 <div className="bg-white rounded-lg shadow-xl max-w-lg w-full" onClick={(e) => e.stopPropagation()}>
710 <div className="bg-[#00946b] text-white px-6 py-4 rounded-t-lg flex justify-between items-center">
711 <h2 className="text-xl font-semibold">{selectedAccount.f109} Control Panel</h2>
712 <button onClick={closeAllModals} className="text-white hover:text-gray-200 text-2xl">×</button>
715 <div className="p-6">
716 <div className="flex items-center gap-3 text-gray-700">
717 {controlPanelStatus.includes('⚠️') ? (
720 <div className="inline-block animate-spin rounded-full h-6 w-6 border-b-2 border-[#00946b]"></div>
722 <p>{controlPanelStatus}</p>
725 <div className="mt-6 flex justify-end">
727 onClick={closeAllModals}
728 className="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50"