Seed database with agencies, brand guidelines, and dropdown options
Backend: - Update migration to seed agencies (OLIVER Agency, Barclays, etc.) - Seed brand guidelines (Barclays, Barclaycard) in dropdown_options - Seed channel/sub-channel/proof-type hierarchy - Add /api/agencies endpoint to list all agencies - Update DropdownOptionsResponse to include brand_guidelines - Update dropdown repository to return brand guidelines Frontend: - Update DropdownOptions interface to include brandGuidelines - CreateCampaignModal now receives brand guidelines from API - Settings UsersTab fetches agencies from API instead of hardcoded list - Add getAgencies() method to apiService 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
d080a20ee1
commit
6bdb02d78b
9 changed files with 151 additions and 32 deletions
|
|
@ -1,4 +1,4 @@
|
|||
"""Seed default dropdown options
|
||||
"""Seed default dropdown options, agencies, and brand guidelines
|
||||
|
||||
Revision ID: 002_seed_dropdown
|
||||
Revises: 001_initial
|
||||
|
|
@ -20,9 +20,44 @@ depends_on: Union[str, Sequence[str], None] = None
|
|||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Seed default channel/sub-channel/proof-type options."""
|
||||
"""Seed default channel/sub-channel/proof-type options, agencies, and brand guidelines."""
|
||||
|
||||
# Default dropdown hierarchy (from original hardcoded values)
|
||||
conn = op.get_bind()
|
||||
|
||||
# ===========================================
|
||||
# 1. Seed Agencies
|
||||
# ===========================================
|
||||
agencies = ["OLIVER Agency", "Barclays", "Mindshare", "Zenith", "Unassigned"]
|
||||
|
||||
for agency_name in agencies:
|
||||
agency_id = str(uuid4())
|
||||
conn.execute(
|
||||
sa.text("""
|
||||
INSERT INTO agencies (id, name)
|
||||
VALUES (:id, :name)
|
||||
ON CONFLICT (name) DO NOTHING
|
||||
"""),
|
||||
{"id": agency_id, "name": agency_name}
|
||||
)
|
||||
|
||||
# ===========================================
|
||||
# 2. Seed Brand Guidelines (using dropdown_options table)
|
||||
# ===========================================
|
||||
brand_guidelines = ["Barclays", "Barclaycard"]
|
||||
|
||||
for idx, brand in enumerate(brand_guidelines):
|
||||
brand_id = str(uuid4())
|
||||
conn.execute(
|
||||
sa.text("""
|
||||
INSERT INTO dropdown_options (id, option_type, parent_id, value, display_order)
|
||||
VALUES (:id, 'brand_guideline', NULL, :value, :order)
|
||||
"""),
|
||||
{"id": brand_id, "value": brand, "order": idx}
|
||||
)
|
||||
|
||||
# ===========================================
|
||||
# 3. Seed Channel/Sub-Channel/Proof-Type Hierarchy
|
||||
# ===========================================
|
||||
dropdown_data = {
|
||||
"Social": {
|
||||
"Meta": ["In-feed 1x1", "In-feed 4x5", "In-feed 9x16", "Stories"],
|
||||
|
|
@ -52,9 +87,6 @@ def upgrade() -> None:
|
|||
},
|
||||
}
|
||||
|
||||
# Get connection for raw SQL inserts
|
||||
conn = op.get_bind()
|
||||
|
||||
for channel_name, sub_channels in dropdown_data.items():
|
||||
# Insert channel
|
||||
channel_id = str(uuid4())
|
||||
|
|
@ -90,5 +122,6 @@ def upgrade() -> None:
|
|||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Remove all seeded dropdown options."""
|
||||
op.execute("DELETE FROM dropdown_options WHERE option_type IN ('channel', 'sub_channel', 'proof_type')")
|
||||
"""Remove all seeded data."""
|
||||
op.execute("DELETE FROM dropdown_options WHERE option_type IN ('channel', 'sub_channel', 'proof_type', 'brand_guideline')")
|
||||
op.execute("DELETE FROM agencies WHERE name IN ('OLIVER Agency', 'Barclays', 'Mindshare', 'Zenith', 'Unassigned')")
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ from app.api.schemas import (
|
|||
ErrorItemResponse,
|
||||
AnalyticsResponse,
|
||||
DropdownOptionsResponse,
|
||||
AgencyResponse,
|
||||
UserResponse,
|
||||
)
|
||||
from app.dependencies.auth import get_current_user
|
||||
|
|
@ -590,3 +591,20 @@ async def delete_proof_type(
|
|||
if not success:
|
||||
raise HTTPException(status_code=404, detail=f"Proof type '{proof_type}' not found")
|
||||
await db.commit()
|
||||
|
||||
|
||||
# Agency endpoints
|
||||
@router.get("/agencies", response_model=list[AgencyResponse])
|
||||
async def list_agencies(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: dict = Depends(get_current_user),
|
||||
):
|
||||
"""List all agencies."""
|
||||
from sqlalchemy import select
|
||||
from app.models.models import Agency
|
||||
|
||||
stmt = select(Agency).order_by(Agency.name)
|
||||
result = await db.execute(stmt)
|
||||
agencies = result.scalars().all()
|
||||
|
||||
return [AgencyResponse(id=a.id, name=a.name) for a in agencies]
|
||||
|
|
|
|||
|
|
@ -150,6 +150,16 @@ class AnalyticsResponse(BaseModel):
|
|||
class DropdownOptionsResponse(BaseModel):
|
||||
campaigns: list[str]
|
||||
channels: dict[str, dict[str, list[str]]]
|
||||
brand_guidelines: list[str] = []
|
||||
|
||||
|
||||
# Agency schemas
|
||||
class AgencyResponse(BaseModel):
|
||||
id: uuid.UUID
|
||||
name: str
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
# User schemas
|
||||
|
|
|
|||
|
|
@ -17,7 +17,11 @@ class DropdownRepository:
|
|||
async def get_all_hierarchical(self) -> dict:
|
||||
"""
|
||||
Get all dropdown options as a hierarchical structure.
|
||||
Returns: { channels: { channel_name: { sub_channel_name: [proof_types] } } }
|
||||
Returns: {
|
||||
channels: { channel_name: { sub_channel_name: [proof_types] } },
|
||||
brand_guidelines: [brand_names],
|
||||
campaigns: []
|
||||
}
|
||||
"""
|
||||
# Get all channels (top-level, no parent)
|
||||
stmt = select(DropdownOption).where(
|
||||
|
|
@ -28,7 +32,7 @@ class DropdownRepository:
|
|||
result = await self.session.execute(stmt)
|
||||
channels = result.scalars().all()
|
||||
|
||||
# Build hierarchical structure
|
||||
# Build hierarchical structure for channels
|
||||
hierarchy: dict[str, dict[str, list[str]]] = {}
|
||||
|
||||
for channel in channels:
|
||||
|
|
@ -41,7 +45,19 @@ class DropdownRepository:
|
|||
pt.value for pt in sub_channel.children
|
||||
]
|
||||
|
||||
return {"channels": hierarchy, "campaigns": []} # campaigns not used from dropdown options
|
||||
# Get brand guidelines
|
||||
stmt = select(DropdownOption).where(
|
||||
DropdownOption.option_type == "brand_guideline"
|
||||
).order_by(DropdownOption.display_order)
|
||||
|
||||
result = await self.session.execute(stmt)
|
||||
brand_guidelines = [bg.value for bg in result.scalars().all()]
|
||||
|
||||
return {
|
||||
"channels": hierarchy,
|
||||
"brand_guidelines": brand_guidelines,
|
||||
"campaigns": [] # campaigns not used from dropdown options
|
||||
}
|
||||
|
||||
async def add_channel(self, name: str) -> DropdownOption:
|
||||
"""Add a new channel (top-level option)."""
|
||||
|
|
|
|||
|
|
@ -25,6 +25,8 @@ export interface DropdownOptions {
|
|||
campaigns: string[];
|
||||
// Hierarchy: Channel -> SubChannel -> ProofType[]
|
||||
channels: Record<string, Record<string, string[]>>;
|
||||
// Brand guidelines for campaign creation
|
||||
brandGuidelines: string[];
|
||||
}
|
||||
|
||||
const App: React.FC = () => {
|
||||
|
|
@ -48,7 +50,8 @@ const App: React.FC = () => {
|
|||
// Dropdown options now loaded from API
|
||||
const [dropdownOptions, setDropdownOptions] = useState<DropdownOptions>({
|
||||
campaigns: [],
|
||||
channels: {}
|
||||
channels: {},
|
||||
brandGuidelines: []
|
||||
});
|
||||
|
||||
// Load dropdown options from API when authenticated
|
||||
|
|
@ -60,7 +63,8 @@ const App: React.FC = () => {
|
|||
const options = await apiService.getDropdownOptions();
|
||||
setDropdownOptions({
|
||||
campaigns: options.campaigns || [],
|
||||
channels: options.channels || {}
|
||||
channels: options.channels || {},
|
||||
brandGuidelines: options.brand_guidelines || []
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to load dropdown options:', error);
|
||||
|
|
@ -70,7 +74,8 @@ const App: React.FC = () => {
|
|||
channels: {
|
||||
"Social": { "Meta": ["In-feed 1x1", "In-feed 4x5"] },
|
||||
"Display": { "Banner": ["300x600", "300x250"] }
|
||||
}
|
||||
},
|
||||
brandGuidelines: ["Barclays", "Barclaycard"]
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -419,7 +424,8 @@ const App: React.FC = () => {
|
|||
const options = await apiService.getDropdownOptions();
|
||||
setDropdownOptions({
|
||||
campaigns: options.campaigns || [],
|
||||
channels: options.channels || {}
|
||||
channels: options.channels || {},
|
||||
brandGuidelines: options.brand_guidelines || []
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to refresh dropdown options:', error);
|
||||
|
|
|
|||
|
|
@ -1311,10 +1311,11 @@ export const Campaigns: React.FC<CampaignsProps> = ({
|
|||
|
||||
return (
|
||||
<>
|
||||
<CreateCampaignModal
|
||||
<CreateCampaignModal
|
||||
isOpen={isModalOpen}
|
||||
onClose={() => setIsModalOpen(false)}
|
||||
onAddCampaign={onAddNewCampaign}
|
||||
brandGuidelines={dropdownOptions.brandGuidelines}
|
||||
/>
|
||||
<CampaignList
|
||||
onSelectCampaign={onSelectCampaign}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { XIcon } from './icons/XIcon';
|
||||
import apiService from '../services/apiService';
|
||||
|
||||
interface CreateCampaignModalProps {
|
||||
isOpen: boolean;
|
||||
|
|
@ -11,11 +12,12 @@ interface CreateCampaignModalProps {
|
|||
clientLead: string;
|
||||
brandGuidelines: string;
|
||||
}) => void;
|
||||
brandGuidelines?: string[];
|
||||
}
|
||||
|
||||
export const CreateCampaignModal: React.FC<CreateCampaignModalProps> = ({ isOpen, onClose, onAddCampaign }) => {
|
||||
export const CreateCampaignModal: React.FC<CreateCampaignModalProps> = ({ isOpen, onClose, onAddCampaign, brandGuidelines: brandGuidelineOptions = [] }) => {
|
||||
const [name, setName] = useState('');
|
||||
const [brandGuidelines, setBrandGuidelines] = useState('');
|
||||
const [selectedBrandGuideline, setSelectedBrandGuideline] = useState('');
|
||||
const [workfrontId, setWorkfrontId] = useState('');
|
||||
const [clientLead, setClientLead] = useState('');
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
|
@ -24,7 +26,7 @@ export const CreateCampaignModal: React.FC<CreateCampaignModalProps> = ({ isOpen
|
|||
// Reset form when modal opens
|
||||
if (isOpen) {
|
||||
setName('');
|
||||
setBrandGuidelines('');
|
||||
setSelectedBrandGuideline('');
|
||||
setWorkfrontId('');
|
||||
setClientLead('');
|
||||
setError(null);
|
||||
|
|
@ -51,7 +53,7 @@ export const CreateCampaignModal: React.FC<CreateCampaignModalProps> = ({ isOpen
|
|||
e.preventDefault();
|
||||
const isIdValidOnSubmit = /^#WF_\d+$/.test(workfrontId);
|
||||
|
||||
if (!name.trim() || !clientLead.trim() || !workfrontId.trim() || !brandGuidelines.trim()) {
|
||||
if (!name.trim() || !clientLead.trim() || !workfrontId.trim() || !selectedBrandGuideline.trim()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -59,15 +61,15 @@ export const CreateCampaignModal: React.FC<CreateCampaignModalProps> = ({ isOpen
|
|||
setError("Workfront Campaign ID must be in the format '#WF_12345'");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
setError(null);
|
||||
onAddCampaign({ name, workfrontId, clientLead, brandGuidelines });
|
||||
onAddCampaign({ name, workfrontId, clientLead, brandGuidelines: selectedBrandGuideline });
|
||||
onClose();
|
||||
};
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const isFormInvalid = !name.trim() || !workfrontId.trim() || !clientLead.trim() || !brandGuidelines.trim();
|
||||
|
||||
const isFormInvalid = !name.trim() || !workfrontId.trim() || !clientLead.trim() || !selectedBrandGuideline.trim();
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
@ -103,14 +105,15 @@ export const CreateCampaignModal: React.FC<CreateCampaignModalProps> = ({ isOpen
|
|||
<label htmlFor="brand-guidelines" className="block text-sm font-medium text-gray-700">Brand Guidelines</label>
|
||||
<select
|
||||
id="brand-guidelines"
|
||||
value={brandGuidelines}
|
||||
onChange={(e) => setBrandGuidelines(e.target.value)}
|
||||
value={selectedBrandGuideline}
|
||||
onChange={(e) => setSelectedBrandGuideline(e.target.value)}
|
||||
className="mt-1 block w-full p-2 border border-gray-300 rounded-md shadow-sm focus:ring-brand-accent focus:border-brand-accent transition bg-white text-gray-900"
|
||||
required
|
||||
>
|
||||
<option value="" disabled>Select brand guidelines</option>
|
||||
<option value="Barclays">Barclays</option>
|
||||
<option value="Barclaycard">Barclaycard</option>
|
||||
{brandGuidelineOptions.map((brand) => (
|
||||
<option key={brand} value={brand}>{brand}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import type { DropdownOptions } from '../App';
|
||||
import apiService from '../services/apiService';
|
||||
import { TrashIcon } from './icons/TrashIcon';
|
||||
import { UserIcon } from './icons/UserIcon';
|
||||
import { PlusIcon } from './icons/PlusIcon';
|
||||
|
|
@ -95,16 +96,36 @@ const INITIAL_USERS: User[] = [
|
|||
{ id: '3', name: "Sarah Jenkins", email: "sarah.jenkins@mindshare.com", agency: "Mindshare" },
|
||||
];
|
||||
|
||||
const AGENCIES = ["OLIVER Agency", "Barclays", "Mindshare", "Zenith", "Unassigned"];
|
||||
// Fallback agencies if API fails
|
||||
const DEFAULT_AGENCIES = ["OLIVER Agency", "Barclays", "Mindshare", "Zenith", "Unassigned"];
|
||||
|
||||
const UsersTab: React.FC = () => {
|
||||
const [users, setUsers] = useState<User[]>(() => {
|
||||
const saved = localStorage.getItem('barclays_modcomms_users');
|
||||
return saved ? JSON.parse(saved) : INITIAL_USERS;
|
||||
});
|
||||
const [agencies, setAgencies] = useState<string[]>(DEFAULT_AGENCIES);
|
||||
const [newName, setNewName] = useState('');
|
||||
const [newEmail, setNewEmail] = useState('');
|
||||
const [newAgency, setNewAgency] = useState(AGENCIES[0]);
|
||||
const [newAgency, setNewAgency] = useState('');
|
||||
|
||||
// Load agencies from API
|
||||
useEffect(() => {
|
||||
const loadAgencies = async () => {
|
||||
try {
|
||||
const response = await apiService.getAgencies();
|
||||
const agencyNames = response.map(a => a.name);
|
||||
setAgencies(agencyNames.length > 0 ? agencyNames : DEFAULT_AGENCIES);
|
||||
if (agencyNames.length > 0 && !newAgency) {
|
||||
setNewAgency(agencyNames[0]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load agencies:', error);
|
||||
setAgencies(DEFAULT_AGENCIES);
|
||||
}
|
||||
};
|
||||
loadAgencies();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem('barclays_modcomms_users', JSON.stringify(users));
|
||||
|
|
@ -174,7 +195,7 @@ const UsersTab: React.FC = () => {
|
|||
onChange={(e) => setNewAgency(e.target.value)}
|
||||
className="w-full p-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-brand-accent focus:border-brand-accent appearance-none"
|
||||
>
|
||||
{AGENCIES.map(a => <option key={a} value={a}>{a}</option>)}
|
||||
{agencies.map(a => <option key={a} value={a}>{a}</option>)}
|
||||
</select>
|
||||
<ChevronDownIcon className="absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-500 pointer-events-none" />
|
||||
</div>
|
||||
|
|
@ -223,7 +244,7 @@ const UsersTab: React.FC = () => {
|
|||
onChange={(e) => handleAgencyChange(user.id, e.target.value)}
|
||||
className="w-full bg-white border border-gray-300 text-gray-700 py-1.5 pl-3 pr-8 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-brand-accent appearance-none"
|
||||
>
|
||||
{AGENCIES.map(a => <option key={a} value={a}>{a}</option>)}
|
||||
{agencies.map(a => <option key={a} value={a}>{a}</option>)}
|
||||
</select>
|
||||
<ChevronDownIcon className="absolute right-2 top-1/2 -translate-y-1/2 h-3 w-3 text-gray-500 pointer-events-none" />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -352,11 +352,22 @@ class ApiService {
|
|||
method: 'DELETE',
|
||||
});
|
||||
}
|
||||
|
||||
// Agency endpoints
|
||||
async getAgencies(): Promise<AgencyResponse[]> {
|
||||
return this.fetch<AgencyResponse[]>('/agencies');
|
||||
}
|
||||
}
|
||||
|
||||
export interface DropdownOptionsResponse {
|
||||
campaigns: string[];
|
||||
channels: Record<string, Record<string, string[]>>;
|
||||
brand_guidelines: string[];
|
||||
}
|
||||
|
||||
export interface AgencyResponse {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export const apiService = new ApiService();
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue