EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
page.tsx
Go to the documentation of this file.
1"use client";
2
3import React, { useEffect, useState } from "react";
4import { apiClient } from "@/lib/client/apiClient";
5
6interface LoyaltyCampaign {
7 f100: number; // ID
8 f101: string; // Name
9 f102?: string; // Start Date
10 f103?: string; // Earn End Date
11 f114?: string; // Redeem End Date
12 f104?: number; // Reward Level
13 f105?: number; // Money Reward Level
14 f106?: number; // Gain (multiplier)
15 f107?: number; // Input Pid
16 f108?: number; // Output Pid
17 f109?: number; // Membership Type
18 f113?: number; // Active Days
19}
20
21export default function LoyaltyCampaignsPage() {
22 const [campaigns, setCampaigns] = useState<LoyaltyCampaign[]>([]);
23 const [loading, setLoading] = useState(false);
24 const [error, setError] = useState<string>("");
25 const [showNewCampaignModal, setShowNewCampaignModal] = useState(false);
26 const [saving, setSaving] = useState(false);
27 const [searchTerm, setSearchTerm] = useState("");
28
29 const [newCampaign, setNewCampaign] = useState({
30 name: "",
31 startDate: "",
32 earnEndDate: "",
33 redeemEndDate: "",
34 rewardLevel: "",
35 moneyRewardLevel: "",
36 gain: "1",
37 inputPid: "",
38 outputPid: "",
39 membershipType: "1", // All Customers by default
40 activeDays: ""
41 });
42
43 useEffect(() => {
44 loadCampaigns();
45 }, []);
46
47 const loadCampaigns = async () => {
48 setLoading(true);
49 setError("");
50 try {
51 const response = await apiClient.getBuckData({ table: 'retailmax.elink.loyalty.campaign', want: 'all' });
52
53 if (response.success && response.data) {
54 const appData = response.data.APPD || [];
55 const mapped: LoyaltyCampaign[] = appData.map((item: any) => ({
56 f100: Number(item.f100) || 0,
57 f101: item.f101 || "",
58 f102: item.f102 || "",
59 f103: item.f103 || "",
60 f114: item.f114 || "",
61 f104: Number(item.f104) || 0,
62 f105: Number(item.f105) || 0,
63 f106: Number(item.f106) || 1,
64 f107: Number(item.f107) || 0,
65 f108: Number(item.f108) || 0,
66 f109: Number(item.f109) || 0,
67 f113: Number(item.f113) || 0
68 }));
69 setCampaigns(mapped);
70 } else if (response.success) {
71 // API succeeded but no data - this is OK, just means no campaigns exist
72 setCampaigns([]);
73 } else {
74 // API call failed
75 setError("Error loading campaigns. Please try again.");
76 }
77 } catch (err) {
78 console.error("Failed to load loyalty campaigns", err);
79 setError("Error loading campaigns. Please try again.");
80 } finally {
81 setLoading(false);
82 }
83 };
84
85 const handleCreateCampaign = async () => {
86 if (!newCampaign.name.trim()) {
87 alert("Please enter a campaign name");
88 return;
89 }
90
91 setSaving(true);
92 try {
93 // Create campaign using DATI API
94 const xml = [
95 '<DATI>',
96 '<f8_s>retailmax.elink.loyalty.campaign</f8_s>',
97 '<f11_B>I</f11_B>',
98 `<f101_s>${newCampaign.name.replace(/&/g, '&amp;').replace(/</g, '&lt;')}</f101_s>`,
99 newCampaign.startDate ? `<f102_s>${newCampaign.startDate}</f102_s>` : '',
100 newCampaign.earnEndDate ? `<f103_s>${newCampaign.earnEndDate}</f103_s>` : '',
101 newCampaign.redeemEndDate ? `<f114_s>${newCampaign.redeemEndDate}</f114_s>` : '',
102 newCampaign.rewardLevel ? `<f104_E>${newCampaign.rewardLevel}</f104_E>` : '',
103 newCampaign.moneyRewardLevel ? `<f105_E>${newCampaign.moneyRewardLevel}</f105_E>` : '',
104 `<f106_E>${newCampaign.gain || 1}</f106_E>`,
105 newCampaign.inputPid ? `<f107_E>${newCampaign.inputPid}</f107_E>` : '',
106 newCampaign.outputPid ? `<f108_E>${newCampaign.outputPid}</f108_E>` : '',
107 `<f109_E>${newCampaign.membershipType}</f109_E>`,
108 newCampaign.activeDays ? `<f113_E>${newCampaign.activeDays}</f113_E>` : '',
109 '</DATI>'
110 ].join('');
111
112 const response = await fetch('/dati', {
113 method: 'POST',
114 headers: { 'Content-Type': 'application/xml', 'Accept': 'application/json' },
115 credentials: 'include',
116 body: xml
117 });
118
119 if (response.ok) {
120 alert("Campaign created successfully!");
121 setShowNewCampaignModal(false);
122 setNewCampaign({
123 name: "",
124 startDate: "",
125 earnEndDate: "",
126 redeemEndDate: "",
127 rewardLevel: "",
128 moneyRewardLevel: "",
129 gain: "1",
130 inputPid: "",
131 outputPid: "",
132 membershipType: "1",
133 activeDays: ""
134 });
135 loadCampaigns();
136 }
137 } catch (err) {
138 console.error("Failed to create campaign", err);
139 alert("Failed to create campaign. Please try again.");
140 } finally {
141 setSaving(false);
142 }
143 };
144
145 const filteredCampaigns = campaigns.filter(c =>
146 !searchTerm ||
147 c.f101.toLowerCase().includes(searchTerm.toLowerCase()) ||
148 String(c.f100).includes(searchTerm)
149 );
150
151 const formatDate = (dateStr?: string) => {
152 if (!dateStr) return "-";
153 try {
154 const date = new Date(dateStr);
155 if (isNaN(date.getTime())) return dateStr;
156 return date.toLocaleDateString("en-NZ", {
157 day: "2-digit",
158 month: "short",
159 year: "numeric"
160 });
161 } catch {
162 return dateStr;
163 }
164 };
165
166 return (
167 <div className="p-6">
168 {/* Header */}
169 <div className="mb-6">
170 <h1 className="text-3xl font-bold mb-2">Loyalty Campaigns 🎁</h1>
171 <p className="text-muted">Manage customer loyalty and rewards programs</p>
172 </div>
173
174 {/* Action Bar */}
175 <div className="bg-surface rounded-lg shadow p-4 mb-6">
176 <div className="flex flex-wrap gap-4 items-center justify-between">
177 <div className="flex gap-3">
178 <button
179 onClick={() => setShowNewCampaignModal(true)}
180 className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 font-medium"
181 >
182 New Campaign
183 </button>
184 <button
185 onClick={loadCampaigns}
186 disabled={loading}
187 className="px-4 py-2 border border-border rounded-lg hover:bg-surface-2 disabled:opacity-50"
188 >
189 {loading ? "Loading..." : "Refresh"}
190 </button>
191 </div>
192
193 <div className="flex gap-2 items-center">
194 <input
195 type="text"
196 placeholder="Search campaigns..."
197 value={searchTerm}
198 onChange={(e) => setSearchTerm(e.target.value)}
199 className="px-3 py-2 border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-green-600"
200 />
201 <span className="text-sm text-muted">
202 Displaying {filteredCampaigns.length}
203 </span>
204 </div>
205 </div>
206 </div>
207
208 {/* Error Message */}
209 {error && (
210 <div className="mb-4 p-4 bg-red-50 border-l-4 border-red-500 text-red-700">
211 {error}
212 </div>
213 )}
214
215 {/* Campaigns Table */}
216 <div className="bg-surface rounded-lg shadow overflow-hidden">
217 {loading ? (
218 <div className="p-8 text-center">
219 <div className="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-green-600"></div>
220 <p className="mt-2 text-muted">Loading campaigns...</p>
221 </div>
222 ) : (
223 <div className="overflow-x-auto">
224 <table className="w-full text-sm">
225 <thead className="bg-surface-2 sticky top-0">
226 <tr>
227 <th className="px-4 py-3 text-left font-semibold">ID</th>
228 <th className="px-4 py-3 text-left font-semibold">Name</th>
229 <th className="px-4 py-3 text-left font-semibold">Start Date</th>
230 <th className="px-4 py-3 text-left font-semibold">Earn End</th>
231 <th className="px-4 py-3 text-left font-semibold">Redeem End</th>
232 <th className="px-4 py-3 text-right font-semibold">Reward Level</th>
233 <th className="px-4 py-3 text-right font-semibold">$ Reward</th>
234 <th className="px-4 py-3 text-right font-semibold">Gain</th>
235 <th className="px-4 py-3 text-center font-semibold">Options</th>
236 </tr>
237 </thead>
238 <tbody className="divide-y divide-gray-200">
239 {filteredCampaigns.length === 0 ? (
240 <tr>
241 <td colSpan={9} className="px-4 py-12 text-center">
242 <div className="flex flex-col items-center justify-center">
243 <div className="text-6xl mb-4">🎁</div>
244 <h3 className="text-lg font-semibold text-text mb-2">
245 {searchTerm ? "No campaigns match your search" : "No loyalty campaigns yet"}
246 </h3>
247 {!searchTerm && (
248 <>
249 <p className="text-muted mb-4">
250 Create your first loyalty campaign to reward your customers
251 </p>
252 <button
253 onClick={() => setShowNewCampaignModal(true)}
254 className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 font-medium"
255 >
256 Create Campaign
257 </button>
258 </>
259 )}
260 </div>
261 </td>
262 </tr>
263 ) : (
264 filteredCampaigns.map((campaign) => (
265 <tr key={campaign.f100} className="hover:bg-surface-2">
266 <td className="px-4 py-3 font-mono text-sm text-green-600">
267 {campaign.f100}
268 </td>
269 <td className="px-4 py-3 font-medium">
270 {campaign.f101}
271 </td>
272 <td className="px-4 py-3 text-muted">
273 {formatDate(campaign.f102)}
274 </td>
275 <td className="px-4 py-3 text-muted">
276 {formatDate(campaign.f103)}
277 </td>
278 <td className="px-4 py-3 text-muted">
279 {formatDate(campaign.f114)}
280 </td>
281 <td className="px-4 py-3 text-right">
282 {campaign.f104 || "-"}
283 </td>
284 <td className="px-4 py-3 text-right">
285 {campaign.f105 ? `$${campaign.f105.toFixed(2)}` : "-"}
286 </td>
287 <td className="px-4 py-3 text-right">
288 {campaign.f106 ? `${campaign.f106}x` : "1x"}
289 </td>
290 <td className="px-4 py-3 text-center">
291 <div className="flex gap-2 justify-center">
292 <button className="px-3 py-1 bg-brand text-white rounded hover:bg-brand/90 text-sm font-medium">
293 Edit
294 </button>
295 <button className="px-3 py-1 bg-surface-2 text-text rounded hover:bg-surface-2 text-sm font-medium">
296 View
297 </button>
298 </div>
299 </td>
300 </tr>
301 ))
302 )}
303 </tbody>
304 </table>
305 </div>
306 )}
307 </div>
308
309 {/* New Campaign Modal */}
310 {showNewCampaignModal && (
311 <div
312 className="fixed inset-0 bg-black/30 z-50 flex items-start justify-center p-4 overflow-y-auto"
313 onClick={() => setShowNewCampaignModal(false)}
314 >
315 <div
316 className="bg-surface rounded-lg shadow-xl max-w-2xl w-full my-8"
317 onClick={(e) => e.stopPropagation()}
318 >
319 <div className="border-b border-border p-4 flex items-center justify-between">
320 <h2 className="text-xl font-bold text-green-600">Create New Loyalty Campaign</h2>
321 <button
322 onClick={() => setShowNewCampaignModal(false)}
323 className="px-3 py-1 text-sm border border-border rounded hover:bg-surface-2"
324 >
325 Close
326 </button>
327 </div>
328
329 <div className="p-6 space-y-4">
330 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
331 <div className="md:col-span-2">
332 <label className="block text-sm font-medium text-text mb-1">
333 Name of Campaign *
334 </label>
335 <input
336 type="text"
337 value={newCampaign.name}
338 onChange={(e) => setNewCampaign({...newCampaign, name: e.target.value})}
339 className="w-full px-3 py-2 border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-green-600"
340 placeholder="e.g., Summer Rewards 2025"
341 />
342 <p className="text-xs text-muted mt-1">Used to name the campaign on your reports</p>
343 </div>
344
345 <div>
346 <label className="block text-sm font-medium text-text mb-1">
347 Start Date
348 </label>
349 <input
350 type="date"
351 value={newCampaign.startDate}
352 onChange={(e) => setNewCampaign({...newCampaign, startDate: e.target.value})}
353 className="w-full px-3 py-2 border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-green-600"
354 />
355 </div>
356
357 <div>
358 <label className="block text-sm font-medium text-text mb-1">
359 Earn End Date
360 </label>
361 <input
362 type="date"
363 value={newCampaign.earnEndDate}
364 onChange={(e) => setNewCampaign({...newCampaign, earnEndDate: e.target.value})}
365 className="w-full px-3 py-2 border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-green-600"
366 />
367 </div>
368
369 <div>
370 <label className="block text-sm font-medium text-text mb-1">
371 Redeem End Date
372 </label>
373 <input
374 type="date"
375 value={newCampaign.redeemEndDate}
376 onChange={(e) => setNewCampaign({...newCampaign, redeemEndDate: e.target.value})}
377 className="w-full px-3 py-2 border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-green-600"
378 />
379 </div>
380
381 <div>
382 <label className="block text-sm font-medium text-text mb-1">
383 Reward Level
384 </label>
385 <input
386 type="number"
387 value={newCampaign.rewardLevel}
388 onChange={(e) => setNewCampaign({...newCampaign, rewardLevel: e.target.value})}
389 className="w-full px-3 py-2 border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-green-600"
390 placeholder="e.g., 100"
391 />
392 </div>
393
394 <div>
395 <label className="block text-sm font-medium text-text mb-1">
396 Money Reward Level
397 </label>
398 <input
399 type="number"
400 step="0.01"
401 value={newCampaign.moneyRewardLevel}
402 onChange={(e) => setNewCampaign({...newCampaign, moneyRewardLevel: e.target.value})}
403 className="w-full px-3 py-2 border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-green-600"
404 placeholder="e.g., 10.00"
405 />
406 </div>
407
408 <div>
409 <label className="block text-sm font-medium text-text mb-1">
410 Gain (Multiplier)
411 </label>
412 <input
413 type="number"
414 step="0.1"
415 value={newCampaign.gain}
416 onChange={(e) => setNewCampaign({...newCampaign, gain: e.target.value})}
417 className="w-full px-3 py-2 border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-green-600"
418 />
419 <p className="text-xs text-muted mt-1">Points multiplier (e.g., 2 = double rewards)</p>
420 </div>
421
422 <div>
423 <label className="block text-sm font-medium text-text mb-1">
424 Membership Type
425 </label>
426 <select
427 value={newCampaign.membershipType}
428 onChange={(e) => setNewCampaign({...newCampaign, membershipType: e.target.value})}
429 className="w-full px-3 py-2 border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-green-600"
430 >
431 <option value="0">Individually add Customers</option>
432 <option value="1">All Customers Included</option>
433 </select>
434 </div>
435
436 <div>
437 <label className="block text-sm font-medium text-text mb-1">
438 Input Pid
439 </label>
440 <input
441 type="number"
442 value={newCampaign.inputPid}
443 onChange={(e) => setNewCampaign({...newCampaign, inputPid: e.target.value})}
444 className="w-full px-3 py-2 border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-green-600"
445 />
446 </div>
447
448 <div>
449 <label className="block text-sm font-medium text-text mb-1">
450 Output Pid
451 </label>
452 <input
453 type="number"
454 value={newCampaign.outputPid}
455 onChange={(e) => setNewCampaign({...newCampaign, outputPid: e.target.value})}
456 className="w-full px-3 py-2 border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-green-600"
457 />
458 </div>
459
460 <div>
461 <label className="block text-sm font-medium text-text mb-1">
462 Active Days
463 </label>
464 <input
465 type="number"
466 value={newCampaign.activeDays}
467 onChange={(e) => setNewCampaign({...newCampaign, activeDays: e.target.value})}
468 className="w-full px-3 py-2 border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-green-600"
469 placeholder="e.g., 365"
470 />
471 <p className="text-xs text-muted mt-1">Days of inactivity before customer considered inactive</p>
472 </div>
473 </div>
474
475 <div className="pt-4 border-t border-border flex justify-end gap-3">
476 <button
477 onClick={() => setShowNewCampaignModal(false)}
478 className="px-4 py-2 border border-border rounded-lg hover:bg-surface-2"
479 disabled={saving}
480 >
481 Cancel
482 </button>
483 <button
484 onClick={handleCreateCampaign}
485 disabled={saving || !newCampaign.name.trim()}
486 className="px-6 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed font-medium"
487 >
488 {saving ? "Saving..." : "Save Campaign"}
489 </button>
490 </div>
491 </div>
492 </div>
493 </div>
494 )}
495 </div>
496 );
497}