Replace entire repo content with code from /root/shahikitchen-google/
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
/** Google accounts allowed to access Menu Management (/admin). */
|
||||
|
||||
const MENU_MANAGER_EMAILS = new Set([
|
||||
'imraswe@gmail.com',
|
||||
'zeeshanatnorth@gmail.com',
|
||||
]);
|
||||
|
||||
export function normalizeEmail(email: string): string {
|
||||
return email.trim().toLowerCase();
|
||||
}
|
||||
|
||||
export function isMenuManagerEmail(email: string | null | undefined): boolean {
|
||||
if (!email) return false;
|
||||
return MENU_MANAGER_EMAILS.has(normalizeEmail(email));
|
||||
}
|
||||
@@ -1,8 +1,12 @@
|
||||
import type { OrderLine } from '../shared/order-line';
|
||||
import type { BookingMode, EventTypeId } from './event-types';
|
||||
|
||||
export type BranchLocation = 'askim' | 'backaplan' | '';
|
||||
|
||||
export interface BookingDetails {
|
||||
bookingMode: BookingMode;
|
||||
eventType: EventTypeId | '';
|
||||
eventTypeOther: string;
|
||||
location: BranchLocation;
|
||||
date: string;
|
||||
time: string;
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
export type BookingMode = 'table' | 'event';
|
||||
|
||||
export type EventTypeId =
|
||||
| 'birthday'
|
||||
| 'wedding'
|
||||
| 'engagement'
|
||||
| 'anniversary'
|
||||
| 'baby-shower'
|
||||
| 'graduation'
|
||||
| 'office-party'
|
||||
| 'corporate-lunch'
|
||||
| 'family-gathering'
|
||||
| 'religious-celebration'
|
||||
| 'farewell'
|
||||
| 'retirement'
|
||||
| 'bridal-shower'
|
||||
| 'holiday-party'
|
||||
| 'other';
|
||||
|
||||
export interface BookingEventType {
|
||||
id: EventTypeId;
|
||||
icon:
|
||||
| 'cake'
|
||||
| 'heart'
|
||||
| 'gem'
|
||||
| 'sparkles'
|
||||
| 'baby'
|
||||
| 'graduation-cap'
|
||||
| 'briefcase'
|
||||
| 'users'
|
||||
| 'party-popper'
|
||||
| 'plane'
|
||||
| 'award'
|
||||
| 'gift'
|
||||
| 'tree-pine'
|
||||
| 'more-horizontal';
|
||||
}
|
||||
|
||||
export const BOOKING_EVENT_TYPES: readonly BookingEventType[] = [
|
||||
{ id: 'birthday', icon: 'cake' },
|
||||
{ id: 'wedding', icon: 'heart' },
|
||||
{ id: 'engagement', icon: 'gem' },
|
||||
{ id: 'anniversary', icon: 'sparkles' },
|
||||
{ id: 'baby-shower', icon: 'baby' },
|
||||
{ id: 'graduation', icon: 'graduation-cap' },
|
||||
{ id: 'office-party', icon: 'briefcase' },
|
||||
{ id: 'corporate-lunch', icon: 'users' },
|
||||
{ id: 'family-gathering', icon: 'users' },
|
||||
{ id: 'religious-celebration', icon: 'sparkles' },
|
||||
{ id: 'farewell', icon: 'plane' },
|
||||
{ id: 'retirement', icon: 'award' },
|
||||
{ id: 'bridal-shower', icon: 'gift' },
|
||||
{ id: 'holiday-party', icon: 'tree-pine' },
|
||||
{ id: 'other', icon: 'more-horizontal' },
|
||||
] as const;
|
||||
|
||||
export const TABLE_GUEST_OPTIONS = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10'] as const;
|
||||
|
||||
export const EVENT_GUEST_OPTIONS = [
|
||||
'2',
|
||||
'3',
|
||||
'4',
|
||||
'5',
|
||||
'6',
|
||||
'7',
|
||||
'8',
|
||||
'9',
|
||||
'10',
|
||||
'10-15',
|
||||
'16-25',
|
||||
'26-40',
|
||||
'41-60',
|
||||
'61-80',
|
||||
'81-100',
|
||||
'100+',
|
||||
] as const;
|
||||
@@ -1,12 +1,18 @@
|
||||
import type { BookingDetails } from './entities';
|
||||
|
||||
export function isBookingComplete(booking: BookingDetails): boolean {
|
||||
const hasEventType =
|
||||
booking.bookingMode === 'table' ||
|
||||
(booking.eventType !== '' &&
|
||||
(booking.eventType !== 'other' || booking.eventTypeOther.trim().length > 0));
|
||||
|
||||
return !!(
|
||||
booking.location &&
|
||||
booking.date &&
|
||||
booking.time &&
|
||||
booking.guests &&
|
||||
booking.name &&
|
||||
booking.phone
|
||||
booking.phone &&
|
||||
hasEventType
|
||||
);
|
||||
}
|
||||
+62
-3
@@ -1,11 +1,70 @@
|
||||
import { PICKUP_LEAD_TIME_MINUTES } from '@/domain/shared/constants';
|
||||
|
||||
export type FulfillmentMode = 'pickup' | 'delivery';
|
||||
|
||||
export interface PickupInquiryDetails {
|
||||
export type InquiryBranch = 'askim' | 'backaplan' | '';
|
||||
|
||||
export const DEFAULT_INQUIRY_BRANCH: InquiryBranch = 'backaplan';
|
||||
|
||||
export type InquiryPreferredDate = 'today' | 'tomorrow' | '';
|
||||
|
||||
export type InquiryScheduleValidation =
|
||||
| 'ok'
|
||||
| 'incomplete'
|
||||
| 'too_soon'
|
||||
| 'invalid_time';
|
||||
|
||||
export interface InquirySchedule {
|
||||
preferredDate: InquiryPreferredDate;
|
||||
preferredTime: string;
|
||||
}
|
||||
|
||||
export interface PickupInquiryDetails extends InquirySchedule {
|
||||
name: string;
|
||||
phone: string;
|
||||
email: string;
|
||||
branch: InquiryBranch;
|
||||
}
|
||||
|
||||
export interface DeliveryInquiryDetails extends PickupInquiryDetails {
|
||||
address: string;
|
||||
preferredTime: string;
|
||||
}
|
||||
|
||||
export function isInquiryBranchOnlineEnabled(branch: InquiryBranch): boolean {
|
||||
return branch === 'backaplan';
|
||||
}
|
||||
|
||||
export function getMinInquiryTimeForToday(now: Date = new Date()): string {
|
||||
const min = new Date(now.getTime() + PICKUP_LEAD_TIME_MINUTES * 60 * 1000);
|
||||
return `${String(min.getHours()).padStart(2, '0')}:${String(min.getMinutes()).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
export function validateInquirySchedule(
|
||||
schedule: InquirySchedule,
|
||||
now: Date = new Date(),
|
||||
): InquiryScheduleValidation {
|
||||
const hasDate = schedule.preferredDate === 'today' || schedule.preferredDate === 'tomorrow';
|
||||
const hasTime = schedule.preferredTime.trim().length > 0;
|
||||
|
||||
if (!hasDate && !hasTime) return 'ok';
|
||||
if (hasDate !== hasTime) return 'incomplete';
|
||||
|
||||
const [hours, minutes] = schedule.preferredTime.split(':').map(Number);
|
||||
if (Number.isNaN(hours) || Number.isNaN(minutes)) return 'invalid_time';
|
||||
|
||||
const scheduled = new Date(now);
|
||||
if (schedule.preferredDate === 'tomorrow') {
|
||||
scheduled.setDate(scheduled.getDate() + 1);
|
||||
}
|
||||
scheduled.setHours(hours, minutes, 0, 0);
|
||||
|
||||
const minTime = new Date(now.getTime() + PICKUP_LEAD_TIME_MINUTES * 60 * 1000);
|
||||
if (scheduled.getTime() < minTime.getTime()) return 'too_soon';
|
||||
|
||||
return 'ok';
|
||||
}
|
||||
|
||||
export function hasInquirySchedule(schedule: InquirySchedule): boolean {
|
||||
return validateInquirySchedule(schedule) === 'ok' &&
|
||||
schedule.preferredDate !== '' &&
|
||||
schedule.preferredTime.trim() !== '';
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/** Göteborg municipality postcodes used for delivery validation. */
|
||||
const GOTHENBURG_POSTCODE_PREFIXES = ['41', '42', '43'] as const;
|
||||
|
||||
const GOTHENBURG_NAME_PATTERNS = [
|
||||
'göteborg',
|
||||
'gothenburg',
|
||||
'goteborg',
|
||||
'göteborgs stad',
|
||||
'goteborgs stad',
|
||||
] as const;
|
||||
|
||||
export interface DeliverableAddressFields {
|
||||
city?: string;
|
||||
municipality?: string;
|
||||
postcode?: string;
|
||||
countryCode?: string;
|
||||
formatted?: string;
|
||||
}
|
||||
|
||||
function normalize(value: string): string {
|
||||
return value.trim().toLowerCase();
|
||||
}
|
||||
|
||||
function matchesGothenburgName(value: string): boolean {
|
||||
const normalized = normalize(value);
|
||||
return GOTHENBURG_NAME_PATTERNS.some((pattern) => normalized.includes(pattern));
|
||||
}
|
||||
|
||||
function hasGothenburgPostcode(postcode?: string): boolean {
|
||||
if (!postcode) return false;
|
||||
const digits = postcode.replace(/\s/g, '');
|
||||
return GOTHENBURG_POSTCODE_PREFIXES.some((prefix) => digits.startsWith(prefix));
|
||||
}
|
||||
|
||||
export function isGothenburgDeliveryAddress(fields: DeliverableAddressFields): boolean {
|
||||
if (fields.countryCode && normalize(fields.countryCode) !== 'se') return false;
|
||||
|
||||
if (matchesGothenburgName(fields.city ?? '')) return true;
|
||||
if (matchesGothenburgName(fields.municipality ?? '')) return true;
|
||||
if (hasGothenburgPostcode(fields.postcode)) return true;
|
||||
|
||||
const formatted = normalize(fields.formatted ?? '');
|
||||
if (
|
||||
formatted.includes('göteborg') ||
|
||||
formatted.includes('gothenburg') ||
|
||||
formatted.includes('goteborg')
|
||||
) {
|
||||
return (
|
||||
hasGothenburgPostcode(fields.postcode) ||
|
||||
/\b41\d{3}\b|\b42\d{3}\b|\b43\d{3}\b/.test(formatted)
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import type { MenuCategory } from './entities';
|
||||
|
||||
export type MenuVersionTrigger = 'auto' | 'manual' | 'restore';
|
||||
|
||||
export interface MenuVersionMeta {
|
||||
id: string;
|
||||
label: string;
|
||||
createdAt: string;
|
||||
trigger: MenuVersionTrigger;
|
||||
categoryCount: number;
|
||||
dishCount: number;
|
||||
isBaseline: boolean;
|
||||
}
|
||||
|
||||
export interface MenuVersionSnapshot {
|
||||
id: string;
|
||||
label: string;
|
||||
createdAt: string;
|
||||
trigger: MenuVersionTrigger;
|
||||
categories: MenuCategory[];
|
||||
isBaseline: boolean;
|
||||
}
|
||||
|
||||
export interface MenuVersionListItem extends MenuVersionMeta {
|
||||
matchesLive: boolean;
|
||||
}
|
||||
|
||||
export const MENU_BASELINE_VERSION_ID = 'baseline' as const;
|
||||
Reference in New Issue
Block a user