Replace entire repo content with code from /root/shahikitchen-google/

This commit is contained in:
root
2026-07-03 02:51:15 +00:00
parent c8b7dc95e4
commit 8ccf033329
79 changed files with 7611 additions and 439 deletions
+15
View File
@@ -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));
}
+4
View File
@@ -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;
+76
View File
@@ -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;
+7 -1
View File
@@ -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
View File
@@ -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() !== '';
}
+55
View File
@@ -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;
}
+28
View File
@@ -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;